深入理解JVM——3.4垃圾收集器
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
明确一个观点:虽然我们是在对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。
因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。
垃圾收集器
1. Serial收集器
单线程,stop the word
2. ParNew收集器
Serial收集器的多线程版本
3. Parallel Scavenge收集器
4. Serial Old收集器
Serial收集器的老年代版本
5. Parallel Old收集器
Parallel收集器的老年代版本
6. CMS收集器
concurrent mark sweep,是一种以获取最短回收停顿时间为目标的收集器,非常符合在注重用户体验的应用上使用。
CMS收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS收集器用标记—清除算法实现。
整个过程分为四个步骤:
初始标记
STW,耗时短,仅标记GC Roots能直接关联到的对象并发标记
不会STW,耗时长,从初识标记的对象中遍历整个对象图重新标记
STW,耗时短,修正并发标记期间用户线程导致的标记变动记录并发清除。
不会STW,产生浮动垃圾。
并发失败的话,启动Serial Old,会导致停顿时间变长。优点:
- 并发收集、低停顿。
缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 使用“标记—清除”算法会导致收集结束时会有大量空间碎片产生。
- 并发失败。
由于浮动垃圾的存在,因此CMS必须预留一部分空间来装载这些新产生的垃圾。CMS不能像Serial Old收集器那样,等到老年代区填满了再来清理。在JDK5时,CMS会在老年代使用了68%的空间时激活,预留了32%的空间来装载浮动垃圾,这是一个比较偏保守的配置。如果实际引用中,老年代增长的不是太快,可以通过-XX:CMSInitiatingOccupancyFraction参数适当调高这个值。到了JDK6,触发的阈值就被提升至92%,只预留了8%的空间来装载浮动垃圾。
如果CMS预留的内存无法容纳浮动垃圾,那么就会导致并发失败,这时JVM会触发预备方案,启用Serial Old收集器来回收Old区,这时停顿时间就变得更长了。
GC时为什么要暂停用户线程?
首先,如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。
其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和错标。
为什么CMS采用“标记-清除”算法而不采用“标记-整理”算法
因为CMS作为第一款实现用户线程和收集线程并发执行的收集器,当时的设计理念是减少停顿时间,最好是能并发执行。
但是问题来了,如要用户线程也在执行,那么就不能轻易的改变堆中对象的内存地址,不然会导致用户线程无法定位引用对象,从而无法正常运行。而标记整理算法和标记复制算法都会移动存活的对象,这就与上面的策略不符,因此CMS采用的是标记-清除算法。
7. G1收集器
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
被视为JDK1.7中HotSpot虚拟机的一个重要进化特征,具备以下特点:
- 分代收集
- 并行与并发。G1能充分利⽤CPU、多核环境下的硬件优势,使⽤多个CPU(CPU或者CPU核⼼)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执⾏的GC动作,G1收集器仍然可以通过并发的⽅式让java程序继续执⾏。
- 空间整合。与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
- 可预测的停顿
G1回收流程
运作分为四个步骤:
- 初始标记
这个阶段是STW(Stop the World )的,所有应用线程会被暂停,标记出从GC Root开始直接可达的对象。 - 并发标记
从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长。 - 最终标记
标记那些在并发标记阶段发生变化的对象,这阶段需要停顿线程,但是可以并行执行。 - 筛选回收
暂停用户线程,筛选阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
G1收集器的内存模型
G1堆内存结构
堆内存会被切分成为很多个固定大小区域(Region),每个是连续范围的虚拟内存。
堆内存中一个区域(Region)的大小可以通过-XX:G1HeapRegionSize参数指定,大小区间最小1M、最大32M,总之是2的幂次方。
默认把堆内存按照2048份均分。
G1堆内存分配
每个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden、Survivor和老年代。
存活的对象从一个区域转移(即复制或移动)到另一个区域。区域被设计为并行收集垃圾,可能会暂停所有应用线程。
如上图所示,区域可以分配到Eden、survivor和老年代。此外,还有第四种类型,被称为巨型区域(Humongous Region),Humongous区域是为了那些存储超过50%标准region大小的对象而设计的,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1收集器采用“标记-复制”和“标记-整理”。从整体上看是基于“标记-整理”,从局部看,两个region之间是“标记-复制”。
G1的GC模式
- YoungGC年轻代收集
在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。
YoungGC的回收过程如下:- 根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。
- 处理Dirty card,更新RSet.
- 扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
- 拷贝扫描出的存活的对象到survivor2/old区
- 处理引用队列,软引用,弱引用,虚引用
- mixed gc
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。
G1没有fullGC概念,需要fullGC时,调用serialOld GC进行全堆扫描(包括eden、survivor、o、perm)。
8. ZGC
ZGC(Z Garbage Collector)是一种低延迟的垃圾回收器,是 JDK 11 引入的一项垃圾回收技术。它主要针对大内存、多核心的应用场景,旨在减少垃圾回收带来的停顿时间。