之前介绍了Java内存运行时区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈这3个区域随线程而生随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或线程结束的时候,内存就自然跟着回收了。

因此我们接下来讨论的内存分配与回收特指Java堆和方法区

对象已死?

垃圾回收器在对堆进行回收前,第一件事就是要确认哪些对象需要被回收也就是已经死去

引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的

在Java领域主流的虚拟机并没有使用引用计数法来管理内存,主要原因是这个看似简单的算法有很多例外需要考虑,比如单纯的引用计数就很难解决对象之间循环引用的问题。

可达性分析算法

当前主流的商用程序语言(Java,C#)的内存管理子系统都是通过可达性分析算法来判定对象是否存活的。

这个算法的思路就是通过一系列成为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,证明此对象不可能再被使用

垃圾收集器_虚引用

在Java中,固定可以作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如当前正在运行的方法所使用到的参数,局部变量,临时变量
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量(static)
  • 在方法区中常量引用的对象,譬如字符串常量池里的引用,final
  • 在本地方法栈中Native方法引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,还有系统类加载器
  • 所有被同步锁(synchronized 关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等

除了这些固定的GC Roots对象外根据用户当前选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。

引用

在JDK1.2以前,Java中引用的定义很传统: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。这种定义有些狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。

我们希望能描述这一类对象: 当内存空间还足够时,则能保存在内存中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。

在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps:强引用其实也就是我们平时A a = new A()这个意思。

软引用

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。JDK 1.2以后提供了SoftReference类来实现软引用

弱引用

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。JDK 1.2以后提供了WeakReference类来实现弱引用

虚引用

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来在对象被回收时收到一个通知。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

在JDK 1.2以后提供了PhantomReference类来实现虚引用

生存还是死亡?

即使在可达性分析算法中判定为不可达的对象,也不意味着对象真正死亡,要宣告一个对象真正死亡需要经过两次标记过程。

第一次标记发生在对对象进行可达性分析时,发现没有与GC Roots相连的引用链,那么他将会被第一次标记。然后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,如果对象没有finalize()方法或者finalize()方法已经被虚拟机调用过,则认为没有必要执行。如果有必要执行finalize()方法,那么该对象会被放在一个F-Queue的队列之中,并在稍后由一条虚拟机自动创建的,低调度优先级的Finalizer线程执行他们的finalize()方法

如果对象在finalize()方法中重新和引用链上的对象建立关联,那么在第二次标记是会被移出“即将回收”的集合,否则就会被真正的回收。

需要注意的是,任何对象的finalize()方法只会被系统调用一次!所以不建议把finalize()作为拯救对象的方法!

方法区垃圾回收

实际上方法区的垃圾回收“性价比”是很低的。

方法区的垃圾回收主要有两部分内容:废弃的常量和不再使用的类型信息。

判断一个常量是否需要回收看虚拟机中有没有地方引用这个常量(即值为这个常量),没有的话就会被清理出常量池。

判断一个类型信息是否需要回收,需要满足三个条件:

  • 该类的所有实例被回收,也就是说Java堆中不存在该类极其子类的实例
  • 加载该类的类加载器被回收,这个条件除非是精心设计的可替换类加载器场景,否则通常是很难达成的
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类

在满足了这三个条件后,Java虚拟机允许对无用类进行回收,仅仅是允许,而不是必须。具体回收与否取决于虚拟机的设置


我有一壶酒 足以慰风尘 尽倾江海里 赠饮天下人