分代收集理论
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。一般将Java堆分为新生代和老年代,可以根据各个年代的特点选择合适的垃圾收集算法。
部分收集(Partial GC): 指⽬标不是完整收集整个Java堆的垃圾收集,其中又分为:
1.新生代收集(Minor GC/Young GC): 指目标只是新生代的垃圾收集。
2.老年代收集(Major GC/Old GC): 指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
3.混合收集(Mixed GC): 指⽬标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
4.整堆收集(Full GC): 收集整个Java堆和⽅法区的垃圾收集。
比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记清除"或标记-整理”算法进行垃圾收集。“标记清除"或标记-整理”算法会比复制算法慢10倍以上。
标记-清除(Mark-Sweep)算法:
算法分为标记和清除阶段:标记存活的对象,统一回收所有未被标记的对象(一般选择这种):也可以反过来,标记出所有需要回收的对像,在标记完成后统一回收所有被标记的对像。它是最基础的收集算法,比较简单,但是会带来两个明显的问题:
1.效率问题(如果需要标记的对象太多,效率不高)
2.空间问题(标记清除后会产生大量不连续的碎片)
标记-复制算法
为了解决效率问题,复制收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把便用的空间一次清理掉。这样就便每次的内存回收都是对内存区间的一半进行回收。
商⽤Java虚拟机⼤多都优先采⽤了这种收集算法去回收新⽣代。半区复制分代策略,称“Appel式回收”。HotSpot虚拟机的Serial、ParNew等新⽣代收集器均采⽤了这种策略来设计新⽣代的内存布局。
Appel式回收的具体做法是把新生代分为⼀块较⼤的Eden空间和两块较小的Survivor空间,每次分配内存只使⽤Eden和其中⼀块Survivor。发⽣垃圾搜集时,将Eden和 Survivor中仍然存活的对象⼀次性复制到另外⼀块Survivor空间上,然后直接清理掉Eden和已⽤过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的⼤⼩⽐例是8∶1,也 即每次新⽣代中可⽤内存空间为整个新⽣代容量的90%(Eden的80%加上⼀个Survivor的10%),只有⼀个Survivor空间,即10%的新⽣代是会被“浪费”的。当Survivor空间不足容纳⼀次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是⽼年代)进⾏分配担保(Handle Promotion)。如果另外⼀块Survivor空间没有足够空间存放上⼀次新⽣代收集下来的存活对象,这些对象便将通过分配担保机制直接进⼊老年代,这对虚拟机来说就是安全的。
标记整理(Mark-Compact)算法
根据老年代的特点特出的一种标记算法,标记过程仍然与标记清除算法一样,但后续不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
标记清除与标记整理的区别就在于是否移动回收后存活的对象
如果像标记整理算法移动存活对象,尤其是在⽼年代这种每次回收都有⼤量对象存活区域,移动存活对象并更新所有引⽤这些对象这些操作必须全程暂停⽤户应⽤程序(STW)才能进⾏。
如果像标记清除算法不移动存活对象,造成的空间碎片化只能依赖复杂的内存分配器和内存访问器来解决,会影响到程序的吞吐量。
是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚⾄可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。 HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,⽽关注延迟的CMS收集器则是基于标记-清除算法的。
五、HotSpot的算法实现细节
根节点枚举:
作为GC Roots的节点,这个分析工作必须确保一致性的快照,来保证分析结果的准确性,不可以出现分析过程中对象引用关系还在不断变化的情况。停顿用户线程STW。在OopMap(一种数据结构)的协助下,HotSpot不需要全部查找,而是能够直接得到哪些地方存放对象引用,可以快速准确地完成GC Roots枚举,
安全点(SafePoint):
实际上HotSpot也的确没有为每条指令都⽣成OopMap,前⾯已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点。有了安全点的设定,也就决定了用户程序执⾏时并⾮在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执⾏到达安全点后才能够暂停。
安全区域(Safe Region):
指在一段代码片段之中,引用关系不用发生变化,在这个区域中的任何地方开始GC都是安全的。
记忆集(Remember Set)和卡表(Card table):
新生代做GCroots可达性扫描过程中可能碰到跨代引用的对象,在这种情况下去对整个老年代扫描效率太低,所以垃圾收集器在新生代引入记忆集的数据结构(记录从非收集区域到收集区的指针集合)。收集器只需通过记忆集判断某一块非收集区域是否存在指向收集区的指针即可,无需了解跨代引用指针的全部细节。
HotSpot使用一种叫做“卡表”的方式实现记忆集。卡表是使用一个字节数组实现card_table[],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”(card page)
⼀个卡页的内存中通常包含不止⼀个对象,只要卡页内有⼀个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty)[卡表变脏即发生引用字段赋值时,如何更新卡表对应的标识为1,HotSpot使用写屏障维护卡表状态],没有则标识为0。在垃圾收集发⽣时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡⻚内存块中包含跨代指针,把它们加⼊GC Roots中⼀并扫描。
写屏障(Write Barrier):
在HotSpot虚拟机⾥是通过写屏障技术维护卡表状态的。写屏障可以看作在虚拟机层⾯对 “引⽤类型字段赋值”这个动作的AOP切⾯,在引⽤对象赋值时会产⽣⼀个环形(Around)通知,供程序执⾏额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post Write Barrier)。HotSpot虚拟机的许多收集器中都有使⽤到写屏障,但直⾄G1收集器出现之前,其他收集器都只⽤到了写后屏障。