美团面试:G1 垃圾回收底层原理是什么?说说你的调优过程?
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
美团面试:G1 垃圾回收底层原理是什么?说说你的调优过程?
尼恩特别说明: 尼恩的文章,都会在 《技术自由圈》 公号 发布, 并且维护最新版本。 如果发现图片 不可见, 请去 《技术自由圈》 公号 查找
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
听说你是高手,说说,你的JVM调优方法论?
说说,何时进行JVM调优?JVM调优的基本原则?
说说,G1 垃圾回收器的底层原理、基本流程、调优过程?
说说,JVM调优量化目标?JVM调优的步骤?
最近有小伙伴在面试 美团,又遇到了相关的面试题。小伙伴懵了,因为没有遇到过,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
另外,此文的内容,作为第9章、第11章,收入尼恩的《JVM 调优圣经》PDF。
完整的PDF还在写作中,晚些时候进行发布。
首先回答一下: JVM调优方法论
尼恩提示,首先回答一下, JVM调优方法论 。
关于JVM调优的方法论,之前写过在线的版本,但是最新版本有调整。
接下来,就可以介绍三大GC 组件( cms 、g1 、zgc )组件的技术选型。
第9章:三大GC 组件的技术选型 cms g1 zgc 选型
在 Java 应用中选择合适的垃圾回收器(GC)是提升应用性能和稳定性的关键。
CMS、G1 和 ZGC 是三种主要的垃圾回收器,每种都有其特点和适用场景。
下面将从几个角度对这三种 GC 进行对比,帮助进行选型:
1. CMS(Concurrent Mark-Sweep GC)
CMS特点:
- 并发回收:CMS 是并发的标记-清除垃圾回收器,年轻代使用复制算法,老年代使用标记-清除算法。这意味着在垃圾回收时,应用线程与 GC 线程同时工作,从而减少应用停顿时间。
- 老年代GC触发条件:CMS 不会等到老年代完全满了才进行回收,而是当老年代使用达到一定阈值时(默认 92%)开始回收,以避免长时间的 Full GC。
- 适用场景:适合对低延迟有较高要求的应用,例如 Web 服务、在线交易系统。
CMS优点:
- 低延迟:由于 CMS 在大部分阶段是并发的,它能大大减少老年代回收时的 "Stop-The-World" 停顿时间。
- 成熟稳定:CMS 已存在多年,适用于许多生产环境。
CMS缺点:
- 内存碎片问题:由于 CMS 是标记-清除回收器,不会整理内存,老年代内存中会产生碎片,这可能导致 Full GC。
- 并发模式失败:如果老年代在回收过程中无法及时腾出足够空间,可能会发生“Concurrent Mode Failure”,这会退回到单线程的 Serial Old GC,导致长时间暂停。
- 较高 CPU 消耗:CMS 在回收时需要额外的 CPU 资源,可能对 CPU 密集型应用有较大影响。
2. G1(Garbage First GC)
G1特点:
- 分区堆模型:G1 将堆分成多个大小相等的区域(Region),不同区域可能属于年轻代或老年代。通过收集垃圾最多的区域进行回收,因此称为“Garbage First”。
- 混合回收:G1 能够同时回收年轻代和老年代的内存,避免了 Full GC 的大范围内存整理。
- 暂停时间可控:G1 可以根据设置的最大暂停时间目标(默认 200ms),智能选择要回收的区域,来控制 GC 的影响。
G1优点:
- 适用于大堆内存:G1 尤其适合大堆内存(通常超过 6GB)环境,能够有效处理较大的老年代回收。
- 避免 Full GC:通过区域化内存管理和并行收集,G1 几乎避免了传统的 Full GC 停顿。
- 碎片整理:G1 在回收时会进行内存整理,减少了内存碎片问题。
G1缺点:
- 调优复杂:虽然 G1 能够自适应配置暂停时间,但在高性能应用场景下,G1 的调优相对复杂。
- 初始性能不及 CMS:在某些场景下,G1 的初始性能可能不如 CMS,尤其是在堆内存较小的情况下。
- 较高内存消耗:相比 CMS,G1 的元数据管理和并行策略可能导致较高的内存占用。
3. ZGC(Z Garbage Collector)
ZGC特点:
- 超低延迟:ZGC 是一种面向超低延迟设计的垃圾回收器,旨在将垃圾回收停顿时间控制在 10ms 以内。
- 堆内存极大:ZGC 支持非常大的堆内存(TB 级别),这使得它在处理大规模内存应用时有很大的优势。
- 并发收集:ZGC 采用完全并发的回收策略,垃圾回收和应用线程几乎同时进行,极大减少了暂停时间。
ZGC优点:
- 几乎无停顿:ZGC 的最大卖点就是它几乎不会产生明显的 GC 停顿,即使是在处理大内存时。
- 处理超大堆内存:ZGC 在堆内存非常大的情况下(如超过 1TB),仍然能保持很好的性能表现。
- 低内存碎片:ZGC 采用了内存指针的标记整理机制,能有效防止碎片问题。
ZGC缺点:
- CPU 开销高:ZGC 对 CPU 的要求较高,适合多核环境下使用,否则可能导致较高的 CPU 资源消耗。
- 尚在发展:相比 CMS 和 G1,ZGC 相对较新(Java 11 引入),尽管表现优异,但成熟度不如 CMS 或 G1。
- 较高的内存使用:ZGC 在运行时需要额外的元数据,导致整体内存占用较高。
CMS、G1 和 ZGC 选型建议:
-
如果应用需要超低延迟,且堆内存非常大(如 TB 级别),并且系统有足够的 CPU 核心,ZGC 是最好的选择。
ZGC 几乎不会产生显著的暂停时间,适合高频交易、超大内存服务等需要极致低延迟的场景。
-
如果应用对延迟有要求,且内存比较大(通常 6GB 以上),并且希望在兼顾延迟和吞吐量之间找到平衡,可以选择 G1。
G1能够提供稳定、可预测的暂停时间,适合大多数服务端应用场景。
-
如果系统是中等规模的内存(<6GB),并且低延迟重要但 CPU 资源有限,CMS 是一个成熟且较为轻量的选择。
尽管 CMS 有内存碎片和并发模式失败的风险,它仍然适合对响应时间要求较高的中小型应用。
第10章: cms 底层原理和调优实战
《cms 底层原理和调优实战》内容正在写作中,本月底发布。
尼恩希望通过 JVM调优圣经一个PDF,帮助大家一举成为 JVM调优 小王子。
实现通过JVM调优 的超级技能,去毒打面试官。
第11章:G1 底层原理 、基本流程、调优实战
11.1 什么叫 Garbage First 垃圾优先呢?
G1的全称是Garbage First,意思是“垃圾优先”。
G1对老年代使用移动式的回收算法,并不属于一个高效率的回收器,虽然没有碎片问题,但效率是较低的。
怎么提高效率呢?
方法很简单,对每一个region区域,按照需要移动的对象数量进行排序。具体来说,是在并发标记时,会根据存活对象的数量/大小,对标记的区域进行降序排序。
到了移动过程时,就会选择,优先选择 移动效率高、移动速度快的region区域作为回收集合,
移动效率高、移动速度快的region,就是垃圾对象多,存活对象少的region,
Garbage 垃圾对象多,需要移动的就少,这,就是Garbage First命名的由来。
因为老年代对象大多数是存活的,所以每次回收需要移动的对象很多。
而清除算法中是清除死亡的对象,所以从效率上来看,清除算法在老年代中会更好。
既然对老生代来说 标记-整理 算法效率不高,但是为什么 Garbage First 还是要用标记-整理 算法,而不用 标记-清除算法呢?
但是由于G1这个可控制暂停的增量回收,可以保证每次暂停时间在允许范围内,对于大多数应用来说,暂停时间比吞吐量更重要。
再加上G1的各种细节优化,效率已经很高了。
11.2. 什么是G1垃圾回收器
G1 GC中的堆结构和其他回收器的有所不同,
在G1中,堆被划分为N个大小的相等的区域(Region),每个区域占用一段连续的地址空间,以区域为单位进行垃圾回收,而且这个区域的大小是可配置的。
在分配时,如果选择的区域已经满了,会自动寻找下一个空闲的区域来执行分配。
G1 is a generational, incremental, parallel, mostly concurrent, stop-the-world, and evacuating garbage collector which monitors pause-time goals in each of the stop-the-world pauses.
在内存空间划分上,G1将堆分为等大的一块区域(region),region 在逻辑上将堆分为年轻代和老年代两种逻辑类型,
年轻代 region
年轻代区域包括:
- Eden区域 - 新分配的对象
- Survivor区域 - 年轻代GC后存活但不需要晋升的对象
年轻代又包含: eden region
和 survivor region
,所有新建的对象均创建在eden region
中,在经过young gc后,对象被复制整理到survivor region
中(年龄不够到老年代时);
老轻代 region
老年代包含:
- 晋升到老年代的对象
- 直接分配至老年代的大对象,占用多个区域的对象
老年代 region 是 可横跨多个region的 大区域,
大对象直接分配至老年代 ,大对象指的是超过Region Size 一半大小的对象,这样的对象多了会造成 Heap 空间碎片化。
G1中的堆结构如下图所示:
和其他的垃圾回收方式有所不同,G1的年轻代/老年代的回收算法都是一致的,属于移动/转移式回收算法。
比如复制算法,就属于移动式回收算法,优点是没有碎片,存活的越少效率越高
示意图如下:
往细粒度讲, Region分为5中类型:
-
FHR - Free Heap Region :空闲分区,还未进行分配
-
YHR - Yound Heap Reagion :年轻代分区
-
ERH - Eden Heap Region : eden区,伊甸园,放新创建对象
-
SRH - Survivor Heap Region : Survivor 区,存货去,放每次GC后存活对象
-
OHR - Old Heap Region : 老年代分区,放长命对象
-
HHR - Humongous Heap Region : 巨型对象分区,存放巨大(>Region Size 的 50%)对象
region是内存分配和垃圾回收的基本单位,其大小为2的幂,范围是 1 MB 到 32 MB 之间,
region大小 可通过-XX:G1HeapRegionSize=4M
进行配置。
G1是一个分代的垃圾回收器,同样的它将堆分为年轻代(young)和老年代(old),将划分的区域又分为年轻代区域和老年代区域。
和其他垃圾回收器不同,G1中不同代的区域空间并不是连续的。
这里解释一下,为什么G1中不同代使用不连续的区域。
因为G1 Heap中初始时只划分了区域,并没有给区域分类,在对象分配时,只需要从空闲区域集(free-list)中选取一个存储对象即可,这样区域分配更灵活。
当发生GC时,EDEN区被清空,然后会作为一个空闲的区域,这个区域待会可能会作为老年代,也可能会被作为Survivor区域。
不过在G1在分配时还是会检查新生代的区域总大小是否超过新生代大小限制的,如果超出就会进行GC。
虽然每个区域的大小是有限的,不过针对一些占用较大的大对象(humongous object),还是会存在跨区域的情况。对于跨区域的对象,会分配多个连续的区域。
11.3. G1的执行流程
G1 中的对象何时进入老年代:
- 存活对象超过年龄阈值(默认 15)仍未被回收则进入老年代:熬过一次 GC 增加一岁,默认年龄超过 15 岁还没有被回收则被移动到老年代,通过设置 jvm 参数
-XX:MaxTenuringThreshold
来设置对象进入老年代的阈值; - 大对象直接进入老年代:超过 G1HeapRegionSize 的一半会被认为是大对象,大对象直接进入老年代;
- 动态年龄判断:在survivor区中,Survivor区中相同年龄的对象总大小是否超过了Survivor区的一半,这个一半的比例阈值可以通过-XX:TargetSurvivorRatio`值(默认50),如果是,那么这个年龄就是晋升的动态年龄,该年龄及以上年龄的对象都会被直接晋升到老年代。
- 空间分配担保:young GC 后,survivor 区空间不能容纳全部存活对象, G1 中的对象何 进入老年代
G1垃圾回收器的垃圾回收过程主要分为两个阶段,这两个阶段会循环进行,以平衡垃圾回收的效率和应用程序的停顿时间。G1垃圾回收器的两个主要阶段:
- 年轻代回收(Young GC)阶段:
- 在这个阶段,G1垃圾回收器主要回收年轻代(包括Eden区和Survivor区)中的对象。
- 当Eden区被填满或者达到了某个条件(例如,G1认为回收这些区域的收益较高)时,就会触发一次年轻代回收。
- 年轻代回收的过程中,存活的对象会从Eden区和Survivor区复制到另一个Survivor区或者直接晋升到老年代。
- 这个过程通常是Stop-The-World(STW)的,即在回收过程中,应用程序的其他线程会被暂停。
-
混合回收(Mixed GC)阶段:
- 当老年代的占用率达到了一定阈值(由
-XX:InitiatingHeapOccupancyPercent
参数控制,默认值为45%),G1会启动混合回收阶段。 - 在这个阶段,G1不仅回收年轻代,还会回收一部分老年代的区域,这些区域被认为含有较多垃圾。
- 混合回收的目的是减少老年代的内存占用,并释放空间供应用程序使用。
- 混合回收也是STW的,但G1会尝试在用户指定的停顿时间目标内完成。
- 当老年代的占用率达到了一定阈值(由
G1 的垃圾回收过程
在逻辑上,G1 分为年轻代和老年代,但它的年轻代和老年代比例,并不是那么“固定”,为了达到 MaxGCPauseMillis 所规定的效果,G1 会自动调整两者之间的比例。
如果你强行使用 -Xmn 或者 -XX:NewRatio 去设定它们的比例的话,我们给 G1 设定的这个目标将会失效。
G1 的回收过程主要分为 3 类:
(1)G1“年轻代”的垃圾回收,同样叫 Minor GC,这个过程和我们前面描述的类似,发生时机就是 Eden 区满的时候。
(2)老年代的垃圾收集,严格上来说其实不算是收集,它是一个“并发标记”的过程,顺便清理了一点点对象。
(3)真正的清理,发生在“混合模式”,它不止清理年轻代,还会将老年代的一部分区域进行清理。
在 GC 日志里,这个过程描述特别有意思,
(1)的过程,叫作 [GC pause (G1 Evacuation Pause) (young),年轻代回收(Young GC)阶段
(2)老年代的垃圾收集,严格上来说其实不算是收集,它是一个“并发标记”的过程,顺便清理了一点点对象。
(3)的过程,叫作 [GC pause (G1 Evacuation Pause) (mixed)。Evacuation 是转移的意思,和 Copy 的意思有点类似。
这三种模式之间的间隔也是不固定的。比如,1 次 Minor GC 后,发生了一次并发标记,接着发生了 9 次 Mixed GC。
G1垃圾回收器的动态年龄判断
G1垃圾回收器的动态年龄判断是其在进行Minor GC(新生代垃圾回收)时,决定对象是否应该晋升到老年代的一种机制。
在G1中,每个Region都可以扮演Eden、Survivor或Old的角色,而对象在新生代中存活的时间越长,其年龄就越大。当对象的年龄达到一定的阈值时,它们会被晋升到老年代。
在动态年龄判断中,G1会检查在一次Minor GC之后,Survivor区中相同年龄的对象总大小是否超过了Survivor区的一半。如果是,那么该年龄及以上年龄的对象都会被直接晋升到老年代。这个机制的目的是为了避免在Survivor区中堆积太多存活对象,导致频繁的复制操作,从而影响垃圾回收的效率。
例如,如果在一次Minor GC后,发现年龄为3的对象总大小超过了Survivor区的一半,那么所有年龄大于或等于3的对象都会被晋升到老年代。这个机制有助于G1在满足用户设置的GC暂停时间目标的同时,有效地管理内存空间。
需要注意的是,动态年龄判断并不是唯一决定对象晋升的因素。
对象晋升到老年代还可能因为它们本身大小达到了MaxTenuringThreshold
设定的年龄阈值,或者因为它们是大对象(超过某个Region一半大小的对象)而直接分配到老年代。
G1垃圾回收器通过这些机制,结合Region的概念和动态调整的策略,实现了在保持低延迟的同时,对堆内存进行有效的管理和回收。这些策略使得G1成为适用于大堆内存和需要可预测停顿时间的应用程序的理想选择。
G1垃圾回收器的空间分配担保(Space Allocation Guarantee)
G1垃圾回收器的空间分配担保(Space Allocation Guarantee)是一种机制,用于确保在进行Minor GC(年轻代垃圾回收)时,有足够的空间来存放从Eden区和Survivor区晋升(promote)到老年代的对象。这个过程是为了预防内存空间不足而导致的Full GC(全堆垃圾回收)。
在G1中,当进行Minor GC时,会检查老年代的可用空间是否足够容纳存活的对象。如果老年代的连续空间小于Minor GC中存活对象的总大小,G1会尝试进行空间分配担保。这个过程包括以下几个步骤:
- 检查老年代空间:在Minor GC之前,G1会检查老年代的可用空间。
- 处理晋升失败:如果老年代空间不足,G1会尝试找到一个足够大的连续空间来存放晋升的对象。如果找不到,就会触发担保失败。
- 担保失败处理:担保失败时,G1会进行一次Full GC,以清理整个堆并释放空间。
G1还提供了一些参数来控制空间分配担保的行为:
-XX:G1HeapWastePercent
:设置老年代中可以浪费的内存百分比,默认值通常是5%。当老年代的剩余空间低于这个百分比时,G1会避免进行Mixed GC,以确保有足够的空间进行对象晋升。-XX:G1OldCSetRegionThresholdPercent
:设置在Mixed GC期间要回收的老年代Region的最大百分比。
此外,G1使用Remembered Sets(记忆集)来跟踪跨Region的引用,确保在垃圾回收过程中能够正确处理对象间的关系。这些记忆集在Minor GC和Mixed GC中都非常重要,因为它们帮助G1确定哪些对象是存活的,哪些可以被回收。
11.4. GC root 可达性分析 与 Remembered Set(记忆集)
在Java虚拟机(JVM)中,GC Roots可达性分析是垃圾回收(GC)过程中的一个关键步骤,
GC root 用于确定哪些对象是活跃的,即从GC Roots开始通过引用链可以到达的对象。
一般来说,这些GC Roots包括但不限于:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
G1垃圾回收器在进行垃圾回收时,会利用这些GC Roots进行可达性分析,以确定对象是否存活。G1 GC的设计目标是在保证吞吐量的同时,尽可能减少GC引起的停顿时间。
G1 GC 中,存在很多的region区域, 如何进行跨区域的 对象引用 关系? 这就是 Remembered Set(记忆集)。
Remembered Set(记忆集)是G1 GC中用于记录跨区域引用的一个重要数据结构。
由于G1将堆内存划分为多个区域(Region),记忆集的作用是记录老年代中的对象直接引用了年轻代中的对象的情况。
通过 Remembered Set(记忆集),在进行年轻代的垃圾回收时,G1可以通过查看记忆集来确定哪些老年代中的对象需要被扫描,以确保不会遗漏任何可达的对象。
Remembered Set(记忆集)的起因
比如在对某个区域进行回收时,首先从GC ROOT开始遍历可直达这些区域中的对象,可G1是多个region,情况变得特殊了:由于晋升或者移动的原因,这些区域中的某些对象移动到了其他区域,可是移动之后仍然保持着对原区域对象的引用;
那么此时原区域中被引用的对象对GC ROOT来说并不能“直达”,他们被其他对象的区域引用,这个发起引用的其他对象对于GC ROOT可达。
这种情况下,如果想正确的标记这种GC ROOT不可直达但被其他区域引用的对象时就需要遍历所有区域了,代价太高。
如下图所示,
如果此时堆区域A进行回收,那么需要标记区域A中所有存活的对象,可是A中有两个对象被其他区域引用,这两个灰色的问号对象在区域A中对GC ROOTS来是不可达的,
但是实际上这两个对象的引用对象被GC ROOTS引用,所以这两个对象还是存活状态。此时如果不将这两个对象标记,那么就会导致标记的遗漏,可能造成误回收的问题
RememberedSet(简称RS或RSet) 就是用来解决这个问题的,RSet会记录这种跨代引用、跨region引用的关系。
在对象晋升的时候,将晋升对象记录下来,这个存储跨区引用关系的容器称之为RSet
RSet记忆集,用一组key - value结构,记录了跨代引用的引用关系,在gc的时候,可以快速的借助记忆集+gc roots搞定同代引用及跨代引用的可达对象分析问题。
在进行标记时,除了从GC ROOTS开始遍历,还会从RSet遍历,确保标记该区域所有存活的对象。
RememberedSet 不光是G1,其他的分代回收器里也有,比如CMS。
如下图所示,G1中利用一个RSet来记录这个跨区域引用的关系,每个区域都有一个RSet,用来记录这个跨区引用,这样在进行标记的时候,将RSet也作为ROOTS进行遍历即可
记忆集的维度应该是什么?针对新生代和老年代各搞一个?还是针对region,每个region都搞一个?
对于G1来说,它是以region为最小内存管理维度的,它的RSet记忆集的维度是对每一个region,都搞一块儿内存,存储region里面所有的对象被引用的引用关系。
针对region这个维度,是因为,每次回收之后,老年代,新生代,大对象区域的region可能都会变化,
所以,如果说,对每个分代都搞一份儿的话,不太合理,因为region不断的在变化,同时也会有并发问题,效率问题。
同时,除了新生代的回收是需要选择所有新生代的region,老年代的回收,是需要找性价比高的region来回收的,也就是选择一部分去回收,
那么选择一部分回收的时候,还要去整个分代对应的这么一大块儿引用关系数据,去做遍历,筛选,才能拿到需要的数据。
最终,G1就选择了使用Rset记忆集这种方式,记录了这些引用关系,方便在进行垃圾回收的时候去找到有哪些GC roots,大大减少了不必要的遍历操作。
位图--bitMap
大家知道,JVM管理的是内存。内存的使用状态,其实是不太好标记的 000000001010。
最笨的方法就是,直接遍历整个内存块儿,看看它到底有没有东西.
- 没有东西,它是空闲的。
- 有东西,它是使用中的。
所以说,为了描述内存的使用状态,G1采取了位图的方式来做描述。
在一个位图里面记录了所有内存的使用状态,如果要看内存是否被使用了,就直接访问位图中这块儿内存对应的坐标里面的内容,就能知道内存是否已经使用了。
举个例子:
RSet,在G1中通过Card Table 卡表来实现。
注意,这里说Card Table实现RSet,并不是说CardTable是RSet背后的数据结构,只是RSet中存储的是CardTable数据
Card Table 卡表
卡表和位图其实是类似的东西。
都是用一段数据去描述另外一块儿内存的情况。
Card Table 跟位图不一样的地方是:由于位图只能用一位来描述,也就是只能记录使用 或者未使用。
因为一个位只能有0 1 这两种状态。
而卡表为了描述更多的信息,比如内存是否使用,内存的引用关系等,使用的是8位,也就是一个字节来描述一块儿内存的使用情况,是否使用,使用了多少。
所以说,本质上卡表在数据结构层面和位图没有什么太大区别。只是描述符比位图长,描述的内容比位图多。
在G1 堆中,存在一个CardTable的数据,CardTable 是由元素为1B的数组来实现的,数组里的元素称之为卡片/卡页(Page)。
在G1中,卡表是用一个字节(8位) 的数组元素来描述512字节的空间的使用情况,及内存被谁使用了。
每个Region 大小为1 MB,每个Region都会对应2048个Card Page。
如下图所示,在一个大小为1 GB的堆下,那么CardTable的长度为2097151 (1GB / 512B);
那么查找一个对象所在的CardPage只需要简单的计算就可以得出:
并且在G1中,是一个全局卡表,也就是,整个堆内存公用一个全局卡表,来描述全局的内存使用情况及对象引用关系。
当然,因为512字节的内存,可能会被引用多次,里面可能有多个对象,或者说,同一个对象,被多个对象引用,所以说,卡表的描述,可以理解为一个大概的引用关系描述。
这个CardTable会映射到整个堆的空间,每个卡片会对应堆中的512B空间。
介绍完了CardTable,下面说说G1中RSet和CardTable如何配合工作。
一个Rset,它是由一个一个key - value对组成的。每个区域中都有一个RSet,通过hash表实现,这个hash表的key是引用本区域的其他区域的地址,value是一个数组,数组的元素是引用方的对象所对应的Card Page在Card Table中的下标。
- 其中,key是引用了当前region的其他 region 区域的地址。
- Value是一个数组,value中的元素是引用方的对象所在内存块儿在CardTable中的下标。
以上是Rset记忆集中存储的信息。
如下图所示,区域B中的对象b引用了区域A中的对象a,这个引用关系跨了两个区域。
b对象所在的CardPage为122,CardPage 是整个堆内存公用一个全局卡表,下标 122 也是一个全局的 内存编号。
在区域A的RSet中,以区域B的地址作为key,b对象所在CardPage下标为value记录了这个引用关系,这样就完成了这个跨区域引用的记录。
不过这个CardTable的粒度有点粗,毕竟一个CardPage有512B,在一个CardPage内可能会存在多个对象,多个对象可能只有一个对象 引用了 A,也有可能都引用了 A。所有, 这种引用关系有点粗粒度,并不是精准的对象引用关系。
所以说,Rset记忆集存储的,其实不是 哪些对象与当前region的引用关系,而是对象所在的卡页CardPage 对当前region的引用关系,从粒度来说,对象所在的卡页CardPage 相比对象来说会稍微大一些(如果对象大的话,粒度可能反而更小,要看具体对象的大小情况。)
在扫描标记时,需要扫描RSet中关联的整个CardPage。
具体说,如果我们在遍历对象的时候,直接找对象所在region的RSet记忆集,从里面就能拿到所有引用了当前对象所在region的卡表数据,及卡表对应的512B内存块儿的地址。
总结来说就是,一旦有老年代的对象引用了一个新生代(老年代)的region中的对象,那么,就会在这个新生代的(老年代)region 的记忆集中维护一个key - value对,其中key是引用方对象对应的region的地址,也就是那个老年代的对象所在region的地址,value是一个数组,里面存储的是这个对象所在的cardpage(512字节的卡页)在全局卡表中的下标。
通过这个Rset,我们在遍历一个region的时候,就能根据这个region的Rset快速定位到引用方所在的region及引用对象所在的cardpage。从而避免对老年代进行全局扫描。
分代回收
G1中有3种回收模式:
- 完全年轻代GC(fully-young collection),也称年轻代垃圾回收(Young GC)
- 部分年轻代GC(partially-young collection)又称混合垃圾回收(Mixed GC)
- Full GC ,退化到 Serial Old收集器。
完全年轻代GC是只选择年轻代区域(Eden/Survivor)进入回收集合(Collection Set,简称CSet)进行回收的模式。
年轻代GC的过程和其他的分代回收器差不多,新创建的对象分配至Eden区域,然后将标记存活的对象移动至Survivor区,达到晋升年龄的就晋升到老年代区域,然后清空原区域(不过这里可没有年轻代复制算法中两个Survivor的交换过程)。
年轻代GC会选择所有的年轻代区域加入回收集合中,但是为了满足用户停顿时间的配置,在每次GC后会调整这个最大年轻代区域的数量,每次回收的区域数量可能是变化的
下面是一个完全年轻代GC过程的简单示意图:将选择的年轻代区域中所有存活的对象,移动至Survivor区域,然后清空原区域
上面只是一个简易的回收过程示意,接下来详细介绍年轻代的回收过程
11.5. G1的 Young GC 年轻代垃圾回收(完全年轻代GC)
当JVM无法将新对象分配到eden区域时,会触发年轻代的垃圾回收,年轻代垃圾回收是完全暂停的,虽然部分过程是并行,但暂停和并行并不冲突,。也会称为“evacuation pause”。
在"evacuation pause"期间,G1会执行对象的移动操作,这个过程是STW的,以确保在对象移动后,所有引用都指向正确的地址。
年轻代回收是一个 STW 的过程,它的跨代引用使用 RSet 数据结构来追溯,会一次性回收掉年轻代的所有 Region。
JVM 启动时,G1 会先准备好 Eden 区,程序在运行过程中不断创建对象到 Eden 区,当所有的 Eden 区都满了,G1 会启动一次年轻代垃圾回收过程。
年轻代的收集包括下面的回收阶段:
(1) 扫描根(Root Scanning)
根,可以看作是我们前面介绍的 GC Roots,加上 RSet 记录的其他 Region 的外部引用。
(2)RSet 记忆集合更新(Update RS )
处理 dirty card queue 中的卡页,更新 RSet。此阶段完成后,RSet 可以准确的反映老年代对所在的内存分段中对象的引用。可以看作是第一步的补充。
(3) RSet 记忆集合扫描(Scan RSet ),
识别被老年代对象指向的 Eden 中的对象,这些被指向的 Eden 中的对象被认为是存活的对象。
(4)移动/复制对象 (Evacuation/Object Copy)
没错,收集算法依然使用的是 Copy 算法。
在这个阶段,对象树被遍历,Eden 区内存段中存活的对象会被复制到 Survivor 区中空的 Region。
这个过程和其他垃圾回收算法一样,包括对象的年龄和晋升,无需做过多介绍。
(5)处理引用
处理 Soft、Weak、Phantom、Final、JNI Weak 等引用。
(6)结束收集。
步骤1. 选择收集集合(Choose CSet)
G1会在遵循用户设置的GC暂停时间上限的基础上,选择一个最大年轻代区域数,将这个数量的所有年轻代区域作为收集集合。
如下图所示,此时A/B/C三个年轻代区域都已经作为收集集合,区域A中的A对象和区域B中的E对象,被ROOTS直接引用
(图上为了简单,将RS直接引用到对象,实际上RS引用的是对象所在的CardPage)
步骤2. 扫描根(Root Scanning)
接下来,需要从GC ROOTS遍历,查找从ROOTS直达到收集集合的对象, 移动他们到Survivor区域的同时,将他们的引用对象加入标记栈
如下图所示,在扫描根(Root Scanning) 阶段,被GC ROOTS直接引用的A/E两个对象直接被复制到了Survivor区域M,同时A/E两个对象所引用路线上的所有对象,都被加入了标记栈(Mark Stack),这里包括E->C->F,这个F对象也会被加入标记栈中
步骤3. RSet 记忆集合更新(Update RS )
在RSet扫描之前,还有一步更新RSet(Update RS)的步骤,
因为RSet是先写日志,再通过一个Refine线程进行处理日志来维护RSet数据的,这里的更新RSet就是为了保证RSet日志被处理完成,RSet数据完整才可以进行扫描
步骤4. RSet 记忆集合扫描(Scan RSet )
将RSet作为ROOTS遍历,
从 RSet 查找可直达到收集集合的对象,移动他们到Survivor区域的同时,将他们的引用对象加入标记栈
如下图所示,老年代区域C中引用年轻代A的 B对象,这个引用关系,被记录在年轻代A的RSet中,
此时遍历这个年轻代A的RS 记忆集合,将老年代C区域中D对象引用的年轻代A中的B对象,添加到 栈中
步骤5. 移动(Evacuation/Object Copy)对象
遍历上面的标记栈,将栈内的所有所有的对象移动至Survivor区域(其实说是移动,本质上还是复制)
如下图所示,标记栈中记录的C/F/B对象被移动到Survivor区域中
当对象年龄超过晋升的阈值时,对象会直接移动到老年代区域,而不是Survivor区域。
步骤6. 处理引用
对象移动后,需要更新引用的指针,
处理 Soft、Weak、Phantom、Final、JNI Weak 等引用。
写入屏障(类似AOP思想的切面函数)
写入屏障 (Write Barrier) 也是GC里的一个关键技术(不是linux里的membarrier),
当发生引用关系的更新时,通过写入屏障来(这里指转移用的写入屏障)记录这个引用关系的变更,
写入屏障 只是一系列函数而已,就像这样(伪代码):
def evacuation_write_barrier(obj, field, newobj){
//检查引用和被引用新对象是否在同一个区域
if(!check_cross_ref(obj, newobj)){
return
}
//不重复添加dirty_card
if(is_dirty_card(obj)){
return
}
to_dirty(obj);
//将obj添加到newobj所在region的rs
add_to_rs(obj, newobj);
}
为了便于理解,上面的伪代码屏蔽了一些细节,了解核心工作内容即可
不过在G1里,不止一种写入屏障,像前面介绍的SATB也是有的写入屏障,这里不做过多介绍
步骤7. 结束收集。
剩下的就是一些收尾工作,Redirty(配合下面的并发标记),Clear CT(清理Card Table),Free CSet(清理回收集合),清空移动前的区域添加到空闲区等等,这些操作一般耗时都很短
G1年轻代回收核心技术总结
卡表(Card Table):
卡表是G1垃圾回收器中的一个核心组件,每个Region都配备有一个独立的卡表。
卡表本质上是一个字节数组,用于记录Region内部对象与老年代对象之间的跨代引用关系。
当发生跨代引用时,G1会识别出该引用,并将卡表中相应位置的字节内容修改为0,这样的卡表条目被称为“脏卡”。
卡表的主要作用是为生成记忆集(Remembered Set,简称RS或RSet)提供必要的数据支持。
卡表的大小与堆的大小直接相关。
例如,当堆大小为1GB时,卡表的大小为1GB ÷ 512 = 2MB。这是因为卡表中的每个字节负责监控一定内存范围内的对象引用情况,通常这个范围被设置为512字节。
记忆集(Remembered Set,简称RS或RSet):
每个Region都拥有一个独立的记忆集,用于记录从老年代引用到当前Region中对象的详细信息。
这些信息包括被引用对象在卡表中的位置等。
在标记阶段,垃圾回收器会将记忆集中的对象加入到GC Root对象集合中,并一同进行扫描。
这样,垃圾回收器就能够准确地识别出哪些对象是被引用的,从而将它们标记为存活状态。
写屏障(Write Barrier):
G1垃圾回收器采用写屏障技术来维护卡表的准确性。
写屏障是一种在对象引用写入操作后自动触发的机制,它会在引用关系建立后的代码中插入一段指令。
这些指令负责更新卡表的状态,确保卡表能够实时反映对象之间的跨代引用关系。
虽然写屏障的引入会带来一定的性能开销,通常这个开销大约在5%~10%之间,但它对于确保垃圾回收的正确性和效率至关重要。
11.6. G1的Mixed GC混合回收(部分年轻代GC)
混合回收,也称部分年轻代GC,
在混合回收过程中,垃圾回收器将同时处理整个年轻代和部分老年代的内存。
混合回收会选择所有年轻代区域(Eden/Survivor)(最大年轻代分区数)和部分老年代区域进去回收集合进行回收的模式。
年轻代区域对象移动到Survivor区,老年代区域移动到老年代区域。
由于G1中老年代区域的回收方式和新生代一样是“移动式”,被回收区域在移动后会全部清空,所以不会像其他使用清除算法的回收器一样(比如CMS)有碎片问题。
下面是一个部分年轻代GC过程的简单示意图:
在G1垃圾回收器的运行过程中,随着多次的年轻代回收,会逐渐形成多个Old老年代区域。
当整个堆内存的使用率达到一个预设的阈值(默认为45%)时,将触发混合回收(Mixed GC)。
混合回收的触发时机不仅限于总堆占有率的阈值达到,它还可能由年轻代回收之后或当分配大对象时触发。
鉴于老年代中可能存在大量的对象,直接标记所有存活对象可能会消耗较多的时间。
为了提升效率,减少应用程序的停顿时间,混合回收的整个标记过程被设计为尽量与 Mutator 用户线程并行执行。这样可以在不影响应用程序性能的前提下,更有效地管理内存。
混合回收的具体步骤如下:
- 初始标记(Initial Mark):这是一个Stop-The-World(STW)阶段,使用三色标记法来快速标记从GC Root直接可达的对象。这一步确保了回收过程中不会遗漏任何重要的根对象。
- 并发标记(Concurrent Mark):在此阶段,标记工作与用户线程并发执行。垃圾回收器遍历对象图,对存活的对象进行标记。这个过程可以充分利用多核处理器的并行能力,提高标记效率。
- 最终标记(Final Mark):再次进入STW阶段,处理与SATB(Snapshot-At-The-Beginning)相关的对象标记。SATB是一种在GC开始时捕获对象图快照的技术,它确保了在并发标记期间新创建的对象也能被正确标记。
- 清理(Cleanup):另一个STW阶段,清理那些没有任何存活对象的区域。这些区域将被回收,以便后续的内存分配。
- 转移(Evacuate):最后,将存活的对象从它们的当前区域复制到其他空闲区域。这个过程可能涉及对象的移动和指针的更新,因此也是STW的。
通过这一系列的步骤,混合回收有效地管理了年轻代和老年代的内存,同时尽量减少了应用程序的停顿时间,提高了整体性能。
1. 初始标记
初始标记阶段是混合垃圾回收(Mixed GC)的一个重要组成部分。
在这个阶段,所有的用户线程会被暂停,以确保垃圾回收器能够专心标记从GC Root直接可达的对象。
由于只关注直接从GC Root出发的引用链,因此这一阶段的停顿时间通常不会过长,从而减少了对应用程序性能的影响。
在初始标记中,G1垃圾回收器采用了三色标记法来识别对象的状态。
这种方法在原有的双色标记(黑色代表存活,白色代表可回收)基础上增加了一种灰色状态。
尼恩提示:三色标记法 的基本原理,请参见尼恩的专栏文章:
死磕GC:Java GC 和 GO GC 大对比,看完秒成高高手
三色标记法通过引入灰色,来标识那些当前对象在GC Root引用链上,但其引用的其他对象尚未完成标记的情况。
三色标记的具体定义如下:
- 黑色:表示当前对象不仅自身在GC Root的引用链上,而且它所引用的所有对象也已经被标记为存活。在位图实现中,黑色对象通过相应的bit位被标识为1。
- 灰色:表示当前对象在GC Root的引用链上,但其引用的其他对象可能尚未被标记。灰色对象不会直接体现在位图中,而是被放入一个专门的队列中,等待后续处理。
- 白色:表示对象不在GC Root的引用链上,因此可以被视为可回收的候选对象。在位图实现中,白色对象通过相应的bit位被标识为0。
在位图(bitmap)的实现中,G1垃圾回收器通常会使用1个bit来标识8个字节的内容。
例如,如果某个对象是黑色的,那么对应的bit位会被设置为1;如果是白色的,则bit位为0。
对于灰色对象,由于它们不会直接体现在位图中,因此位图中相应的bit位保持为0,而灰色对象会被单独放入一个队列中,以便后续处理。
如果某个对象的大小超过8个字节,通常只会使用其第一个bit位进行处理,以确保内存使用的效率。
通过这种方式,G1垃圾回收器能够在初始标记阶段快速准确地识别出从GC Root直接可达的存活对象,为后续的内存回收操作提供基础数据。
2. 并发标记
接下来,系统进入并发标记阶段,该阶段将并行处理之前尚未完成的标记任务,同时与用户线程并发执行,以实现更高的效率。
在这一阶段,系统从灰色队列中提取出尚未完成标记的对象B,并对其关联的A和C对象进行标记。
首先看对象A.
系统发现A对象并未引用其他任何对象,因此不用标记为灰色,可以立即将其标记为黑色,表示其已被完全标记且不会被回收。
再来看对象C.
然而,C对象引用了另一个对象E,因此C对象被暂时标记为灰色,并将其放入队列中等待进一步处理。
回来看对象B.
由于B对象已完成了对其所有引用对象的标记,因此也将B对象标记为黑色。
随后,系统从队列中获取C对象,并对其进行标记。
在这一过程中,系统确认C对象及其引用的E对象均已完成标记,因此将它们都标记为黑色。
此时,系统中剩余的对象F由于未被标记,因此被视为白色对象,即垃圾对象,可以被安全地回收。
然而,三色标记算法存在一个潜在的问题,即用户线程可能同时修改对象的引用关系,导致标记结果出现错误。
例如,在本案例中,正常情况下B和C都应该被标记为黑色。
但是,如果在B和C被标记之前,用户线程执行了B.c = null操作,将B到C的引用去除,
同时执行了A.c = C操作,添加了A到C的引用,那么就会出现严重问题。
因为此时C对象可能仍被错误地标记为白色或灰色,并被错误地视为可回收对象。
一旦C对象被错误地回收,而代码中仍然存在对C对象的引用,那么在后续执行过程中就会出现空引用异常等重大问题。
G1为了解决这个问题,使用了SATB技术(Snapshot At The Beginning, 初始快照)。
G1垃圾收集器为了克服三色标记算法在并发阶段可能遇到的对象引用变化问题,引入了SATB(Snapshot At The Beginning,初始快照)技术。
SATB技术的核心思想是:在标记过程的起始阶段捕捉一个对象的快照,并基于这个快照来进行后续的标记工作。
SATB技术的具体实现如下:
-
在标记阶段开始时,G1垃圾收集器会创建一个当前所有对象的快照。
在这个快照之后新生成的对象,由于它们尚未被任何旧对象引用,因此它们会被直接标记为黑色,表示它们是活跃的,不应该被回收。
-
为了处理在标记过程中可能发生的对象引用变化,G1采用了 前置写屏障技术。
前置写屏障技术 会在引用赋值操作(如
B.c = null
)之前被触发,将即将被改变引用的对象(在这个例子中是C
)放入SATB待处理队列中。每个线程都有自己的SATB队列,但最终这些队列会被汇总到一个全局的SATB队列中。
3. 最终标记(Final Mark)
在标记阶段的最后,所有用户线程会被暂停,以处理SATB相关的对象标记。
这一步是必要的,因为只有在所有线程都停止执行后,我们才能确保所有的引用变化都已经被捕获并处理。
在这个阶段,所有线程的SATB队列中剩余的数据会被合并到全局的SATB队列中,并逐一进行处理。
对于SATB队列中的对象,它们默认会被按照存活对象来处理,同时还会处理它们引用的其他对象。
这意味着,即使一个对象在标记过程中被解除了引用,只要它曾经被引用过,并且这个引用变化被SATB捕获,那么这个对象就不会被错误地回收。
然而,SATB技术也有其缺点。
由于它基于初始快照进行标记,因此在本轮垃圾回收过程中,可能会将一些实际上应该被回收的不存活对象错误地标记为存活对象。
这些错误标记的对象被称为 “浮动垃圾”。这些浮动垃圾需要等到下一轮垃圾回收时才能被正确回收。
深度问题:SATB “浮动垃圾” 怎么来的?
浮动垃圾(Floating Garbage)是并发垃圾回收过程中产生的,在垃圾回收器(例如 G1 和 CMS)的 并发标记阶段出现。它的产生机制与 SATB(Snapshot-at-the-Beginning) 标记算法直接相关。
浮动垃圾的产生机制:
- 并发标记阶段的并发性:
- 垃圾回收器(例如 G1、CMS)为了减少暂停时间,通常在回收过程中让应用程序的线程继续运行。这个阶段被称为并发标记阶段,回收器通过 SATB 算法来标记堆中的存活对象。
- 在此阶段,垃圾回收器开始标记那些通过根对象(GC Roots)可达的对象。SATB 通过在标记阶段开始时捕捉堆的快照来记录对象的引用关系,并据此标记对象。
- SATB 算法的标记策略:
- SATB 使用 开始时的快照(这个是一个静态的快照),即在标记阶段开始时,存活对象已经在快照里边被标记。
- 在并发标记期间,应用程序继续运行,存活对象 可能已经被 改变了引用关系。例如,一个对象在标记阶段开始时是存活的,但在标记结束之前,已经没有应用关系,已经死了。但是SATB 并不知道。
- 浮动垃圾的形成:
- 在标记阶段开始时,如果一个对象是存活的(即它被引用),但在标记过程中应用程序修改了引用关系,使得该对象变得不再被引用,理论上该对象在本轮标记结束时已经是垃圾。
- 然而,由于 SATB 是基于标记开始时的快照,在标记结束前,标记器不会察觉这个对象已经变成垃圾,因此它仍然被认为是存活的,无法在当前回收周期内被回收。
- 这些在标记过程中变成垃圾但未能及时被标记器识别的对象就形成了浮动垃圾。它们不会被当前的回收周期回收,而是留到下一个垃圾回收周期才会被处理。
为什么会有浮动垃圾?
- 浮动垃圾是并发回收的自然现象,主要源于垃圾回收器在标记阶段与应用程序线程的并发执行。在这种并发执行的环境中,程序的状态会不断变化,而垃圾回收器基于“快照”进行标记,因此只能根据标记开始时的状态来做判断,无法实时跟踪对象的存活状态变化。
- SATB 的快照机制虽然减少了停顿时间和性能开销,但由于它是基于标记开始时的状态,如果在标记阶段结束前对象的引用发生变化(如对象被废弃),这些对象就会变成“浮动垃圾”。
浮动垃圾的影响与应对:
- 影响:
- 浮动垃圾无法在当前 GC 周期内被回收,它们会暂时继续占用内存。这会导致短期内内存占用的增加,甚至可能导致内存膨胀。
- 如果浮动垃圾过多,可能会增加下一次 GC 的负担,并可能在系统的极端高并发下引发内存压力。
- 应对方法:
- 增加堆内存:如果浮动垃圾引起内存膨胀,可以增加堆内存来缓解短期内的内存压力。
- 优化 GC 配置:通过调节垃圾回收器的参数,如 G1 的暂停时间目标,或者调节老年代与年轻代的比例,优化内存的回收频率,减少浮动垃圾的积累。
- 调整应用代码:有时候应用程序的对象生命周期管理可以优化,减少在并发标记期间频繁创建和丢弃对象的情况,降低浮动垃圾的数量。
浮动垃圾是并发标记过程中产生的,它们在标记开始时是存活的,但在标记结束前已经成为垃圾。
由于 SATB 算法基于“开始时快照”,这些垃圾无法被当前回收周期识别和清理,只能等待下一个回收周期。
这是并发垃圾回收的副作用之一,但可以通过合理的内存配置和调优来减轻影响。
4. 清理(Cleanup)
计算标记区域的活动对象数量,清理没有存活对象的区域(标记后没有存活对象,并不是正经的回收阶段),对区域排序等 (部分STW)。
5. 转移
在垃圾回收过程中的“转移”步骤,通常涉及到将存活的对象从一个内存区域复制到另一个内存区域,以便清理包含大量垃圾对象的区域。
转移步骤详解:
-
区域选择:
根据最终标记的结果,垃圾收集器会分析每个内存区域中垃圾对象所占用的内存大小。
在此基础上,结合预期的停顿时间,垃圾收集器会选择转移效率最高的若干个区域进行转移操作。
选择的标准通常是基于垃圾对象数量和区域的整体活跃对象比例,以最大化单次转移过程中的清理效率。
-
对象转移:
在选择好目标区域后,垃圾收集器会开始转移过程。
转移时,首先会处理GC Root直接引用的对象,这些对象通常是垃圾回收过程中的根节点,它们保证了程序的运行不会因垃圾回收而中断。
在复制这些对象之后,垃圾收集器会继续转移其他非直接引用的对象,直到所有选定区域中的存活对象都被复制到新的内存区域。
-
引用关系更新:
在对象转移完成后,垃圾收集器会清理掉原先区域中的垃圾对象,释放相应的内存空间。
如果外部的其他区域对象引用了已经被转移的对象,垃圾收集器还需要更新这些引用关系,确保它们指向新的内存位置。
这一步骤是确保程序在垃圾回收后能够继续正确运行的关键。
通过更新引用关系,垃圾收集器确保了程序内部的对象引用不会因为内存位置的改变而失效。
通过上述步骤,垃圾收集器能够有效地进行内存整理,减少内存碎片,提高内存的使用效率,并为应用程序提供持续稳定的运行环境。
11.7. G1的Full GC
当混合回收无法跟上内存分配的速度,导致老年代也满了,就会进行Full GC对整个堆进行回收。
G1垃圾回收器在面对全堆内存回收需求时,会执行Full GC。
G1中的Full GC也而是单线程串行的,而且是全暂停,使用的是标记-整理算法,代价非常高。
Full GC在G1中不是一个常规操作,因为它通常意味着G1的常规回收策略无法满足内存回收需求。
以下是G1执行Full GC的流程:
-
触发条件:
Full GC通常在以下情况下被触发:
- 老年代的占用率达到了一定阈值,无法为新晋升的对象提供足够的空间。
- 无法找到足够的连续空间来存放大对象(Humongous Objects)。
- 在并发标记阶段,老年代被填满,无法等待并发标记完成。
- 显式调用
System.gc()
,尽管G1会尽量忽略这个请求,但某些情况下仍然可能触发。
-
执行过程:
-
G1的Full GC会暂停所有应用线程(Stop-The-World)。
-
进行全堆的标记、清理和压缩整理。
-
Full GC这个过程可能会使用Serial Old收集器,它是单线程的,会进一步增加GC的停顿时间。
"Serial Old" 垃圾回收(GC)指的是 Java 虚拟机(JVM)中的一种垃圾回收器,属于 老年代(Old Generation) 的垃圾回收机制。它是基于 标记-整理(Mark-Compact) 算法的单线程回收器,通常与 Serial GC 搭配使用,形成一种简单、低开销但不适合高并发环境的垃圾回收策略。
-
Full GC会尝试回收所有可回收的对象,包括年轻代和老年代。
-
-
优化建议:
- 避免Full GC的关键是保持G1的Mixed GC和Young GC的有效运行,确保它们能够及时回收内存。
- 监控和分析GC日志,识别触发Full GC的原因,并进行相应的优化,如增加堆大小、调整G1的参数等。
- 避免在老年代中分配过大的对象,或者确保有足够的连续空间来存放这些大对象。
-
日志记录:
- G1在执行Full GC时,会在GC日志中记录相关信息,如
[Full GC (Allocation Failure)]
,表示由于分配失败触发了Full GC。
- G1在执行Full GC时,会在GC日志中记录相关信息,如
-
性能影响:
- Full GC是一个昂贵的操作,因为它涉及到整个堆的回收,可能会导致显著的停顿时间,影响应用程序的性能。
-
避免策略:
- 通过合理配置G1的参数,如
-XX:InitiatingHeapOccupancyPercent
、-XX:G1HeapWastePercent
和-XX:G1MixedGCCountTarget
,可以调整G1的行为,以减少Full GC的发生。
- 通过合理配置G1的参数,如
在实际应用中,应该尽量避免Full GC的发生,因为它会显著增加GC的停顿时间,影响应用程序的响应性能。
通过监控GC日志和调整G1的参数,可以优化G1 GC的性能,减少Full GC的发生。
暂停时间的控制
G1在移动过程中虽然也是全暂停,不过G1在选择回收集合上是变化的,每次只选择部分的区域进行回收,通过计算每个区域的预测暂停时间来保证每次回收所占用的时间。
简单的说就是将一次完整的GC拆分成多次短时间的GC从而降低暂停的时间,尽量保证每次的暂停时间在用户的配置范围(-XX:MaxGCPauseMilli)内。
年轻代大小的配置
G1为了控制暂停时间,年轻代最大区域数是动态调整的,不过如果手动设置了年轻代大小,比如Xmn/MaxNewSize/NewRatio等,并且年轻代最大和最小值一样,那么相当于禁用了这个最大区域数调整的功能,
禁用了这个最大区域数调整的功能,就可能会导致暂停时间控制的失效,,因为年轻代GC是选择全部区域的,区域过多会导致暂停时间的增加。
所以G1中尽量不要设置年轻代的大小,让G1自动的进行调整
11.8. CMS与G1的区别
关于JVM 垃圾收集器 ,面试常问CMS与G1的区别问题,总是记不住。
如下表格,从不同维度对比CMS和G1两个收集器。
区别 | CMS | G1 |
---|---|---|
回收对象 | 回收老年代需要配合新生代收集器一起使用 | 老年代和新生代 |
STW时间 | 以最小停顿时间为目标 | 可预计的垃圾回收停顿时间 |
回收算法 | 标记清除 | 标记整理 |
垃圾碎片 | 产生内存碎片 | 没有内存碎片 |
垃圾回收过程 | 1 初始标记(STW) 2 并发标记 3 重新标记(STW) 4 并发清除 |
1 初始标记(STW) 2 并发标记 3 最终标记(STW) 4 筛选回收(STW) |
浮动垃圾 | 会产生浮动垃圾(第四阶段产生) | 没有浮动垃圾(第四阶段,用户线程卡停) |
浮动垃圾产生原因 | 第四阶段并发清楚,GC线程和用户线程同时运行,用户线程会产生浮动垃圾 | |
浮动垃圾导致结果 | 浮动垃圾导致内存不足时候,出现“Concurrent Mode Failure”,出现此错误时就会切换到SerialOld收集模式 | |
大对象处理 | 直接进入老年代 | 如果大于一个region的50%,会横跨多个region进行存放 |
优点 | 并发收集,低停顿 | 1 控制垃圾回收时间:选择一组合适的region最为回收目标,达到实时收集目的 2 空间整理:不会产生空间碎片 |
缺点 | 1 标记清除,产生大量内存碎片。(导致fullGc) 2 无法处理浮动垃圾,内存不足时出现“Concurrent Mode Failure”(并发模式故障),切换到SerialOld收集模式 3 CPU敏感资源敏感,第二阶段并发阶段虽然不会导致用户线程停顿,但如果再CPU资源不足情况下,应用会有明显卡顿 |
|
使用场景 | 1.JDK8及更高版本同等环境下只要cpu性能比较好并且内存不算大 (最少4G)可以使用CMS 2.JDK7及更低版本同等环境下 可选择CMS (G1不完善 |
1.G1适合8/16G以上的内存使用 2.实时数据占用超过一半的堆空间 3.对象分配或者晋升的速度变化大 4.希望消除长时间的GC停顿 |
11.9. G1的 配置参考
规格实例 | 配置参数 |
---|---|
1C2G | 4g以下,建议使用cms |
4C8G | -Xmx6G -Xms6G -Xss512K -XX:MaxMetaspaceSize=320M -XX:MetaspaceSize=320M -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:ParallelGCThreads=4 -XX:ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=50 -XX:-OmitStackTraceInFastThrow -XX:+ParallelRefProcEnabled -XX:+PrintGCDetails -XX:+PrintGCDateStamps |
8C16G | -Xmx12G -Xms12G -Xss512K -XX:MaxMetaspaceSize=320M -XX:MetaspaceSize=320M -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=2 -XX:InitiatingHeapOccupancyPercent=40 -XX:-OmitStackTraceInFastThrow -XX:+ParallelRefProcEnabled -XX:+PrintGCDetails -XX:+PrintGCDateStamps |
11.10. G1的GC垃圾回收日志解读
年轻代GC日志(完全年轻代)
//[GC pause (G1 Evacuation Pause) (young) 代表完全年轻代回收
// 0.0182341 secs 是本次GC的暂停时间
0.184: [GC pause (G1 Evacuation Pause) (young), 0.0182341 secs 是本次GC的暂停时间]
// 并行GC线程,一共有8个
[Parallel Time: 16.7 ms, GC Workers: 8]
/*这一行信息说明的是这8个线程开始的时间,Min表示最早开始的线程时间,Avg表示平均开始时间,Max表示的是最晚开始时间,Diff为最早和最晚的时间差。这个值越大说明线程启动时间越不均衡。线程启动的时间依赖于GC进入安全点的情况。关于安全点可以参考后文的介绍。*/
[GC Worker Start (ms): 184.2 184.2 184.2 184.3 184.3 184.4 186.1 186.1
Min: 184.2, Avg: 184.7, Max: 186.1, Diff: 1.9]
/*根处理的时间,这个时间包含了所有强根的时间,分为Java根,分别为Thread、JNI、CLDG;和JVM根下面的StringTable、Universe、JNI Handles、ObjectSynchronizer、FlatProfiler、Management、SystemDictionary、JVMTI */
[Ext Root Scanning (ms): 0.3 0.2 0.2 0.1 0.1 0.0 0.0 0.0
Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.8]
/*Java线程处理时间,主要是线程栈。这个时间包含了根直接引用对象的复制时间,如果根超级大,这个时间可能会增加 */
[Thread Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[StringTable Roots (ms): 0.0 0.1 0.1 0.1 0.1 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
[Universe Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[JNI Handles Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[ObjectSynchronizer Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[FlatProfiler Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Management Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[SystemDictionary Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[CLDG Roots (ms): 0.3 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.3, Diff: 0.3, Sum: 0.3]
[JVMTI Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// CodeCache Roots实际上是在处理Rset的时候的统计值,它包含下面的
// UpdateRS,ScanRS和Code Root Scanning
[CodeCache Roots (ms): 5.0 3.9 2.2 3.3 2.1 2.2 0.6 2.2
Min: 0.6, Avg: 2.7, Max: 5.0, Diff: 4.4, Sum: 21.6]
[CM RefProcessor Roots (ms): 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Wait For Strong CLD (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Weak CLD Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[SATB Filtering (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 这个就是GC线程更新RSet的时间花费,注意这里的时间和我们在Refine里面处理RSet
// 的时间没有关系,因为它们是不同的线程处理
[Update RS (ms): 5.0 3.9 2.2 3.3 2.1 2.2 0.6 2.2
Min: 0.6, Avg: 2.7, Max: 5.0, Diff: 4.4, Sum: 21.5]
// 这里就是GC线程处理的白区中的dcq个数
[Processed Buffers: 8 8 7 8 8 7 2 4
Min: 2, Avg: 6.5, Max: 8, Diff: 6, Sum: 52]
// 扫描RSet找到被引用的对象
[Scan RS (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): 0.0 0.0 0.0 0.0 0.0 0.1 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
// 这个就是所有活着的对象(除了强根直接引用的对象,在Java根处理时会直接复制)复制
// 到新的分区花费的时间。从这里也可以看出复制基本上是最花费时间的操作。
[Object Copy (ms): 11.3 12.5 14.2 13.1 14.3 14.2 14.2 12.5
Min: 11.3, Avg: 13.3, Max: 14.3, Diff: 3.0, Sum: 106.3]
// GC线程结束的时间信息。
[Termination (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: 1 1 1 1 1 1 1 1
Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
// 这个是并行处理时其他处理所花费的时间,通常是由于JVM析构释放资源等
[GC Worker Other (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
// 并行GC花费的总体时间
[GC Worker Total (ms): 16.6 16.6 16.6 16.5 16.5 16.4 14.7 14.7
Min: 14.7, Avg: 16.1, Max: 16.6, Diff: 1.9, Sum: 128.7]
// GC线程结束的时间信息
[GC Worker End (ms): 200.8 200.8 200.8 200.8 200.8 200.8 200.8 200.8
Min: 200.8, Avg: 200.8, Max: 200.8, Diff: 0.0]
// 下面是其他任务部分。
// 代码扫描属于并行执行部分,包含了代码的调整和回收时间
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
// 清除卡表的时间
[Clear CT: 0.1 ms]
[Other: 1.5 ms]
// 选择CSet的时间,YGC通常是0
[Choose CSet: 0.0 ms]
// 引用处理的时间,这个时间是发现哪些引用对象可以清除,这个是可以并行处理的
[Ref Proc: 1.1 ms]
// 引用重新激活
[Ref Enq: 0.2 ms]
// 重构RSet花费的时间
[Redirty Cards: 0.1 ms]
[Parallel Redirty: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Redirtied Cards: 8118 7583 6892 4496 0 0 0 0
Min: 0, Avg: 3386.1, Max: 8118, Diff: 8118, Sum: 27089]
// 这个信息是是可以并行处理的,这里是线程重构RSet的数目
// 大对象处理时间
[Humongous Register: 0.0 ms]
[Humongous Total: 2]
// 这里说明有2个大对象
[Humongous Candidate: 0]
// 可回收的大对象0个
// 如果有大对象要回收,回收花费的时间,回收的个数
[Humongous Reclaim: 0.0 ms]
[Humongous Reclaimed: 0]
// 释放CSet中的分区花费的时间,有新生代的信息和老生代的信息。
[Free CSet: 0.0 ms]
[Young Free CSet: 0.0 ms]
[Non-Young Free CSet: 0.0 ms]
// GC结束后Eden从15M变成0,下一次使用的空间为21M,S从2M变成3M,整个堆从
// 23.7M变成20M
[Eden: 15.0M(15.0M)->0.0B(21.0M) Survivors: 2048.0K->3072.0K
Heap: 23.7M(256.0M)->20.0M(256.0M)]
老年代垃圾回收(部分年轻代/混合回收)日志
并发标记日志
并发标记是全局的,和回收过程是两个阶段,所以并发标记可以说是独立的。
//并发标记 - 初始标记阶段,在年轻代GC中完成
100.070: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0751469 secs]
[Parallel Time: 74.7 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 100070.4, Avg: 100070.5, Max: 100070.6, Diff:
0.1]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum:
1.6]
[Update RS (ms): Min: 0.6, Avg: 1.1, Max: 1.5, Diff: 0.9, Sum: 8.9]
[Processed Buffers: Min: 1, Avg: 1.6, Max: 4, Diff: 3, Sum: 13]
[Scan RS (ms): Min: 1.0, Avg: 1.4, Max: 1.9, Diff: 0.9, Sum: 10.8]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum:
0.0]
[Object Copy (ms): Min: 71.5, Avg: 71.5, Max: 71.6, Diff: 0.1, Sum: 572.1]
[Termination (ms): Min: 0.3, Avg: 0.3, Max: 0.4, Diff: 0.1, Sum: 2.6]
[Termination Attempts: Min: 1382, Avg: 1515.5, Max: 1609, Diff: 227,
Sum: 12124]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
[GC Worker Total (ms): Min: 74.5, Avg: 74.5, Max: 74.6, Diff: 0.1, Sum:
596.3]
[GC Worker End (ms): Min: 100145.1, Avg: 100145.1, Max: 100145.1, Diff:
0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.4 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 23.0M(23.0M)->0.0B(14.0M) Survivors: 4096.0K->4096.0K Heap: 84.5M
(128.0M)->86.5M(128.0M)]
[Times: user=0.63 sys=0.00, real=0.08 secs]
// 把YHR中Survivor分区作为根,开始并发标记根扫描
100.146: [GC concurrent-root-region-scan-start]
// 并发标记根扫描结束,花费了0.0196297,注意扫描和Mutator是并发进行,同时有多个线程并行
100.165: [GC concurrent-root-region-scan-end, 0.0196297 secs]
// 开始并发标记子阶段,这里从所有的根引用:包括Survivor和强根如栈等出发,对整个堆进行标记
100.165: [GC concurrent-mark-start]
// 标记结束,花费0.08848s
100.254: [GC concurrent-mark-end, 0.0884800 secs]
// 这里是再标记子阶段,包括再标记、引用处理、类卸载处理信息
100.254: [GC remark 100.254: [Finalize Marking, 0.0002228 secs] 100.254:
[GC ref-proc, 0.0001515 secs] 100.254: [Unloading, 0.0004694 secs],
0.0011610 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
// 清除处理,这里的清除仅仅回收整个分区中的垃圾
// 这里还会调整RSet,以减轻后续GC中RSet根的处理时间
100.255: [GC cleanup 86M->86M(128M), 0.0005376 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
混合回收日志
// 混合回收Mixed GC其实和YGC的日志类似,能看到GC pause(G1EvacuationPause)(mixed)这样的信息
// 日志分析参考Y年轻代GC。
122.132: [GC pause (G1 Evacuation Pause) (mixed), 0.0106092 secs]
[Parallel Time: 9.8 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 122131.9, Avg: 122132.0, Max: 122132.0,
Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.7]
[Update RS (ms): Min: 0.5, Avg: 0.7, Max: 0.9, Diff: 0.4, Sum: 5.4]
[Processed Buffers: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 14]
[Scan RS (ms): Min: 1.0, Avg: 1.3, Max: 1.5, Diff: 0.5, Sum: 10.4]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum:
0.0]
[Object Copy (ms): Min: 7.5, Avg: 7.6, Max: 7.7, Diff: 0.2, Sum: 60.9]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Termination Attempts: Min: 92, Avg: 105.1, Max: 121, Diff: 29, Sum: 841]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 9.7, Avg: 9.7, Max: 9.8, Diff: 0.1, Sum: 77.6]
[GC Worker End (ms): Min: 122141.7, Avg: 122141.7, Max: 122141.7, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.2 ms]
[Other: 0.7 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.5 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 3072.0K(3072.0K)->0.0B(5120.0K) Survivors: 3072.0K->1024.0K
Heap: 105.5M(128.0M)->104.0M(128.0M)]
[Times: user=0.00 sys=0.00, real=0.01 secs]
11.11. G1 常用参数
G1 GC需要配置参数:-XX:+UseG1GC
开启
大多数情况下的GC调优,调的只是一些内存/比例/时间,对各种线程数调整的场景很少,更多的是基于GC日志来分析修改代码,所以一般默认参数下也足够了。
# 启动G1
-XX:+UseG1GC
# 最小堆内存
-Xms8G
# 最大堆内存
-Xmx8G
# metaspace初始值
-XX:MetaspaceSize=256M
# 期望的最大暂停时间,默认200ms
-XX:MaxGCPauseMillis
# 简称为IHOP,默认值为45,这个值是启动并发标记的阈值,当老年代使用内存占用堆内存的45%启动并发标记。
# 如果该过大,可能会导致mixed gc跟不上内存分配的速度从而导致full gc
-XX:InitiatingHeapOccupancyPercent
# G1自动调整IHOP的指,JDK9之后可用
-XX:+G1UseAdaptiveIHOP
# 并发标记时可以卸载Class,这个操作比较耗时,对Perm/MetaSpace进行清理,默认未开启
-XX:+ClassUnloadingWithConcurrentMark
# 多个线程并行执行java.lang.Ref.*,对象回收前的引用处理
-XX:-ParallelRefProcEnabled
这里只列出了一些最基础的参数。
1. 堆内存配置 相关参数
配置 | 说明 | 建议 |
---|---|---|
-Xms8G | 堆内存最小值 | 一般为pod内存的80% |
-Xmx8G | 堆内存最大值 | 一般为pod内存的80% |
-Xss512K | 线程栈大小,默认1M | 512K或1M |
-XX:G1NewSizePercent=30 | 年轻代占整个堆的比例最小值 (需开启-XX:+UnlockExperimentalVMOptions) | 对于启动pod时获取缓存的场景, 可适当调大, 减少启动阶段的频繁调整 |
-XX:G1MaxNewSizePercent=40 | 年轻代占整个堆已使用的比例最大值 (需开启-XX:+UnlockExperimentalVMOptions) | 大堆建议调小阈值(>=32G), 降低单次年轻代回收的STW时间 |
-XX:G1HeapRegionSize=4M | G1 Region的大小. 值是 2 的幂, 范围是 1 MB 到 32 MB 之间 | 小堆且常态有持续大对象产生的场景, 可适当调大 |
关于Region Size可参考:
Heap Size | Region Size |
---|---|
heap < 4GB | 1MB |
4GB <= heap < 8GB | 2MB |
16GB <= heap < 32GB | 8MB |
32GB <= heap < 64GB | 16MB |
64GB <= heap | 32MB |
2. GC配置 相关参数
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=50 -XX:MaxTenuringThreshold=10
配置 | 说明 | 建议 |
---|---|---|
-XX:+UseG1GC | 开启G1垃圾回收 | |
-XX:MaxGCPauseMillis=100 | 预期GC STW时间,单位ms | 看场景,低时延的可配置到100ms以内,最大不超过500ms |
-XX:InitiatingHeapOccupancyPercent=50 | 触发GC标记周期的堆占用百分比 | 大堆(>=32GB)时适当调大阈值, 减少标记触发 |
-XX:G1HeapWastePercent=10 | 默认值 : 10% , 堆浪费百分比, 当G1发现可被回收的空间小于10%时, 就不会再进行混合收集, 也就是会结束当前的混合收集周期 | |
-XX:G1OldCSetRegionThresholdPercent=10 | 默认值 : 堆的5%, 设置混合垃圾回收期间要回收的最大旧区域数占整个堆的百分比 | |
-XX:G1MixedGCLiveThresholdPercent=70 | 默认值 : 65%, MixGC时, 年老代Region中存活对象百分比, 只有在此阈值下的Region才会被选入回收列表CSet (需开启-XX: +UnlockExperimentalVMOptions ) | 大堆(>=32GB)时适当调大阈值, 增加扫描年老带Region的备选集合, 增加年老代回收效率 |
-XX:G1MixedGCCountTarget=8 | 最多8次混合垃圾回收, 设置标记周期完成后, 对存活数据上限为 G1MixedGCLIveThresholdPercent 的旧区域执行混合垃圾回收的目标次数 | |
-XX:MaxTenuringThreshold=n | 默认值15, 对象进入年老带前在Survivor区中的最大存活年龄 | survivor区过大时, 可以跟进GC Log分析分代存活分布, 适当调小阈值, 减轻复制压力, 让对象快速进入年老代,降低young gc压力. |
3. NMT 本地内存相关参数
-XX:NativeMemoryTracking=detail
配置 | 说明 | 建议 |
---|---|---|
-XX:NativeMemoryTracking | 默认值 : 关闭 [summary/detail] , 用于堆外内存泄露分析 |
4. GC日志配置 相关参数
// Java8: -XX:+PrintGCCause -XX:+PrintGCDetails -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy -XX:+PrintTenuringDistribution -XX:+PrintReferenceGC -XX:+PrintHeapAtGC -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=2M // Java11以上: -Xlog:gc*=info,phases*=debug,region*=debug,age*=trace,ergo*=debug,safepoint,heap*=debug:file=gc.log:time,level,tags:filecount=5,filesize=2m
配置 | 说明 | 建议 |
---|---|---|
-XX:+PrintGCCause | 打印触发GC的原因 | 默认开启 |
-XX:+PrintGCDetails | 打印GC各阶段详细日志 | 默认开启 |
-XX:+PrintGCApplicationConcurrentTime | 打印GC并行处理时间 | 不开启 |
-XX:+PrintGCApplicationStoppedTime | 打印GC STW时间 | 默认开启 |
-XX:+PrintAdaptiveSizePolicy | 打印自适应分代调整信息 | 默认开启 |
-XX:+PrintTenuringDistribution | 打印GC扫描存活对象年龄分布 | 默认开启 |
-XX:+PrintReferenceGC | 打印引用GC处理详情 | 默认开启 |
-XX:+PrintGCDateStamps | 打印GC绝对日期, 默认为从JVM开始的相对时间戳 | 默认开启 |
-XX:+PrintGCTimeStamps | 打印GC绝对时间, 默认为从JVM开始的相对时间戳 | 默认开启 |
-XX:+PrintHeapAtGC | 打印GC前后堆内存详情 | 默认开启 |
5 其他 参数
配置 | 说明 | 建议 |
---|---|---|
-XX:-UseBiasedLocking | 默认值 : 偏向锁开启。当锁竞争不激烈时可以通过偏向来提高性能. 但是高并发的情况下, 偏向锁会经常失效, 取消偏向锁时, 需要获取每个线程使用锁的状态以及运行状态,该过程会STW | |
-XX:-OmitStackTraceInFastThrow | 默认值 : 开启 | 同一位置不断抛出同样的Exception, JIT会丢弃原始堆栈和Message来提升性能. 无脑关闭 |
11.12. G1 调优实战
G1 调优实战 ,这里暂时省略,后面通过《尼恩Java面试宝典》配套视频,进行展示。
11.13. 结语
在JVM调优中,关键在于准确识别系统的性能瓶颈和优化方向,选择适合的调优策略和参数。
实施调优方案后,必须验证效果,并持续监控系统性能,及时调整优化策略和参数以保持系统高性能和稳定性。
同时,需要及时发现和解决各种潜在的性能问题,如内存泄漏、CPU飙升、频繁的垃圾回收等,以确保系统在高负载和复杂环境下能够保持卓越的性能表现。
总之,JVM调优是一个持续改进的过程,通过对系统性能的深入分析和优化,确保Java应用程序在各种情况下都能够保持高效稳定的运行状态。
说在最后:有问题找老架构取经
通过JVM底层原理和调优的的深度回答,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》