这里简要介绍几种垃圾收集算法的思想
标记 - 清除算法
该算法如同它的名字一样,分为“标记”和“清除”两个阶段:
- 首先标记出所有需要回收的对象
- 在标记完后统一回收所有被标记的对象
这个算法其实已经过时了,但是后续的算法都是基于这种思路来的。它主要的不足点有两个:
- 效率问题。标记和清理两个过程的效率都不高
- 空间问题。标记清除后会产生大量不连续的内存碎片,空间碎片太对会导致程序运行过程中需要分配大对象时,无法找到连续的内存而不得不提前触发另一次垃圾收集动作
复制算法
复制算法的流程如下:
- 它将可用内存按容量大小划分为大小相等的两块,每次只使用其中一块。
- 当这块的内存用完了,就将还存活着的对象复制到另一块上面,
- 然后把使用过的内存空间一次性清理掉。
可以看到每次只对一半区域进行收集,这样就不用考虑内存碎片等复杂情况了,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。但是这种算法的代价是将内存缩小为原来的一半,内存成本高
复制算法一般用于收集新生代,因为新生代大部分的对象的存活时间很短,因此新生代中存活的对象远远少于垃圾对象。
新生代:存放年轻对象的堆空间。年轻对象是指刚刚创建,或者经历垃圾回收次数不多的对象。
老年代:存放老年对象的堆空间。老年对象指经历过多次垃圾回收依然存活的对象。
在商业虚拟机中,例如我们常见的HotSpot虚拟机,将新生代分为一个Eden区和两个Survivor区,Eden区与Survivor区的大小比例是8:1,也即是说Eden区占新生代的80%,两个Survivor分别占10%。新生代的复制算法执行规则如下:
- 每次使用复制算法进行垃圾回收时,会将Eden区和其中一块Survivor区的所有存活对象复制到另一块空闲Survivor区中,在复制操作中,大对象和老年对象将直接复制到老年代;
- 然后将原来的Eden区和Survivor区的对象一次性清理掉;
- 如果在执行复制算法时一块空闲Survivor区域不能够容纳原来的Eden区和Survivor区的对象,就需要依赖老年代,将多余的对象直接复制到老年代。
可以发现,这种复制机制保证只有一块Survivor区的内存(仅占新生代内存的10%)是被浪费的。新生代的复制算法示意图如下:
标记 - 整理算法
在对象存活率较低的新生代使用复制算法效率高。那么在对象存活率高的老年代,使用复制算法效率将会变得很低。根据老年代的特点,有人提出了“标记 - 整理”算法。算法流程如下:
- 首先标记出所有需要回收的对象
- 让所有存活的对象都向一端移动
- 然后清理掉端边界以外的内存
分代收集算法
当前商业虚拟机的垃圾收集算法都采用“分代收集算法”。主要思想是根据对象存活周期的不同将内存划分为几块,并采用最适合的收集算法。
- 在大批对象死去,少量存活的新生代中,采用复制算法
- 在对象存活率高、没有额外空间对它进行分配担保,采用“标记 - 清理”或“标记 - 整理”算法。
OopMap、Safe Point和Safe Region
上面介绍了几种垃圾收集算法,但是虚拟机(这里以HotSpot为例子)在发起内存回收的时候会遇到很多问题。因此诞生了OopMap、Safe Point和Safe Region来解决
OopMap
问题:
- GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,但是现在引用众多,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
- 另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行————这里“一致性”是指分析过程中不可以出现引用关系还在不断变化的情况,因此GC进行时必须停顿所有的Java执行线程
解决: 在HotSpot的实现中,使用一组成为OopMap的数据结构。
- 在类加载完成的时候,就把对象内什么偏移量上是什么类型的数据计算出来
- 在JIT编译过程中,也会在特定位置记录下栈和寄存器中哪些位置是引用
这样,GC在扫描时就可以直接得知这些信息了。
Safe Point
在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:
- OopMap内容变化的指令过多导致需要大量额外空间的问题
解决:
- HotSpot没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safe Point),即程序执行时只有在到达Safe Point时才能更新自己的OopMap。
对于Safe Point,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都运行到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension):
- 抢先式中断。不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它继续运行到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
- 主动式中断。当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
Safe Region
Safe Point机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safe Point。 问题:
- 但是当线程没有分配CPU时间(如线程处于Sleep状态或者Blocked状态),这时候线程无法响应JVM的中断请求以继续到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。