Overview
- Java一个重要特性就是通过GC自动管理内存的回收,而不需要程序员自己来释放内存。因而,理论上Java中所有不再利用的对象所占用的内存都可以被GC会回收。但事实上,Java也存在内存泄露。
Java中的内存管理
- 要了解Java中的内存泄露,首先就得知道Java中的内存是如何管理的。
- Java使用可达性分析来判断对象是否可以被清理,详见link。
Java中的内存泄露
- 内存泄露就是指:不会再被使用的对象的内存没有(也不再能够)被释放。
- 不同于C++中程序员必须手动释放所分配的对象内存,Java不用也不能自己释放内存。
- 如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。
例子如下:
public class Simple {
Object object;
public void method1() {
object = new Object();
// ...else
}
}
(长生命周期的对象(Simple对应的实例对象))持有短生命周期的引用(object引用))
上述代码就是一个典型的例子(突然想起之前code review的时候,董老师always让我在用对象的时候才定义那个引用,一来可以避免上述现象,二来也使对象作用更清晰~)
- 这里的object实例,我们只期望它作用于method1()中,其他地方不会再用。但是,在method1执行完后,object所分配的内存不会马上被认为是可以被释放的对象。只有在Simple类创建的对象释放后才会被释放。严格的说,这里就是一种内存泄露。
- 解决的方法就是将object作为method1()中的局部变量。或者也可以:
public class Simple {
Object object;
public void method1() {
object = new Object();
// ...else
object = null;
}
}
- Summary:
- 在堆中分配的内存,在没有将其释放掉时,就将所有能访问这块内存的方式都删掉,这是C++等需要自己垃圾回收的语言,Java中不需要担心。
- 在内存对象已经不再需要的时候,还仍然保留着这块内存和它的引用,这是所有语言都可能出现的内存泄露方式。编程时如果不小心,我们很容易发生这种情况,如果不太严重,可能就只是短暂的内存泄露。
Examples & Solutions
- 上述例子的解决原则就是 --> 尽量减小对象的作用域。
- 及时将无用对象标记为可被清理的对象。
静态集合类
- 像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有对象Object也不能被释放。
- 例如:
Static Vector v = new Vector(10);
for(int i = 0; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
上述例子中,虽然原引用o被释放,但对象本身仍然被Vector引用,所以该对象对GC来说是不可回收的。只能通过v = null的方式来回收。
Closeable
- 数据库连接、网络连接和io连接等,除非显示调用其close方法将其连接关闭,否则是不会自动被GC回收的。其原因仍然是长生命周期对象持有短生命周期对象的引用。
- 比如,操作数据库时我们会通过SessionFactory获取一个session:
Session session = SessionFactory.openSession();
完成后:session.close()
SessionFactory就是一个长生命周期的对象,而session相对是个短生命周期的对象。框架这样设计是合理的:它并不清除我们要使用session到多久,于是只能提供一个方法让我们决定何时不再使用。
Singleton
- 单例对象通常生命周期与整个程序的生命周期差不多,所以是一个长生命周期地对象。如果该对象持有其他对象的引用,也很容易发生内存泄露。
与清理相关的方法
gc()
- 对程序员而言,gc基本是透明的,不可见的。运行gc的函数是System.gc(),调用后启动垃圾回收器开始清理。
- 但是根据java的语义,该函数不保证GC一定会执行。通常GC的线程优先级较低。
finalize()
- finalize()是Object类中的方法。
- finalize绝不等于C++中的析构方法。
- 一旦GC准备好释放对象所占用的存储空间,将先调用其finalize()方法,并在下一次GC回收动作发生时,才会真正回收对象占用的内存。因此,一些清理工作可被放到finalize()中。
- 该方法的一个重要作用就是:当在java中调用非java代码时,在这些非java代码中可能会用到相应的申请内存的操作,而在这些非java代码中并没有有效的释放这些内存,就可以使用finalize()方法,并在里面调用本地方法的free()等函数。
- finalize()并不适合用作普通的清理工作。
FYI
- Java内存泄露详解.