在编写文章前,有几个问题需要思考一下:
内存如何划分?对象如何分配内存?哪些内存需要回收?在哪个节点回收?如何回收?当前商业虚拟机采用分代思想管理内存,接下来的部分已 HotSpot 虚拟机的内存分配为分析模型,在 HotSpot 里,Java 堆中可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor空间(8 : 1 : 1)等。在 JDK 1.7之前,使用永久代实现方法区,在1.7之后,逐步改为采用 Native Memory 来实现方法区的规划了。
通过一系列称为 "GC Roots" 的对象作为起点,从这些节点开始向下搜索,当一个对象没有和任何引用链相连时,则说明该对象不可用。
可作为 GC Roots 对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)引用的对象。本地方法栈中(一般说的 Native 方法)引用的对象。方法区中类静态属性引用的对象。方法区中常量引用的对象。引用类型:
强引用:在程序代码中普遍存在的,类似 "Object obj = new Object()" 这类的引用,只要强引用还存在,垃圾收集器不会回收被引用的对象。软引用:用来描述一些有用但非必须对象。对于软引用关联着的对象,垃圾收集器运行时可能会(可能不会)回收软引用对象。对象是否被回收取决于垃圾收集器的算法以及垃圾收集器运行时可用的内存数量。JDK1.2 之后提供了 SoftReference 类来实现软引用。弱引用:用来描述非必须对象。被弱引用关联着的对象只能生存到下一次垃圾回收发生之前。JDK1.2 之后提供了 WeakReference 类来实现软引用。虚引用:最弱的引用关系。对象的虚引用完全不会对其生命周期构成影响,也无法通过虚引用来取得一个对象的实例。JDK1.2 之后提供了 PhantomReference 类来实现软引用。垃圾收集器回收的是到 GC Roots 的引用链不可达对象,在可达性分析时必须在确保一致性的快照中进行。不可以出现在分析过程中对象的引用还在不断变化的情况。这就会导致在 GC 运行时必须停顿所有 Java 执行线程(Stop the world)。所以在枚举根节点操作中必须要停顿的。
在触发 GC 回收时让线程在哪里停下来?如何让线程停下来?程序执行时并非在任何地方都可以停顿下来开始 GC,只有在到达安全点是才能停顿。安全点的选定基本上是以程序 "是否让程序具有长时间执行的特征" 为标准进行选定。例如方法调用、循环跳转、异常跳转等。
在 GC 发生时,可以通过两种选择让线程跑到安全点停顿下来:抢先式中断和主动式中断。抢先式中断先把所有线程全部中断,发现中断的地方不在安全点上,就恢复线程,让它继续运行到安全点上;主动式中断设置一个标志位,线程执行时让线程主动轮询这个标志位,发现中断标志位为真是就中断挂起。
通过上面分析知道 GC 发生时执行线程会在安全点上中断,如果线程不执行(处于 sleep 或 block 状态),无法响应 JVM 中断请求,运行到安全点上中断挂起。对于这种情况就需要安全区域来解决。
安全区域指的是一段代码片段中,引用关系不会发生变化。在这个区域中的任何地方开始 GC 都是安全的。线程执行到安全区域中的代码时,标识自己已经入安全区域,当 GC 发生时,就不需要处理这些线程。当线程要离开安全区域时,首先要检查系统是否完成了根节点的枚举,如果完成,线程就继续执行,否则必须等到收到可以离开安全区域信号为止。
当前商业虚拟机采用分代思想管理内存,根据不同代采用不同的分配策略和垃圾回收算法。不同代垃圾回收触发机制可能也会不一样。新生代采用复制策略,当需要分配的内存不足时就触发新生代 GC(Minor GC)。老年代会根据系统设置的空间使用比例参数来触发老年代 GC(Major GC)。
标记所有需要回收的对象,标记完成后统一回收被标记的对象。
标记所有需要回收的对象,让所有存活的对象都向一端移动,然后清理掉端边界以外的数据。
将内存划分为大小相等的两块,每次只是用一块,当这一块内存用完时,就将还存活的对象复制到另一块内存上,然后再把已使用过的内存空间清理掉。将内存划分为一块较大的 Eden 区和两块较小的 Survivor 区(8:1:1),使用 Eden 和其中一块 Survivor 存放新对象,发生内存回收时把 Eden 和 Survivor 存活的对象复制到另一块 Survivor 空间上。如果出现存活对象大小大于 Survivor 空间大小的极端情况时,使用空间分配担保来把不能容纳的对象存放到 Tenured 空间。
根据对象存活周期不同将内存划分为几块,再根据各个代的特点采用最适当的收集算法。