Java的内存回收

Java是面向对象的编程语言,一个Java程序往往需要创建大量的Java类,然后对各Java类创建大量的Java对象,再调用这些Java对象的属性和方法来操作它们。

程序员可以通过关键字 new 创建Java对象,即可视作为Java对象申请内存空间,JVM会在堆内存上为每个对象分配内存空间。当一个Java对象失去引用时,JVM的垃圾回收机制会自动回收它们,并释放它们占用的内存空间。

垃圾回收机制具有如下特征:

  • 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(如数据库连接、网络IO的资源)
  • 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候运行。当对象永久性失去引用后,系统就会在合适的时候回收该对象所占用的内存。
  • 在垃圾回收机制回收对象之前,总会调用该对象的 finalize() 方法,该方法可能使该对象重新获得引用变量的引用,从而导致垃圾回收机制取消回收。 当一个对象失去引用后,系统何时调用该对象的 finalize() 方法对它进行资源清理,何时它会变为不可达状态,系统何时回收它所占用的内存,对程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,但无法控制该对象何时被回收。

finalize方法具有如下 4 个特点:

  • 永远不要主动调用某个对象的 finalize() 方法,该方法应交给垃圾回收机制调用。
  • finalize方法何时被调用,是否被调用具有不确定性,不要把finalize方法当成一定会执行的方法。
  • 当JVM执行可恢复对象的 finalize 方法时,可能使该对象或系统中其他对象重新变成可达状态
  • 当JVM执行 finalize 方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。

注意:因为 finalize 方法不一定会执行,故要清理某个对象里打开的资源,则不要放在 finalize 方法中进行清理。

我们也可以强制系统进行垃圾回收,但系统是否进行垃圾回收依然不确定,强制系统进行垃圾回收有一下两种方法:

  • 调用 System 类的 gc() 静态方法: System.gc();
  • 调用 Runtime 对象的 gc() 实例方法:Runtime.getRuntime().gc();

对象再内存中的状态

对于JVM的垃圾回收机制来说,是否回收一个对象的标准在于:是否还有引用变量引用该对象?只要有引用变量还引用该变量,垃圾回收机制就不会回收它。

也就是说,当Java对象被创建出来以后,垃圾回收机制会实时监控每个对象的运行状态,包括对象的申请、引用、被引用、赋值等。当垃圾回收机制实时的监控到某个对象不在被引用变量引用时,垃圾回收机制就会回收它所占用的内存空间。

JVM的垃圾回收机制采用有向图的方式来管理内存中的对象,方便的解决了循环引用的问题,管理对象具有较高的精确度,但缺点是效率低。

当一个对象在堆内存中运行时,根据它在对象有向图的状态,可以把它所处的状态分为以下三种:

  • 可达状态:当一个对象被创建后,有一个以上的引用变量引用它。在有向图中可以从起始顶点导航到该对象(因为Java中的所有对象是有线程创建出来的,所以把线程对象当成有向图的起始顶点),那么它处于可达状态,程序可以通过引用变量来调用该对象的属性和方法。
  • 可恢复状态:如果程序中的某个对象不再有任何引用变量引用它,它将进入可恢复状态,此时从有向图的起始顶点不能导航到该对象。在这种情况下,系统的垃圾回收机制准备回收该对象占用的内存。但是在回收之前,系统会调用可恢复状态的对象的 finalize 方法进行资源清理,如果系统调用 finalize 方法重新让一个以上的引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象就会进入不可达状态。
  • 不可达状态:当对象的所有关联都被切断,且系统调用所有对象的 finalize 方法依然没有使该对象变为可达状态,这个对象将永久地失去引用,最后变成不可达状态。当只有一个对象处于不可达状态时,JVM的垃圾回收机制才会真正回收该对象,并释放该对象占用的内存资源。

一个对象可以被一个方法的局部变量引用,也可以被其他类的类变量引用,或者被其他对象的实例变量引用。当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态。当某个对象被其他对象的实例变量引用时,只有引用该对象的对象被销毁或者变为不可达状态后,该对象才会进入可恢复状态。

对于JVM的垃圾回收机制来说,判断一个对象是否可回收的标准就在于该对象是否被引用,因此引用也是JVM进行内存管理的一个重要概念。

  • 强引用
  • 软引用(SoftReference)
  • 弱引用(WeakReference)
  • 虚引用(PhantomReference)

强引用

程序通 new 关键字创建一个新的Java对象,并把该对象赋给一个引用变量,这个引用变量就是强引用了。

被强引用引用的Java对象绝对不会被JVM的垃圾回收机制回收,即使内存非常紧张;即使有些Java对象以后都不会被用到,JVM也不会回收被强引用引用的Java对象。

由于JVM肯定不会回收被强引用引用的对象,因此强引用是造成Java内存泄露的主要原因之一。

软引用

软引用通过 SoftReference 类实现,当一个对象只有软引用时,它可能被垃圾回收机制回收。对于一个只有软引用的对象而言,当系统内存空间足够时,它不会被回收,程序也可使用该对象;当系统内存不足时,该对象将被回收。

弱引用

弱引用与软引用有点相似,区别在于弱引用所引用的对象的生存期更短。弱引用通过 WeakReference 类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当JVM的垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存空间。当然并不是说当一个对象只用弱引用时它就会被回收————正如那些失去引用的对象一样,必须等到JVM的垃圾回收机制运行才会被回收。

注意: 弱引用具有很大的不确定性,因为每次垃圾回收机制运行时,都会回收弱引用所引用的对象,而垃圾回收机制的运行又不受程序的控制,因此程序获取弱引用所引用的Java对象时,必须小心空指针异常————通过弱引用获得的Java对象可能是 null。

虚引用

软引用和弱引用可以单独使用,但虚引用不能单独使用,单独使用虚引用没有太大的意义,虚引用的主要作用是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用的对象是否即将被回收。

引用队列用 java.lang.ref.ReferenceQueue 类表示,它用于保存被回收会对象的引用。当把软引用、弱引用和引用队列联合使用时,系统回收被引用的对象之后,将会把被回收对象的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被回收之前,将把它对应的虚引用添加到它关联的引用队列中,这使得可以在对象被回收之前采取行动。

虚引用通过 PhantomReference 类实现,它完全类似于没有引用。虚引用对对象本身没有太大的影响,对象甚至感觉不到虚引用的存在。如果一个对象只用一个虚引用,那么他和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列联合使用。

垃圾回收机制

// TODO: