阿里妹导读
本文记录了作者升级到JDK11后,使用G1GC导致内存利用率飙升至90%以上的问题及其解决方案。
背景
7 月份的时候,由于发现集团已经提供 JDK11 的流水线升级,可以通过流水线快速升级 JDK11,并解决相关的依赖问题。于是我欢天喜地地升级了 JDK11,在预发经过测试后没有问题后,顺利发布上线,GC 次数有了明显下降。
故障出现
线上稳定运行了半个月,突然开始触发告警,内存利用率超过 85%。一看监控,发现出现了几个现象。
1.内存利用率不断升高,提升到 85%,一天内没有下降。
2.G1 Old GC 不断升高,没有进行回收。
当时我设置的 JVM 参数非常简单,只保留了 CMS 的堆内存设置,最大最小堆内存都是 12G。
-Xms12g -Xmx12g
通过 jmap 查看 heap 情况(jhsdb jmap --heap --pid 185579):
临时解决方案
1.保留一台 beta 环境机器进行观察,其他环境机器批量重启。
2.重启机器后,内存利用率下降,因此初步认定,造成告警的原因是 G1_OLD 区未触发回收。
观察情况
beta 机器在未重启的情况下,内存使用率不断提高,最后超过 90%(这也是本文标题由来)。
最终解决方案
经过查询资料之后,最后的解决方案就是调整 JVM 参数,将堆内存缩减到8G,问题解决。
简单结论:一般8G容器的堆内存建议设置4G,16G容器堆内存建议设置8G。
那么从 JDK8 升级到 JDK11 之后,发生了什么呢?为什么同样的参数在 CMS 上一点问题没有,上到 G1GC 之后却有这样的问题呢?接下来我们好好介绍 G1GC。
从 GC 开始
内存中创建对象时发生了什么?
内存分配的两种手段
Java 最核心的一点特性就是它的内存管理机制。要聊 GC,我们先从内存分配开始。每个 Java 对象都需要在内存中有一个空间,用于保存它的数据和引用关系。那么创建对象时,怎么在内存中划出一份空间给对象呢?最直觉的方法跟数组的使用是一致的。把内存认为是一个数组,用一个内存指针作为索引指向当前空闲空间的开头,当需要为对象分配空间时,由于对象的大小是确定的,因此可以直接把指针后面的空间直接分给当前对象,只需要将指针移动指定大小的位置即可完成分配。这种方式就叫做指针碰撞。
指针碰撞:它通过维护一个指针,指向空闲内存的起始位置。当需要分配内存时,只需将该指针向前移动所需内存大小的距离即可。
通过这种技术进行内存分配后,当出现对象被回收的情况时,因为指针只有一个,因此无法把前面回收的部分进行再次分配,它只能继续往后分配。因此又出现了一种新的技术。空闲列表技术。它的本质正如它的名字一样,使用一个列表来记录内存中的空闲位置,每次分配对象时,就会查找这个列表,找到一块大小可以分配当前对象的空闲空间,然后把对象放到这块空间里,再从空闲列表中把这块空闲区域去掉。
空闲列表:它维护一个列表,记录所有可用的内存块信息。当需要分配内存时,从列表中查找合适大小的空闲块进行分配。
多线程分配时怎么办?
以上两种技术,遇到多线程时要怎么处理呢?对于指针碰撞这种方案,最简单的方式就是给每个线程划分一个空间,每个线程在自己的空间中进行内存分配,这样就不会发生冲突。这也就是 JVM 中 TLAB 的由来,(Thread Local Allocation Buffer)线程本地分配缓存区,每次 GC 结束后,每个线程都会申请一块 TLAB 进行内存分配,当 TLAB 申请不到时,线程会尝试在 Eden 区分配,若 Eden 也不足,则可能触发 Young GC。
而对于空闲列表,多线程的分配方案就是乐观锁机制,也就是CAS+失败重试。
TLAB (Thread Local Allocation Buffer):线程本地分配缓存区,每次 GC 结束后,每个线程都会申请一块 TLAB 进行内存分配。
内存回收
内存回收的算法
只有了解了内存的分配方式,我们才能更加深刻地理解内存回收的方案。针对前面我们说的两种内存分配方式,我们可以提出以下这些内存回收的方案。
1.标记-清除算法:标记出使用的对象,然后将没有标记的对象清理。
- 缺点:随着对象增多,标记和清理时间增加;会产生内存碎片。
2.标记-复制算法:为了解决面对大量可回收对象时,清理时间增加的情况。出现了一种半区复制的算法。也就是将内存分成两部分,每次使用其中一部分,回收时,将少量剩余的存活对象按顺序复制到另一部分。
- 优点:执行简单,回收效率高
- 缺点:内存只能使用一半,对内存是一种浪费。
- 后续改进:将内存分为三部分,伊甸园区、幸存者 1 区 、幸存者 2 区,也就是我们 GC 参数中常见的 8:1:1 的由来。
3.标记-整理:这个跟标记-复制有点像,核心差异点是,标记复制的对象移动是在两个区来回移动的。而标记整理只会往内存的一端移动。
- 优点:没有空间碎片
- 缺点:移动时需要暂停用户程序,也就是触发 STW
分代回收
分代回收,是指将内存区域分为新生代和老年代,不同代使用不同的回收手段。而这个分代回收是建立在两条原则上的
1.绝大多数对象都是朝生夕死的。
2.熬过越多次垃圾收集过程的对象就越难以消亡。
如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空,因此使用标记-复制算法。
对于老年代的对象,由于他们难以使用消亡,频繁的挪动成本非常的高,使用空闲列表的方式进行内存的分配和回收往往是性价比更高的方案。
CMS 回收就是使用标记清除算法,标记垃圾,然后清除,再使用空闲列表记录空闲区域。但是这样,当内存碎片很多时,会存在总体空闲内存足够多,但是却不能给对象分配空间的情况。这个时候,CMS 就会使用标记-整理算法,进行一次内存空间整理。
高吞吐和低延迟不能兼得
高吞吐(High Throughput)
高吞吐是指GC系统在单位时间内能够处理的内存回收量。
低延迟(Low Latency)
低延迟是指垃圾回收过程中应用程序暂停的时间尽可能短。在低延迟的GC策略下,系统会尽量减少每次垃圾回收操作的停顿时间,即减少应用程序因等待GC完成而停止响应的时间。
不可兼得
高吞吐和低延迟之间存在一种权衡关系。通常情况下,为了实现高吞吐,GC可能会采取一些策略,比如增加垃圾回收的频率或者扩大垃圾回收的范围,这可能会导致每次垃圾回收的时间变长,从而增加了延迟。
相反,为了实现低延迟,GC可能会采取减少每次垃圾回收的范围或者优化垃圾回收算法来减少停顿时间,但这可能会增加垃圾回收的总次数,从而降低了吞吐量。
GC 策略转变
回顾内存管理技术的发展历程,GC 策略的演进呈现出明显的方向性转变:从追求高吞吐量逐步转向对低延迟的优化,以适配现代互联网服务的性能要求。早期的垃圾收集算法以提升系统吞吐量为首要目标,其设计理念导致在极端场景下可能产生较高的延迟波动。随着Java语言在互联网服务领域的广泛应用,加之硬件成本下降带来的内存容量提升,系统架构逐渐能够承载更频繁的垃圾回收操作,以此换取更稳定的服务响应。
这一技术演变路径在主流垃圾收集器的迭代中清晰可见:从采用并发标记清除(CMS)机制,到引入分代式收集器(G1),直至实现亚毫秒级停顿的ZGC无停顿收集器,每一种 GC 机制的出现都标志着对对延迟控制的进一步优化。
CMS
CMS(Concurrent Mark Sweep)垃圾收集器是第一个关注 GC 停顿时间(STW 的时间)的垃圾收集器。
MS 就是 mark-sweep 的缩写,即标记-清除算法。
CMS 垃圾收集器之所以能够实现对 GC 停顿时间的控制,其本质来源于对「可达性分析算法」的改进,即三色标记算法。在 CMS 出现之前,它们在进行垃圾回收的时候都需要 Stop the World,无法实现垃圾回收线程与用户线程的并发执行。而 CMS 将整个回收过程分为了四步,在最耗时的标记和清除阶段都可以跟用户线程并行执行,这也是为什么它延迟低的原因。
G1GC 介绍
G1GC 最大的特征是非常重视实时性,G1GC 的实时性是软实时性,即处理多用于稍微超出几次最后期限也没什么问题的系统中,例如网络银行系统。用户总会期待所有的交易都能完美地处理好,但是稍微超出几次最后期限,比如交易完成界面的展示慢了一些,应该也不会构成致命的问题。
为了实现软实时性,它具备以下两个功能。
1.设置期望暂停时间(最后期限)
2.可预测性
G1 如何实现可预测性
G1 会分步并行进行空间回收。G1 通过跟踪先前应用行为和垃圾收集暂停的信息,建立相关成本模型,从而实现可预测性。它利用这些信息来确定暂停期间的工作量。
分区
G1 的老年代和年轻代不再是一块连续的空间,整个堆被划分成若干个大小相同的 Region,也就是区。
Region 的类型有 Eden、Survivor、Old、Humongous 四种,而且每个 Region 都可以单独进行管理。
Humongous 是用来存放大对象的,如果一个对象的大小大于一个 Region 的 50%(默认值),那么我们就认为这个对象是一个大对象。为了防止大对象的频繁拷贝,我们可以将大对象直接放到 Humongous 中。
回收的实际过程
G1 回收的实际过程可以概括为一个循环,以下的官方文档的介绍。
On a high level, the G1 collector alternates between two phases. The young-only phase contains garbage collections that fill up the currently available memory with objects in the old generation gradually. The space-reclamation phase is where G1 reclaims space in the old generation incrementally, in addition to handling the young generation. Then the cycle restarts with a young-only phase.
翻译:在高层次上,G1收集器在两个阶段之间交替进行。young only 阶段包含了垃圾收集,这些收集逐渐用老年代中的对象填满当前可用的内存。space-reclamation (空间回收)阶段是G1在处理年轻代的同时,逐步回收老年代的空间。然后,周期以young only 阶段重新开始。
怎么理解这段话?
1.G1收集器的两个主要阶段:
- 仅年轻代阶段(Young-only phase)
- 空间回收阶段(Space-reclamation phase)
2.仅年轻代阶段(Young-only phase):
- 这个阶段主要进行年轻代的垃圾回收(Minor GC)。
- 存活对象可能会被提升到Survivor 区或老年代。
- “逐渐用老年代对象填满当前可用内存”的意思是:
- 新生代 GC 过程中,部分长生命周期的对象晋升到老年代。
- 经过多次 Minor GC,老年代会逐渐被这些晋升对象占据。
- “当前可用内存”指的是整个堆(Heap)中分配给老年代的部分。
3.空间回收阶段(Space-reclamation phase):
- 这个阶段进行Mixed GC(混合回收),同时清理年轻代和部分老年代。
- 目标是回收老年代中的垃圾对象,避免其占满整个堆。
- 由于 G1 采用Region(分区)管理,可以增量地清理老年代,不必一次性 STW 整个老年代。
- 这种增量老年代回收机制是 G1 相较于 CMS 的一大优势。
4.循环过程:
- G1 在 Young-only Phase 和 Space-reclamation Phase 之间交替运行。
- 在年轻代的对象逐渐晋升到老年代后,G1 进入空间回收阶段,进行Mixed GC来回收老年代。
- 这一过程不断循环,确保垃圾回收既高效又低延迟。
Young Only 介绍
- 这个阶段主要进行 Minor GC(年轻代回收),存活对象晋升到老年代。
- 当老年代使用率达到InitiatingHeapOccupancyPercent (IHOP)阈值(称为"初始堆占用阈值"),G1 会启动并发标记周期(Concurrent Marking Cycle),进入空间回收阶段(Mixed GC 阶段)的准备过程。
- 这一转换的关键点是"并发开始"(Concurrent Start)GC,它是 Young GC但额外启动了并发标记。
三个阶段的介绍:
1.并发开始(Concurrent Start):
- 既执行普通的年轻代回收(Young GC),又启动老年代的并发标记。
- 并发标记(Concurrent Marking)的作用是:
- 确定老年代中哪些对象是存活的,为后续的Mixed GC 选择要清理的 Region。
- 在并发标记完成之前,普通的 Young GC 仍然会继续发生。
- 标记阶段的结束伴随着STW 的 重新标记(Remark) 和 清理(Cleanup) 阶段,确保准确性。
- 这是从 Young-only 阶段向 Mixed GC 阶段转换的关键步骤。
2.重新标记(Remark):
- 用于完成并发标记的最终校正,需要Stop-The-World(STW)暂停
- 主要作用:
- 确保在最终标记时,所有存活对象的信息是一致的(避免遗漏存活对象)。
- 计算Region 存活率,预估回收成本,预测 Mixed GC 的影响。
- 这一步暂停时间较短,因为 G1 采用了SATB(Snapshot-At-The-Beginning)作为可达性分析的基础,减少了需要重新扫描的对象数量。
3.清理(Cleanup):
- 决定是否正式进入 Mixed GC(空间回收阶段)。
- 关键点:
- 如果老年代的存活对象太多,G1 可能不会进入 Mixed GC,而是继续 Young GC。
- 如果老年代垃圾足够多,G1 进入 Mixed GC,并在下一次 Young GC 时,执行"Prepare Mixed"。
- "Prepare Mixed" 这一步会标记哪些老年代的 Region 将参与下一次 GC,确保混合回收的效率。
4.总结:仅年轻代阶段(Young-only Phase)主要进行 Minor GC(Young GC),不断将存活对象晋升到老年代。当老年代占用率超过IHOP阈值时,G1 触发并发开始 GC(Concurrent Start GC),启动并发标记(Concurrent Marking),用于检测老年代中的存活对象。并发标记完成后,G1 进入STW 的重新标记(Remark)和清理(Cleanup)阶段,最终决定是否进入Mixed GC(空间回收阶段)。如果老年代回收效率足够高,G1 进入Mixed GC,否则继续 Young GC。
Space-reclamation 介绍
混合回收(Mixed Collections, Mixed GC):这个阶段,G1不仅清理年轻代,还会增量回收部分老年代,以防止老年代占用率过高导致 Full GC。
这里的关键点是“混合”:
1.继续执行年轻代 GC(Minor GC)。
2.选取部分老年代 Region进行存活对象疏散(Evacuation),将存活对象复制到其他 Region,然后回收已清理的 Region。这样,G1避免了 CMS 那种碎片化问题,确保老年代的可用空间更整齐。
具体流程:
1.Mixed GC 如何进行:
- G1 会根据“老年代回收效率”选择适合清理的老年代 Region,并按照回收性价比排序。
- 每次 Mixed GC 仅回收部分老年代 Region,并不会一次性清理整个老年代,以减少 STW(Stop-The-World)时间。
- 存活对象会被疏散(Evacuate)到 Survivor 区、老年代,甚至 Humongous 区(超大对象区),然后回收腾空的 Region。
- 这个过程可以并发执行,降低应用程序停顿时间。
2.结束条件:
- G1 会持续执行 Mixed GC,直到“继续清理老年代”变得不值得。
- 主要判断标准:
- 回收的内存空间不足以抵消 GC 的成本。
- 通过参数G1MixedGCLiveThresholdPercent控制老年代存活率的阈值,当存活率过高时,不再清理这些 Region。
- G1MixedGCCountTarget控制一次标记周期内 Mixed GC 的最大次数,超过后就结束。
3.循环重启:
空间回收阶段(Mixed GC)结束后,G1 重新进入 Young-only 阶段。
这个过程不断循环:Young-only Phase(仅年轻代阶段) → Space-reclamation Phase(空间回收阶段) → Young-only Phase
如果 Mixed GC 不能及时回收足够的老年代空间,并且应用程序继续分配对象导致堆空间耗尽,G1 会触发 Full GC(STW 进行全堆压缩整理)。Full GC 代价极高,一般是 G1 最不希望发生的情况。
4.总结:
空间回收阶段(Space-reclamation Phase)主要通过Mixed GC(混合回收)逐步回收老年代。G1 选择部分老年代 Region,将存活对象疏散到其他区域后释放这些 Region,从而避免碎片化。Mixed GC 会持续进行,直到回收效率下降到设定阈值,之后进入新的 Young-only 阶段。如果在这个过程中应用程序分配过快,G1 无法及时回收足够的空间,就会触发代价极高的Full GC。
G1 中的 GC 类型
G1 GC中涉及的几种GC包括:
1.Young GC(新生代回收):
Young GC主要负责回收新生代中的对象。新生代包含新创建的对象,这些对象更有可能在短时间内变成垃圾。Young GC执行过程相对较快,因为它只涉及新生代中对象的扫描和回收。在Young GC过程中,Eden区和Survivor区的存活对象会被复制到另一个Survivor区或者晋升到老年代。这个过程是Stop-The-World(STW)的,意味着在回收过程中,应用程序的所有线程都会被暂停。
年轻代GC会选择所有的年轻代区域加入回收集合中,但是为了满足用户停顿时间的配置,在每次GC后会调整这个最大年轻代区域的数量,每次回收的区域数量可能是变化的,换言之,young 区的大小是会动态调整的。
2.Mixed GC(混合回收):
Mixed GC是G1收集器特有的回收策略,它不仅回收新生代中的所有Region,还会回收部分老年代中的Region。这种策略的目标是在保证停顿时间不超过预期的情况下,尽可能地回收更多的垃圾对象。在Mixed GC过程中,首先会进行全局并发标记(global concurrent marking),这个过程是并发的,与应用程序线程同时执行,用于标记出所有存活的对象。然后,在回收阶段,G1会根据标记结果选择收益较高的部分老年代Region和新生代Region一起进行回收。这个选择过程是基于对Region中垃圾对象的数量和回收价值的评估。
3.Full GC:
Full GC是指对整个Java堆(包括年轻代、老年代和元空间)的垃圾回收。在G1中,通常尽量避免Full GC的发生,因为Full GC会导致较长时间的停顿。G1通过Mixed GC来回收部分老年代的Region,以减少Full GC的需要。但如果Mixed GC无法跟上内存分配的速度,导致老年代空间不足,或者在某些特殊情况下,比如巨型对象分配失败时,就会触发Full GC。G1中的Full GC使用的是Serial Old GC的代码,这意味着它会暂停所有应用线程,并执行效率相对较慢的单线程垃圾回收。它是原地(in-place)进行的,意味着在同一内存空间内移动和压缩对象。
总结:G1 GC通过Young GC和Mixed GC来优化垃圾回收性能,减少停顿时间,而Full GC是作为最后的手段,在必要时对整个堆进行回收。
与 CMS 进行对比
CMS垃圾回收器是第一个并发垃圾回收器。像其他算法一样,CMS使用多个线程来执行回收操作。CMS垃圾回收器自JDK 11被正式废弃,而且不鼓励在JDK 8中使用。从实际的角度来看,由于CMS使用的是标记-清除算法,导致它不能在后台处理过程中压缩堆。如果堆变得碎片化,那么CMS必须停止所有的应用程序线程并压缩堆,也就是执行标记-整理算法,这就违背了使用并发垃圾回收器的初衷。因为这个原因,并且 G1 GC 具备自动整理堆内存的能力,减少了碎片问题,因此官方推荐使用 G1 GC 替代 CMS。
性能提升
整体而言 JDK11 优于 JDK8,G1 优于 CMS。在两个 JDK 版本默认状态下(JDK11 + G1 V.S JDK8 + CMS),JDK11 max-jOPS 分数提升 17%,critical-jOPS 分数提升 105%
GC |
运行 SPECJbb2015 性能分析 |
|||
YGC平均暂停时间 |
吞吐量 |
max-jOPS (纯吞吐量) |
critical-jOPS (限制响应时间下的吞吐量) |
|
JDK8+CMS(基线数据) |
311ms |
92.7% |
15706 |
3898 |
JDK8+G1 |
187ms |
94.7% |
17098 |
5338 |
JDK11+CMS |
274ms |
94.2% |
15905 |
4821 |
JDK11+G1 |
177ms (↓43%) |
94.7%(↑2pt) |
18376 (↑17%) |
7980(↑105%) |
数据来自:AArch64版JDK8/11性能分析
内存使用率增加的复盘
内存分配原理
物理内存指的是PSS或RSS, 包括堆内的物理内存和堆外的, 小于等于预留的内存. 物理内存的分配方式通常是按需分配, 也就是只有写入虚拟内存的时候,内核才进行物理内存分配 (又叫touch)。读取虚拟内存或者预留 (mmap) 虚拟内存, 进程当时都不会产生物理内存。
换言之,通过JVM的参数-Xmx和-Xms可以设置JVM的堆大小,但是此时操作系统分配的只是虚拟内存,只有JVM真正要使用该内存时,才会被分配物理内存。
内存分配过程
1.对象首先会先分配在年轻代,因为之前分配的只是虚拟内存,所以每次新建对象都需要操作系统来先分配物理内存,只有等第一次新生代GC后,该被分配的内存空间都已经分配了,之后分配对象的速度才会加快。
2.那么老年代也是同理,老年代的空间何时真正使用,自然是对象需要晋升到老年代时,所以新生代GC的时候,对象要从新生代晋升到老年代,操作系统也需要为老年代先分配物理内存。
为什么升级后内存占用率提升
不同分区策略导致的
G1老区占用比CMS多, 原因是
1.CMS 采用的是 按地址顺序分配老年代内存,并且固定老年代的回收阈值。在低负载下,老年代内存的某些区域可能长时间不会被触及,造成碎片化,直到内存压力较高时才开始回收。
2.G1 GC使用Region-based 分配策略,这种策略采用了启发式算法,尝试尽量填满堆内存,避免老年代有空闲区域而造成内存浪费。因此,G1 的区域分配比 CMS 更加灵活,它会自动调整阈值并较快touch所有堆内存。
记忆集的占用
CMS 中有一个结构叫 card table,卡表,它是一种记录老年代对新生代引用的机制,卡表是为了解决了在新生代垃圾收集时,如何快速找到老年代中引用新生代对象的问题。它的工作原理:将老年代内存空间划分为固定大小的卡页。使用一个字节数组(卡表)来标记每个卡页的状态。如果一个卡页中的对象可能引用了新生代对象,该卡页就被标记为"脏"。在进行新生代垃圾收集时,只需要扫描被标记为脏的卡页,而不是整个老年代。
像 G1 这种分区回收算法,有些 Region 可能被选入 CSet,有些则不会。所以,我们需要知道当一个 Region 需要被回收时,有哪些其他的 Region 引用了自己。相应地,为了加快定位速度,分区回收算法为每个 Region 都引入了记录集(Remembered Set,RSet),每个 Region 都有自己的专属 RSet。和 Card table 不同的是,RSet 记录谁引用了我,这种记录集被人们称为 point-in 型的,而 Card table 则记录我引用了谁,这种记录集被称为 point-out 型。
1.不需要记录的引用:
- 同一个Region内的引用:因为它们都在同一个区域内,所以不需要在RSet中记录。
- 年轻代Region到其他Region的引用:在垃圾回收时,年轻代的所有Region都会被清理,这意味着所有年轻代对象都会被检查,因此不需要在RSet中记录这些引用。
- CSet集合的Region到其他Region的引用:CSet是即将被回收的Region集合,这些Region中的对象也会被完全检查,所以同样不需要记录。
2.需要记录的引用:
- 非CSet老年代Region到年轻代Region的引用:这些引用需要被记录,以便在回收年轻代时知道哪些老年代对象正在引用年轻代对象。
- 非CSet老年代Region到CSet老年代Region的引用:这些引用也需要被记录,以便在回收CSet中的老年代Region时,知道哪些其他老年代对象还在引用它们。
3.记录引用的方式:通过写屏障(Write Barrier)机制,当对象A在Region1中引用对象B在Region2时,这个引用信息(称为“dirty card”)会被添加到Region2的RSet中。在 G1 中,我们把这种 card 称为 dirty card。业务线程也不是直接将 dirty card 放到 RSet 中的。而是在业务线程中引入一个叫做 dirty card queue(DCQ)的队列,在写屏障中,业务线程只需要将 dirty card 放入 DCQ 中。接下来, G1 GC 中的 Refine 线程,会从 DCQ 中找到这种 dirty card,然后再去做更精细的检查,只有确实不属于上面所描述的三种情况的跨区引用,才真正放到专属 RSet 中去。
值得一提的是,Java 一直在尝试优化记忆集的占用,下图是 P99 CONF-G1:To Infinity and Beyond 的一个对比,在 16G 的堆内存情况下,高版本的的 JDK 的记忆集大小有了明显的改善。
总结
1.由于设置了 12G 的堆内存,且 G1 修改了内存管理方式,导致 G1 能够占用满这 12G 的堆内存,而 CMS 由于有固定old区回收阈值, 如果低压力下, old区末尾的部分内存几乎永远不会被用到,因此即使分配了 12G 内存也不会真正占用 12G 内存。
2.JDK11 下的 G1 的记忆集占用了较大的空间。
因此,12g 堆内存+2.25g记忆集+700m 的全局内存+非堆内存 ≈ 16g,这也是为什么最后能升到 95% 以上的原因。
几种解决方案
启动时 touch
如果想要避免内存逐渐增加并导致突发的延迟,JVM 提供了-XX:+AlwaysPreTouch 参数。该参数能够在服务启动时预先将虚拟内存映射为物理内存,避免后续因按需分配物理内存而导致的延迟。这样可以提高程序在运行时的内存分配效率,避免由于内存分配引起的卡顿或停顿。但它的缺点是会显著增加 JVM 启动时间,毕竟把分配物理内存的事提前放到JVM进程启动时做了,尤其是在内存较大的情况下,启动时间可能会大幅增加。
缩小堆内存
虽然在 G1 下老年代内存使用较高,但通常不会引发 OOM(内存溢出)风险,因为即使在长时间运行中,G1 也会调整回收策略以避免内存溢出。如果内存使用率已经达到警戒线,建议适当缩小堆大小。通常认为在 JDK11中,对于许多 Java 应用而言,堆内存占总内存的一半是比较合适的选择。
G1GC参数调优
CMS切换成G1的参数替换
CMS原参数 |
G1替换参数 (加粗为必改, 否则选改) |
解释 |
-Xms10g -Xmx10g |
-Xms10g -Xmx10g 或 -Xms9500m -Xmx9500m |
G1堆外内存稍高, 且使用region-based的管理算法, 导致footprint稍高, 为了不出现意外容器OOM, 适当降低5-8%的堆大小 比如buy2本来内存已经到82%了, 再高就要破90%了, 需要调小以应对风险 注: 内存水线是视业务行为而定的, 比如ump2不用调堆, 也可以保持原来的水线 注2: 这个幅度调小堆一般不会导致Java OOM, 因为G1对Survivor区的利用率比CMS高不少, 碎片化风险也比CMS低 |
-Xmn6g |
-XX:MaxNewSize=6g |
把-Xmn替换成-XX:MaxNewSize即可, 数字不变 也可以省略这个参数, 因为默认MaxNewSize就是60%, 所以相当于10g x 60% = 6g, 但是必须要记得把Xmn删掉 总之G1需要灵活调整Young区, 所以G1不要设置Xmn |
-XX:G1HeapRegionSize=32m |
G1跟CMS的一个巨大差别就是他用region-based的管理算法, 超过region大小的一半会当成"大对象", 大对象太多了有损性能,原因是 G1 对于大对象的处理非常消极,基本上不会怎么处理 |
|
-XX:+CMSScavengeBeforeRemark -XX:+UseConcMarkSweepGC -XX:CMSMaxAbortablePrecleanTime=5000 -XX:+CMSClassUnloadingEnabled |
-XX:+UseG1GC |
CMS相关参数, 不需要 |
-XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly |
-XX:InitiatingHeapOccupancyPercent=40 -XX:-G1UseAdaptiveIHOP |
控制Old区大小, 比如原来CMS大约在 (0.8*(10g-6g)=3277m) 触发Old区GC, G1配置成在 (0.4*9500=3800m) 触发 因为G1会把大对象也往老区放, 所以我们会倾向于把这个设置得比CMS高一点 |
-XX:G1HeapWastePercent=2 |
G1默认会接受5%的"浪费", 把他减少到2%, 可以大约挤出200-300m的空间, 当然这个是有代价的, G1回收成本会变高, 视情况加 |
推荐参数介绍
let JVM_HEAP = "-Xms8g -Xmx8g -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m -XX:ReservedCodeCacheSize=512m -XX:MaxDirectMemorySize=512m"let JVM_GC = "-XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:InitiatingHeapOccupancyPercent=40 -XX:-G1UseAdaptiveIHOP -XX:+ExplicitGCInvokesConcurrent -XX:ParallelGCThreads=8 -Dio.netty.tryReflectionSetAccessible=true --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED"
-XX:MaxGCPauseMillis
设置期望的最大GC停顿时间指标,G1收集器会尽力在这个时间内完成垃圾回收,以减少应用程序的停顿时间。默认值是200毫秒,调整这个参数可以影响GC的停顿时间
无论是 young GC 还是 mixed GC,都会回收全部的年轻代,因此这个参数如果设置得太短,会限制 young 区占用的 region数量,可能会导致G1跟不上垃圾产生的速度,最终退化成Full GC。
-XX:ReservedCodeCacheSize
作用:
- 存储编译后的本地代码:当 JVM 运行时,它会将频繁执行的 Java 字节码编译成本地机器代码以提高性能。这些编译后的代码被存储在代码缓存中。
- 影响性能:适当大小的代码缓存可以提高应用程序的性能,因为它允许更多的代码被编译和优化。
-XX:MaxDirectMemorySize
定义:MaxDirectMemorySize 指定了 JVM 可以分配的最大直接内存大小。
直接内存(Direct Memory):
- 这是在 Java 堆外分配的内存,不受 Java 堆大小限制。
- 主要用于 NIO(New I/O)操作,如 DirectByteBuffer。
- 可以减少垃圾回收的压力,因为它不在 Java 堆中。
默认值:如果不指定,默认值通常等于 -Xmx(最大堆大小)。
-XX:G1HeapRegionSize
设置每个Region的大小,G1的目标是根据最小的Java堆大小划分出约2048个这样的区域。这个值必须是2的幂,范围在1MB到32MB之间。默认情况下,这个值是堆内存的1/2000,这意味着G1收集器管理的最小堆内存应该是2GB以上,最大堆内存为64GB。调整这个参数可以影响G1的内存管理效率和垃圾回收的性能。
-XX:+ExplicitGCInvokesConcurrent
用于控制显式垃圾回收(Explicit GC)调用的行为。当这个参数被启用时,它会使得通过 System.gc() 或者其他形式的显式垃圾回收请求触发并发(concurrent)的垃圾回收操作,而不是完全停止(full stop-the-world)的应用程序线程进行垃圾回收。
-XX:ParallelGCThreads
1.定义:-XX:ParallelGCThreads 用于设置并行垃圾收集器的线程数。这些线程用于执行 Stop-The-World(STW)垃圾收集阶段。
2.默认值:
- 如果 CPU 核心数小于等于 8:ParallelGCThreads = CPU 核心数
- 如果 CPU 核心数大于 8:ParallelGCThreads = 8 + ((CPU 核心数 - 8) * 5/8)
-XX:G1NewSizePercent & -XX:G1MaxNewSizePercent
分别用于设置新生代最小和最大容量的百分比。这两个参数可以控制新生代的大小,影响垃圾收集的频率和性能。一般不推荐修改。
IHOP
The Initiating Heap Occupancy Percent (IHOP) is the threshold at which an Initial Mark collection is triggered and it is defined as a percentage of the old generation size.
G1 by default automatically determines an optimal IHOP by observing how long marking takes and how much memory is typically allocated in the old generation during marking cycles. This feature is called Adaptive IHOP. If this feature is active, then the option -XX:InitiatingHeapOccupancyPercent determines the initial value as a percentage of the size of the current old generation as long as there aren't enough observations to make a good prediction of the Initiating Heap Occupancy threshold. Turn off this behavior of G1 using the option-XX:-G1UseAdaptiveIHOP. In this case, the value of -XX:InitiatingHeapOccupancyPercent always determines this threshold.
Internally, Adaptive IHOP tries to set the Initiating Heap Occupancy so that the first mixed garbage collection of the space-reclamation phase starts when the old generation occupancy is at a current maximum old generation size minus the value of -XX:G1HeapReservePercent as the extra buffer.
翻译:初始堆占用百分比(IHOP)是触发初始标记收集的阈值,它被定义为老年代大小的百分比。
G1默认会通过观察标记过程所需的时间以及在标记周期期间通常在老年代中分配的内存量来自动确定最佳的IHOP。这个特性被称为自适应IHOP。如果这个特性是激活的,那么 -XX:InitiatingHeapOccupancyPercent 选项会决定初始值,作为当前老年代大小的百分比,这种情况会持续到有足够的观察数据来做出良好的初始堆占用阈值预测为止。使用 -XX:-G1UseAdaptiveIHOP 选项可以关闭G1的这种行为。在这种情况下,-XX:InitiatingHeapOccupancyPercent 的值将始终决定这个阈值。
在内部,自适应IHOP试图设置初始堆占用,使得空间回收阶段的第一次混合垃圾收集在老年代占用达到当前最大老年代大小减去 -XX:G1HeapReservePercent 值作为额外缓冲区时开始。
1.自适应IHOP:
- G1默认使用自适应IHOP机制。
- 它通过观察标记过程的时间和老年代内存分配情况来动态调整IHOP。
- 目的是找到最优的IHOP值,以平衡垃圾收集频率和效率。
2.-XX:InitiatingHeapOccupancyPercent:
- 这个参数设置IHOP的初始值。
- 在自适应IHOP有足够数据之前,这个值会被使用。
- 如果禁用自适应IHOP,这个值将始终决定IHOP阈值。
3.禁用自适应IHOP:
- 使用 -XX:-G1UseAdaptiveIHOP 可以关闭自适应IHOP。
- 关闭后,IHOP将固定为 -XX:InitiatingHeapOccupancyPercent 指定的值。
4.自适应IHOP的内部工作:
- 目标是在老年代占用达到特定水平时开始混合收集。
- 这个水平是:当前最大老年代大小 - G1HeapReservePercent。
- G1HeapReservePercent 作为一个额外的缓冲区。
-XX:InitiatingHeapOccupancyPercent
这个参数指定触发在老年代的内存空间达到一定百分比之后,启动并发标记。默认值是 45%,当然,这更进一步是为了触发 mixed GC,以此来回收老年代。如果一个应用老年代对象产生速度较快,可以尝试适当调小 IHOP。
1.并发标记的目的:
- 并发标记的主要目的是为后续的 Mixed GC 做准备,识别整个堆中的存活对象。
- Mixed GC 会同时收集年轻代和部分老年代。
2.G1 收集器的工作流程:
- 通常顺序是:若干次 Young GC → 并发标记 → Mixed GC
- 并发标记后,G1 可能会执行一系列 Mixed GC 来回收标记的区域。
-XX:-G1UseAdaptiveIHOP
UseAdaptiveIHOP 是 G1 垃圾收集器的一个功能,它根据堆内存晋升速率自动调节 IHOP(Initiating Heap Occupancy Percent)。IHOP 决定了触发并发标记周期的堆占用阈值。使用 -XX:-G1UseAdaptiveIHOP 参数可以禁用这个自动调节功能,此时 IHOP 值将固定为初始设置,通常由 -XX:InitiatingHeapOccupancyPercent 参数指定。这种做法类似于使用 -Xmn 参数固定年轻代大小。
关于是否应该禁用 UseAdaptiveIHOP,存在不同观点。有建议关闭此功能以防止频繁的并发 GC,而另一种观点认为除非 G1 的自动调节表现不佳,否则不应更改。
我的看法是,如果应用程序在默认设置下表现良好,就保持 UseAdaptiveIHOP 启用。只有在遇到明确的性能问题时,才考虑禁用它。在做出任何更改之前,最好先收集详细的 GC 日志进行分析,然后根据分析结果决定是否需要调整。需要注意的是,更改 IHOP 设置可能会影响 GC 的频率和停顿时间,因此调整后需要进行充分的测试和监控。
-XX:G1HeapWastePercent
1.参数定义:-XX:G1HeapWastePercent 设置了 G1 垃圾收集器在停止回收之前允许的堆内存浪费百分比。
2.默认值:默认值是 5%,意味着 G1 允许最多 5% 的堆内存被浪费(即未被使用)。
3.参数作用:
- 控制空间回收阶段(Space-reclamation phase)的结束时机。
- 影响混合收集(Mixed Collections)的次数。
4.工作原理:
- 在空间回收阶段,G1 执行混合收集,回收老年代和年轻代的区域。
- G1 会持续进行混合收集,直到堆内存中的自由空间(未使用空间)百分比达到 100% - G1HeapWastePercent。
- 一旦达到这个阈值,G1 认为继续回收的收益不大,就会停止当前的空间回收阶段。
5.调优考虑:
- 降低这个值会导致 G1 执行更多的混合收集,可能会增加总的 GC 时间,但可以得到更紧凑的堆。
- 提高这个值会减少混合收集的次数,可能会减少 GC 时间,但可能会留下更多未使用的空间。
6.使用场景:
- 对于内存敏感的应用,可以考虑降低这个值,以更充分地利用堆空间。
- 对于更关注吞吐量的应用,可以考虑略微提高这个值,以减少 GC 的频率。
netty 优化
-Dio.netty.tryReflectionSetAccessible=true --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED
必须设置,解决堆外内存增加 600M 的问题。
参考资料:
1.G1GC官方介绍:Garbage-First Garbage Collectorhttps://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-g1-garbage-collector1.html#GUID-ED3AB6D3-FD9B-4447-9EDF-983ED2F7A5732.G1GC官方调优推荐:Garbage-First Garbage Collector Tuninghttps://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-garbage-collector-tuning.html3.深入理解Java虚拟机(第3版):https://book.douban.com/subject/34907497/4.Java 性能权威指南:https://www.ituring.com.cn/book/2774
5.P99 CONF-G1:To Infinity and Beyond:https://www.p99conf.io/session/g1-to-infinity-and-beyond/
基于缓存实现应用提速
随着业务发展,承载业务的应用将会面临更大的流量压力,如何降低系统的响应时间,提升系统性能成为了每一位开发人员需要面临的问题,使用缓存是首选方案。本方案介绍如何运用云数据库Redis版构建缓存为应用提速。