关于内存泄漏的问题
关于内存泄漏的问题
什么是内存泄漏
内存泄漏:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
关于OOM
下面是可能出现的一些报错信息。
-
java.lang.OutOfMemoryError: Java heap space//所需堆内存不足
-
java.lang.OutOfMemoryError: PermGen space//永久代满了,永久代存储的是类信息,如果加载了大量类,会有这个问题
-
java.lang.OutOfMemoryError: Requested array size exceeds VM limit//尝试分配的内存太大
-
java.lang.OutOfMemoryError: request bytes for . Out of swap space?
通常是操作系统层面的原因导致 java.lang.OutOfMemoryError: Out of swap space? 问题, 例如:
- 操作系统的交换空间太小。
- 机器上的某个进程耗光了所有的内存资源。
- 当然也可能是应用程序的本地内存泄漏(native leak)引起的, 例如, 某个程序/库不断地申请本地内存,却不进行释放。
-
java.lang.OutOfMemoryError: Native method
如果您看到此错误消息并且堆栈跟踪的顶部框架是本机方法,则该本机方法遇到分配失败。此消息与上一个消息之间的区别在于,在JNI或本机方法中检测到Java内存分配失败,而不是在Java VM代码中检测到。
需要注意的是,OOM的原因并不一定是内存泄漏,也有可能是分配的推内存太小之类的问题。
而内存泄漏通常会导致OOM的问题。
内存泄漏的原因
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。
Java内存泄漏的根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。
//长生命周期的Vector引用了短生命周期的Object,尽管将o设置为null,但是GC并不会回收
Vector v = new Vector(10);
for (int i = 0; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
可以看下图,A引用B,尽管B的生命周期到期了,但是B并不会被回收。
静态集合类引起内存泄漏
由于静态集合的生命周期和应用程序是一样的,但是就比如刚才的例子,就会引起内存泄漏,解决方法就是及时将无用的静态变量设为null。
static Vector v = new Vector(10);
for (int i = 0; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
监听器
各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close() 方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try 里面去的连接,在finally里面释放连接。
内部类和外部模块的引用
单例模式
防止内存泄漏的发生
尽早释放无用对象的引用
检查内存泄漏的工具
可以使用VisualVM,这是一款可以追踪JVM内部对象分配的工具。
可以看到,这个工具可以监测多方面的问题,可以是CPU,可以是堆内存,可以是类,也可以是线程。
当然可以安装VisualGC插件,进一步观测堆内存中,新生代,老年代的内存分配情况,这对我们分析java内存泄漏有比较大的帮助。
下面提供一个检查的例子
import java.util.HashMap;
import java.util.Map;
class A {
int x = 10;
}
public class CyclicDependencies {
//声明缓存对象
private static final Map map = new HashMap();
public static void main(String args[]) {
try {
Thread.sleep(10000);//给打开visualvm时间
} catch (InterruptedException e) {
e.printStackTrace();
}
//循环添加对象到缓存
for (int i = 0; i < 1000000; i++) {
map.put("key" + i, new A());
}
System.out.println("first");
//为dump出堆提供时间
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 1000000; i++) {
map.put("key" + i, new A());
}
System.out.println("second");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 3000000; i++) {
map.put("key" + i, new A());
}
System.out.println("third");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 4000000; i++) {
map.put("key" + i, new A());
}
System.out.println("forth");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("qqqq");
}
}
/*
JVM执行下述内容
-Xms512m -Xmx512m
-XX:-UseGCOverheadLimit
-XX:MaxPermSize=50m
*/
通过VisualVM运行上述代码
每隔10s可以点击堆Dump 按钮,这个的功能是生成当前堆分配的一个快照,如果我们发现最后堆空间长时间占用很大,且点击执行垃圾回收没有反应,那么有可能就发生了内存泄漏。我们可以通过比较堆Dump前后生成的快照,查看究竟是哪些对象一直在占用内存。
这是一个堆dump的页面记录
这是两份快照的比较,我们可以看到A的对象一直在生成,String对象也在一直增多,还有HashMap$Node,那么我们就可以推断是否是HashMap产生了内存溢出。
那么答案是是的,因为我们我们没有重写equal方法,HashMap是按照地址来判断是否是相等的两个对象,而我们的操作也是一直new 新的对象,很明显是产生了内存泄漏。