垃圾收集器与内存分配策略(二)
垃圾收集算法
标记清除
这个很好理解,整个过程分为标记和清除两个阶段。标记上一章已经说过了,在标记结束后统一进行清除。
缺点:
(1) 效率不高
(2) 内存中存在大量的内存碎片,如果后续有大的对象生成,可能无法找到足够的连续空间,从而提前触发垃圾回收。
复制算法
把内存分为平均的两部分,平时只使用其中一部分A,回收时,将还存活的对象全部复制到另一个部分B,然后A整个回收掉就可以了。
注意的问题
- 50%的内存是不用的,太浪费了
商业上不会使用1比1的比例。HotSpot将内存分为一块较大的Eden和两块较小的Survivor空间。每次使用Eden和其中一块Survivor空间。回收时讲Eden和Survivor还存活的对象复制到另一块Survivor空间。HotSpot默认Eden和Survivor的比例是8:1. - 如果回收时发现Survivor不够放存活的对象怎么办
需要老年代进行分配担保,即当Survivor不够放的话,剩下的对象直接放入老年代。
标记整理
和标记清除类似,在回收时,将还存活的对象向一端移动。
分代收集算法
根据对象的存活周期,分为年轻代和老年代。不同年代的对象采用不同的垃圾收集算法。
- 显然复制算法适合年轻代。因为年轻代对象的存活率很低,复制的对象的个数很少。
- 标记清除和标记整理适合老年代。老年代对象的存活率很高,复制算法会复制的次数太高,效率不高,而使用标记清除和标记整理算法。
Hotspot的算法实现
枚举根节点
这一节主要是针对如何找到GC Root对象?
- 现有的问题
(1) 我们知道可作为GC Root对象的有全局性引用(static/常量)和执行上下文(栈中局部变量引用)中,但是这块地方有时可达几百M,依次遍历其中每个数据是否是引用,效率太低。
(2) 且我们知道在进行GC Root枚举时,必须stop the world来保证引用关系不再改变。所以这也要求GC Root的枚举不能太耗时。 - HotSpot的解决办法
使用一组叫做OopMap的数据结构。
(1) 记录每个类的每个数据位置(通过偏移量来确定)的类型(即是否是引用)。这一步在类加载后即可确定。
(2) 在特定的位置上记录栈和寄存器中哪些位置是引用。
这样我们在GC Root枚举时,可以快速找到所有引用。
安全点
- 程序执行时并非在任何地方都能停顿下来GC。
只有在安全点的位置上才能停顿下来,原因是因为只有安全点上才有全部的OopMap信息(就是上面说的“特定位置”),即只有安全点才能安全地进行GC Root枚举。
安全点的选取由程序本身结构决定。不能太多也不能太少。一般在方法调用、循环跳转、异常跳转等。 - 还有一个问题:如何在发生GC时,让所有的线程都跑到最近的安全点停下来?
两种方式:抢占式和主动式
抢占式:在GC发生时,中断所有线程,如果发现有的线程不在安全点,则重新唤醒知道跑到最近的安全点。--这种方式几乎没人使用
主动式:线程主动发起。在GC发生时,在安全点位置上设置标记位,线程执行到安全点时轮询这个标志位,如果发现为true,则中断。
安全点很好地解决了如何进入GC的问题。
安全区
- 安全点无法解决的问题
当一个线程没有处于执行状态:就绪(等待分配CPU时间)。此时如果要发生GC,如何让这个线程进入安全点?总不能一直等,知道CPU分配给它时间吧。 - 解决办法
引入安全区的概念,即一段代码内无引用关系改变。线程进入安全区代码后,标记自己“安全区”标记,GC发生时就不用管标记“安全区”的线程了。当线程想要离开安全区时,判断GC是否结束,如果没结束要等GC结束后才能继续执行。
上面这段文章讲的是虚拟机如何发起内存回收,下面讲具体如何回收。
垃圾收集器
Serial
单线程收集器。即只有一个线程去垃圾回收工作。必须先进行stop the world,然后进行回收工作。
算法:由于是新生代,采用复制算法。
特点:单线程简单高效,多线程下效率不高。且必须stop the world。
ParNew
和Serial唯一的区别就是使用多线程去垃圾回收工作。其他和Serial完全一样。
特点:有一个不和性能有关的优点:能和CMS配合的最好新生代收集器。
Parallel Scavenge
也是多线程去垃圾回收工作。算法也是复制算法。
它和别的收集器主要有两点不同:
- 关注点不同
其他收集器关注:如何让用户的线程停顿时间越短越好。
它关注:如何让CPU的吞吐量最高。
吞吐量=(CPU执行代码的时间)/(CPU执行代码的时间+CPU GC的时间)
也被称为:吞吐量优先收集器 - 自适应
它无需用户设置Eden与Survivor的比例,进入老年代年龄参数,它可以动态调整,以达到最高吞吐量。
也被称为:自适应调节策略。
Serial Old
是Serial的老年代版本。单线程+标记整理算法。
Parallel Old
是Parallel Old的老年代版本。多线程+标记整理算法。
CMS
- 说明
一个真正意义上的并行收集器:用户线程和GC线程可以同时运行。
目标是尽可能缩短GC停顿时间的回收器。多用于B/S模型中,用户感知的停顿时间很小。
算法:使用标记-清除算法。 - 分为四个步骤:三标记一清除
(1) 初始标记:标记GC Root直接关联的对象。
(2) 并发标记:进行GC Root的trace过程,进行可达性分析。
(3) 重新标记:修正(2)时,由于用户线程也在运行导致的引用关系变化。
(4) 并发清除:就是清除的过程。
(1)和(3)需要stop the world,但(1)和(3)的消耗时间非常短。
(2)和(4)为并发,即可以和用户线程同时运行。消耗时间相较于1和3较长。但由于是并行,所以整个收集器看来就是和用户线程并行运行。
- CMS的三大缺点
- 由于2和4需要CPU进行GC,所以用户线程的可用CPU变少了。
- 并发清除阶段引起:提前Full GC和引发Concurrent Mode Failure问题。
- 提前Full GC
由于用户线程也在运行,所以清除时,必须留着内存给正在运行的用户线程使用。所以无法像其他收集器一样,老年代用满了才进行GC,CMS在老年代在65%就要开始GC了。 - Concurrent Mode Failure
由于用户线程也在运行且用户线程需要的内存未知,如果此时内存无法满足用户线程的需要,则就会引起CMF。进而引发虚拟机启动备选预案:使用Serial Old来进行GC,这就要花很长时间了。
- 提前Full GC
- 标记-清除算法自身的缺点:存在空间碎片,容易导致大对象无法找到连续的内存分配,而导致提前Full GC。
G1
jdk1.7中最具先进的收集器。
- 有几个特点
- 并行
即整体看来,可以和用户线程并行执行 - 分代收集
前面说的收集器只针对某个年轻代或老年代,它G1可以管理整个堆。 - 空间整合
整体来看是标记-整理算法。从局部(Region)看是复制算法。总而言之,不会有空间碎片。 - 可预测的停顿
这是G1一个很大的特点:即用户可以指定在一个M长的时间片内,GC的停顿时间不超过N秒。
- 并行
- G1是基于Region来进行内存管理的。
G1收集器的堆的分布已经和其他收集器不一样了。G1将堆分为大小相等的Region。仍然有年轻代和老年代的概念,但两者没有物理界限,都是一连串的Region集合。 - 可预测的停顿实现方法
G1会跟踪每个Region的回收价值(回收得到的空间以及GC时间),维护一个优先列表。每个GC时,在用户指定的时间内,找到回收价值最高的进行回收。 - 如何解决跨Region引用来引起的全局堆引用扫描问题
我们知道引用可能发生在跨Region之间,这个问题不仅仅是G1的问题,在其他收集器中也存在年轻代引用老年代的问题。而虚拟机的解决办法是Remembered Set,即每次赋值引用时,记录这个引用否引用其他Region(或老年代),如果是则在Remembered Set记录其他Region(或老年代)。保证不会圈堆扫描。 - G1的具体步骤
- 初始标记
- 并发标记
- 最终标记
-
筛选回收
前三个阶段几乎和CMS一样。最后的筛选回收:在运行的GC停顿时间内,选择回收价值最高的Region回收。这个阶段虽然可以和用户线程并行,但实际上由于所花费时间短,且停顿用户线程这个时间会更短,所以采用停顿用户线程的方式。