JVM垃圾回收——G1垃圾收集器
目录
一、什么是G1垃圾收集器
Garbage First(简称G1)收集器是垃圾收集器技术发展史上里程碑式的成果,它摒弃了传统垃圾收集器的严格的内存划分,而是采用局部回收的设计思路和基于Region的内存布局形式。
G1是一款主要面向服务端应用的垃圾收集器,在jdk6 update14时,就有了实验版本。而到了jdk7 update4之后移除了“Experimental”标识。它的目的是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。如今已经完全替代CMS垃圾收集器,CMS收集器在JDK9 中被废弃,在JDK 14中被移除。
二、G1垃圾收集器的内存划分
从上面图上可以看到,G1垃圾收集器也是基于分代收集理论设计的,但是它的堆内存的布局与其他垃圾收集器的布局有很明显的区别,G1收集器不再按照固定大小以及固定数量的分代区域划分,而是把JAVA堆划分为2048个大小相等的独立的Region,每个Region大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1-32MB,且必须为2的N次幂。每一个Region都可以根据需要充当新生代的Eden区、S0和S1区或者老年代。在一般的垃圾收集中对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。 G1的大多数行为都把H区作为老年代的一部分来看待。当一个对象的大小超过了一个Region容量的一半,即被认为是大对象。
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,而是一系列区域(不需要连续,逻辑连续即可)的动态集合。由于G1这种基于Region回收的方式,可以预测停顿时间。G1会根据每个Region里面垃圾“价值”的大小,在后台维护一个优先级列表,每次根据用户设定的允许收集停顿的时间(-XX:MaxGCPauseMillis,默认为200毫秒)优先处理价值收益最大的Region。
三、G1垃圾收集器的收集过程
在了解G1收集过程之前,我们首先考虑几个问题
1、将java堆分成多个独立的Region后,Region里面存在的跨Region引用的对象是如何解决的?
答:G1收集器和其他垃圾收集器模型一样,存在跨代引用的解决方法是使用记忆集(Remembered Set)具体细节可以参考本人的另一篇博文中的跨代引用JVM垃圾回收——垃圾回收的一些细节实现_熟透的蜗牛的博客-CSDN博客
使用记忆集避免了全堆作为GC Roots扫描,但实际上G1收集器为每一个Region都维护了自己的记忆集,这些记忆集会记录下其他的Region指向自己的指针,并标记这些指针分别在那些卡页范围之内。G1的记忆集在存储结构的本质上是一种哈希表,key是其他Region的起始地址,value是卡表索引号的集合。这种双向卡表的结构比原来的卡表实现更复杂,也正是因为这种双向卡表和多Region的之间的引用导致G1收集器比其他收集器占用的内存要多。根据经验G1至少占用JAVA堆容量的10%-20%额外内存来维护收集器的工作。
2、G1是一个并发标记的收集器,那么在并发回收阶段,G1如果解决“漏标”的情况?如何解决在回收过程中重新分配到该Region上的新对象不被回收的?
答:G1是如何解决存活对象被标记成垃圾对象的,CMS收集器采用增量更新算法实现,G1收集器是通过原始快照(SATB)算法来实现的,具体细节可以参考本人的这篇博文JVM垃圾回收——三色标记法_jvm 三色标记_熟透的蜗牛的博客-CSDN博客。
G1是一款并发回收的垃圾收集器,那么垃圾在回收时程序还继续运行,只要程序运行就会持续有新对象创建。G1为每个Region设计了两个名为TAMS(Top At Mark Start )的指针,把Region中的一部分空间划分出来,用于存储并发回收阶段新分配的对象,并发回收时新分配的对象的地址都必须在这两个指针位置以上,G1默认这个指针位置以上的对象都是隐式标记过的,默认他们是存活对象,不纳入本次回收的范围。与CMS中的“Concurrent Mode Failure”失败会导致Full GC 类似,如果回收速度赶不上分配速度,那么G1也要被迫冻结用户线程,进行Full GC而产生长时间的Stop The World。
3、G1如何建立可靠的停顿模型?
答:用户使用-XX:MaxGCPauseMills参数指定停顿时间,这个时间只是期望值,而不是说指定了200毫秒,就会在200毫秒内将垃圾回收完毕。G1收集器的停顿模型是以衰减均值为理论基础来实现的。在垃圾收集过程中,G1会记录每个Region的回收耗时,记忆集中脏卡数量等各个步骤的花费成本,然后统计出平均值,标准偏差、置信度等统计信息。然后可以计算回收那些Region,可以在期望的时间内达到最大的收益。
初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能够在Region上正确的分配对象。这个阶段需要STW,耗时很短,而且是借用MinorGC(上一轮垃圾回收时触发GC)时候同步完成的。
并发标记:从GC Roots 开始对堆中的对象进行可达性分析,递归扫描整个堆里的对象,这个过程耗时较长,但是是与用户线程并发执行的。对象扫描完之后还需要重新处理STAB记录下的在并发时有引用变动的对象。
最终标记:这个阶段也需要STW,用于处理并发阶段结束后仍然遗留下来的最后少量的STAB记录。
筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本排序,根据用户期望的停顿时间来执行回收计划,然后把决定回收的Region里的存活对象复制到空的Region,然后清空旧Region的空间。由于涉及到对象的移动,所以这个阶段也是需要STW的。
从上述可以看出,除了并发标记,其他阶段都是需要STW的,G1收集器不单单是追求低延迟的收集器,也衡量了吞吐量,所以在延迟和吞吐量之间做了一个权衡。
四、G1收集器的优缺点
G1收集器一直都拿来和CMS垃圾收集器作比较,这里也用CMS垃圾收集器作对比。
- 优势:因为CMS是基于标记-清除的算法实现的,所以CMS会有空间碎片化的问题。而在G1收集器上是不存在的,G1从整体上来看是基于标记-整理算法实现,从Region之间又是基于标记-复制算法实现的。
- 由于G1不会产生空间碎片,可以为对象的分配提供更规整的内存。此外还避免了由于分配大对象时找不到连续的内存空间,而不得不提前触发下一次垃圾回收。
- 不足:由于跨Region引用等大量双向卡表的存在,G1收集器比CMS(只需要处理老年代到新生代的引用)占用更多的内存。
- CMS收集器使用写后屏障来更新维护卡表,而G1收集器除了使用写后屏障维护卡表,为了实现SATB的算法,还需要使用写前屏障来跟踪并发时指针变化情况。所以G1收集器会增加程序运行时的额外负载。
五、G1收集器的JVM参数配置
- -XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务(JDK9后不用设置,默认就是G1)。
- -XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
- -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms(如果这个值设置很小,如20ms,那么它收集的region会少,这样长时间后,堆内存会满。产生FullGC,FullGC会出现STW,反而影响用户体验)。
- -XX:G1NewSizePercent 新生代的最小值默认是5%,此参数在实验阶段,如果想使用加-XX:+UnlockExperimentalVMOptions参数。
- -XX:G1MaxNewSizePercent 新生代的最大值,默认值是60%,此参数在实验阶段,如果想使用加-XX:+UnlockExperimentalVMOptions参数。
- -XX:ParallelGCThreads 设置STW时GC线程数的值。最多设置为8(垃圾回收线程)。
- -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
- -XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45%。
测试代码
package com.wssnail.test;import java.util.ArrayList;import java.util.List;public class TestG1 { private static String[] strArr = new String[]{"中国人民万岁", "梅西好样的,梅西好样的梅西好样的梅西好样的梅西好样的梅西好样的梅西好样的梅西好样的", "我爱看世界杯,我爱看世界杯我爱看世界杯我爱看世界杯我爱看世界杯我爱看世界杯我爱看世界杯我爱看世界杯我爱看世界杯"}; public static void main(String[] args) { List list = new ArrayList<>(); for (int i = 0; i < 100000; i++) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } list.add(strArr); } while (true) { } }}
JVM参数如下
-XX:+UseG1GC-Xlog:gc*-XX:G1HeapRegionSize=1-XX:MaxGCPauseMillis=250-XX:+UnlockExperimentalVMOptions-XX:G1NewSizePercent=5-XX:G1MaxNewSizePercent=60-XX:ParallelGCThreads=2-XX:+PrintCommandLineFlags
来源地址:https://blog.csdn.net/weixin_39555954/article/details/128022143
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341