文章目录
- 简介
- 何为内存泄漏
- 内存泄漏带来的问题
- 导致内存泄漏的原因
- 内存泄漏分析常用手段
- 内存泄漏分析与实践
- 静态字段导致的内存泄漏
- 不正确的hashCode和equals实现
- 内部类导致的内存泄漏
- 小节面试分析
简介
何为内存泄漏
动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占据该内存单元,直到程序结束。这个现象称之为内存泄漏。因此良好的代码规范,可以有效地避免这些错误。
内存泄漏带来的问题
1)长时间运行,程序会变卡,性能严重下降。
2)OutOfMemoryError错误,系统直接挂掉。
导致内存泄漏的原因
1)大量使用静态变量(静态变量与程序生命周期一样)
2)IO/连接资源用完没关闭(记得执行close操作)
3)内部类的使用方式存在问题(实力内部类或默认引用外部类对象)
4)缓存(Cache)应用不当(尽量不要使用强引用)
5)ThreadLocal应用不当(用完记得执行remove操作)
内存泄漏分析常用手段
- 应用内存分析工具 JProfiler, YourKit, Java VisualVM等。
- 在开发阶段时或者在测试环节,增加压力测试。
- 认真对待开发工具给出的告警提示,该关闭的资源尽早关闭。
- 选择合适的时机进行代码 review。
通俗地说,我们可以将内存泄漏视为一种疾病,如果不治愈,随着时间的推移,它可能导致致命的应用程序崩溃。内存泄漏很难解决,发现它们需要对 Java 语言的复杂掌握和掌握。在处理内存泄漏时,没有一种万能的解决方案,因为泄漏可能通过各种不同的事件发生。
但是,如果我们采用最佳实践并定期执行严格的代码排查和分析,那么我们可以将应用程序中内存泄漏的风险降到最低。
内存泄漏分析与实践
静态字段导致的内存泄漏
演示通过静态字段造成的内存泄漏,代码如下:
package com.java.jvm.leak;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class StaticMemoryLeakTests {
// 使用静态变量去存储大量的数据
public static List<byte[]> bytes = new ArrayList<>();
public void makeBytes() {
for (int i = 0; i < 1000 * 200; i++) {
bytes.add(new byte[1024]);
}
System.out.println("Debug point 2");
}
public static void main(String[] args)throws Exception {
// 给时间我打开 visualVM 监控工具
TimeUnit.SECONDS.sleep(10);
System.out.println("Debug point 1");
new StaticMemoryLeakTests().makeBytes();
System.out.println("Debug point 3");
for (; ; ) {
}
}
}
运行代码,此时打开jdk/bin目录的jvisualvm,基于VisualVM 分析这段程序执行期间堆内存的使用情况。当程序运行从Debug point 1 到 Debug point 2 的时候,如预期的那样,堆内存增加了。 但当我们从Debug point 3 离开 makeBytes() 方法时,此时 JVM 不一定达到了触发 GC 的条件, 可以手动执行 Perform GC, 发现占用的Heap 内存并没有得到释放。
演示非static属性值的存储以及内存分析,代码如下:
package com.java.jvm.leak;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class NoStaticMemoryLeakTests {
// 使用非静态变量去存储大量的数据
public List<byte[]> bytes = new ArrayList<>();
public void makeBytes() {
for (int i = 0; i < 1000 * 200; i++) {
bytes.add(new byte[1024]);
}
System.out.println("Debug point 2");
}
public static void main(String[] args)throws Exception {
// 给时间我打开 visualVM 监控工具
TimeUnit.SECONDS.sleep(10);
System.out.println("Debug point 1");
new NoStaticMemoryLeakTests().makeBytes();
System.out.println("Debug point 3");
for (; ; ) {
}
}
}
运行代码,此时打开jdk/bin目录的jvisualvm,基于VisualVM 分析这段程序执行期间堆内存的使用情况。当程序运行从Debug point 1 到 Debug point 2 的时候,如预期的那样,堆内存增加了。 但当我们从Debug point 3 离开 makeBytes() 方法时,此时 JVM 不一定达到了触发 GC 的条件, 可以手动执行 Perform GC, 发现占用的Heap 内存被释放了。
不正确的hashCode和equals实现
当我们将一些pojo对象作为HashMap的key或者直接将pojo对象存储到HashSet集合时,假如没有正确重写equals() 和 hashCode()方法,可能就会出现内存泄漏问题。例如:
当我们将一些pojo对象作为HashMap的key或者直接将pojo对象存储到HashSet集合时,假如没有正确重写equals() 和 hashCode()方法,可能就会出现内存泄漏问题。例如:
package com.java.jvm.leak;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
class Pig {
private String name;
public Pig(String name) {
this.name = name;
}
}
public class IncorrectHashAndEqualsTests {
public static void main(String[] args) throws Exception{
TimeUnit.SECONDS.sleep(10);
Map<Pig, Integer> pigs = new HashMap<>();
for (int i = 0; i < 10000 * 1000; i++) {
pigs.put(new Pig("佩奇"), i);
TimeUnit.MILLISECONDS.sleep(2);
}
System.out.println(pigs.size());
}
}
一般存储到缓存的对象,都要重写hashCode()和 equals()方法。假设我们没有覆写,后果真的严重。
在Pig类中重写HashCode和equals方法,例如:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pig pig = (Pig) o;
return Objects.equals(name, pig.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
内部类导致的内存泄漏
在应用程序中使用这个内部类的对象不当,导致的内存泄漏,例如:
package com.java.jvm.leak;
class OuterClass {
private int o;
private byte[] bigObject = new byte[1024 * 10];
class InnerClass {
private int i;
int add() {
return i++;
}
}
}
public class InnerLeakTests {
public static void main(String[] args) {
OuterClass o = new OuterClass();
OuterClass.InnerClass innerClass = o.new InnerClass();
innerClass.add();
o=null;
System.gc();
}
}
小节面试分析
1)何为内存泄漏?
2) 内存泄漏可能会带来什么问题?
3)导致内存泄漏的原因有哪些?
4) 内存泄漏分析常用的手段有哪些?