JVM调优和G1
- 背景和价值
- 1 回收算法选型 CPU核数,内存大小(4g用CMS,8G或者以上用G1)
- 2 堆内存占比。 8G容器5G堆
- 3 新时代老年代占比。默认新时代占60%,如果应用有大量短期对象,可以适当增加年轻代的大小。
- 4 -Xms 和 -Xmx` 设置为相同的值,以避免堆内存动态调整带来的性能开销。
- 5 停顿时间。-XX:MaxGCPauseMillis 默认200ms,对响应时间敏感的应用(如 Web 应用、金融交易系统)可设置更低的值
- 6 Region大小设置。较大的 Region 可以减少内存碎片,但可能会增加垃圾回收的粒度。
- 7 设置 Mixed GC 的阈值,如果老年代占用堆内存的比例超过该阈值,G1 会启动 Mixed GC。如果应用有大量长期存活对象,可以适当降低该值。
- 8 设置 Survivor 区比例。如果应用有大量短期对象,可以适当增加 Survivor 区的大小。
- -XX:InitiatingHeapOccupancyPercent
- -XX:+UseStringDeduplication
- G1垃圾回收生命周期
- 参考资料
背景和价值
1 回收算法选型 CPU核数,内存大小(4g用CMS,8G或者以上用G1)
2 堆内存占比。 8G容器5G堆
8G的物理内存,最多堆内存设置多少合适?
JVM进程占用的内存:heap+metaspace+ 额外的几百M(代码多的程序,甚至上G的空间),所以8G物理内存,分配5G最大堆内存比较合适
案例:生产环境8G内存,JDK为1.8,最大堆设置6G,元数据空间设置为256M。 生产运行一段时间,运维监控发现物理内存用到90%!!
通过top查看,JAVA进程占7G内存,远大于6G+256M+线程栈的几十M的内存。 那么多出的700M内存花到哪里呢?
通过arthas dashboard分析
Memory used total max usage GC
heap 2210M 6092M 6092M 36.28% gc.parnew.count 2078
par_eden_space 204M 409M 409M 49.83% gc.parnew.time(ms) 115760
par_survivor_space 3M 51M 51M 7.28% gc.concurrentmarksweep.count 24
cms_old_gen 2002M 5632M 5632M 35.56% gc.concurrentmarksweep.time(ms) 101565
nonheap 287M 299M -1 95.96%
code_cache 100M 101M 240M 41.77%
metaspace 167M 177M -1 94.57%
- JVM本身需要的内存,包括其加载的第三方库以及这些库分配的内存。内存映射文件,包括JVM加载的一些JAR和第三方库,以及程序内部用到的。上面 pmap 输出的内容里,有一些静态文件所占用的大小不在Java的heap里
- NIO的DirectBuffer是分配的native memory
- Native Memory包含Native Code,Native Stack。对应arthas的nonheap+code_cache。JIT(Just-In-Time (JIT) 编译器是运行时环境的一个组件,通过在运行时将字节码编译为本机机器代码来提高Java™ 应用程序的性能),JVM会将Class编译成native代码,这些内存也不会少,如果使用了Spring的AOP,CGLIB会生成更多的类,JIT的内存开销也会随之变大。一些JNI接口调用的native库也会分配一些内存,如果遇到JNI库的内存泄露,可以使用valgrind等内存泄露工具来检测
- 线程栈,每个线程都会有自己的栈空间,如果线程一多,这个的开销就很明显
- Metaspace:class文件元信息描述,编译后的代码数据,引用类型数据,类文件常量池(而字符串常量池 存在于堆中)
总结
JVM进程占用的内存:heap+metaspace+ 额外的几百M(代码多的程序,甚至上G的空间),所以8G物理内存,分配5G最大堆内存比较合适
什么是运行时常量池
运行时常量池
运行时常量池就是将编译后的类信息放入方法区中,也就是说它是方法区的一部分。
运行时常量池用来动态获取类信息,包括:class文件元信息描述、编译后的代码数据、引用类型数据、类文件常量池等。
运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中。每个class都有一个运行时常量池,类在解析之后将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
3 新时代老年代占比。默认新时代占60%,如果应用有大量短期对象,可以适当增加年轻代的大小。
4 -Xms 和 -Xmx` 设置为相同的值,以避免堆内存动态调整带来的性能开销。
5 停顿时间。-XX:MaxGCPauseMillis 默认200ms,对响应时间敏感的应用(如 Web 应用、金融交易系统)可设置更低的值
6 Region大小设置。较大的 Region 可以减少内存碎片,但可能会增加垃圾回收的粒度。
6.1 堆内存大小与 Region 大小的关系。如果堆内存较小,Region 大小应该相应减小;如果堆内存较大,Region 大小可以适当增大。
6.2 如果应用程序中经常会产生大对象(如大于 Region 大小一半的对象),需要确保 Region 大小能够容纳这些大对象。否则,G1 可能需要使用多个 Region 来存放一个大对象,这会增加内存管理的复杂性和垃圾回收的难度。
7 设置 Mixed GC 的阈值,如果老年代占用堆内存的比例超过该阈值,G1 会启动 Mixed GC。如果应用有大量长期存活对象,可以适当降低该值。
Mixed GC 负责回收老年代 Region
Mixed GC 负责回收老年代 Region,关键参数:
-XX:G1MixedGCLiveThresholdPercent
仅回收存活对象比例低于此值的 Region(默认 85%):-XX:G1MixedGCLiveThresholdPercent=80
-XX:G1HeapWastePercent
允许的堆浪费比例,达到该值会触发 Mixed GC(默认 5%):-XX:G1HeapWastePercent=10
-XX:G1MixedGCCountTarget
单次 Mixed GC 的回收轮次(默认 8),增大此值可减少单次停顿时间:-XX:G1MixedGCCountTarget=16
8 设置 Survivor 区比例。如果应用有大量短期对象,可以适当增加 Survivor 区的大小。
-XX:InitiatingHeapOccupancyPercent
触发并发标记周期的堆占用阈值(默认 45%)。如果 Full GC 频繁,可适当降低该值:
避免Full GC,因为G1的设计目标之一就是减少Full GC的发生。这时候需要确保堆足够大,或者调整InitiatingHeapOccupancyPercent,避免过早触发并发周期。
例
-XX:InitiatingHeapOccupancyPercent=35
- 当整个堆占用超过某个百分比时,就会触发并发GC周期,这个百分比默认是45%,我的理解来说,如果你的项目没有大的cpu负载压力,可以适当降低这个值,带来的好处就是提前开始Concurrent Marking Cycle Phases ,进一步来说,回收 年轻代 and 老年代 也会提前开始,这样有利于防止年轻代晋升老年代失败(老年代容量不足)而触发Full GC。
经过观察发现如果这个数值设定过大会导致JVM无法启动并发标记,直接进行FullGC处理。G1的FullGC是单线程,一个22G的对GC完成需要8S的时间,所以这个值在调优的时候写的45%
-XX:+UseStringDeduplication
最近的一项研究表明,应用程序 13.5% 的内存中存在重复的字符串。当你传递 “-XX:+UseStringDeduplication” 参数时,G1 垃圾收集器(G1 GC)提供了一种消除重复字符串的选项。
以上大部分参数要根据实际业务压测。
G1垃圾回收生命周期
Java G1(Garbage-First)垃圾回收器的生命周期包含多个阶段,按触发顺序和执行流程可分为以下核心阶段:
1. 年轻代回收(Young GC)
- 触发条件:Eden 区内存不足时(常规的年轻代回收)。
- 执行过程:
- STW 暂停:暂停所有应用线程(Stop-The-World)。
- 复制存活对象:将 Eden 区和 Survivor 区的存活对象复制到新的 Survivor 区或直接晋升到老年代。
- 调整 Region 归属:回收后的 Eden 区变为空闲 Region。
- 特点:仅处理年轻代 Region,速度快但频率高。
2. 并发标记周期(Concurrent Marking Cycle)
- 触发条件:老年代占用达到 IHOP 阈值(默认 45%,通过
-XX:InitiatingHeapOccupancyPercent
调整)。 - 分阶段执行:
- (1) 初始标记(Initial Mark)
- STW 暂停:标记从 GC Roots 直接可达的对象(与 Young GC 同步触发)。
- (2) 根区域扫描(Root Region Scanning)
- 并发执行:扫描 Survivor 区(根区域)中引用到老年代的对象。
- (3) 并发标记(Concurrent Marking)
- 并发执行:遍历堆,标记所有存活对象(与应用线程并行)。
- (4) 最终标记(Remark)
- STW 暂停:处理并发标记期间的变化(SATB 算法修正)。
- (5) 清理(Cleanup)
- STW 暂停:统计每个 Region 的存活对象占比,为混合回收做准备。
- (1) 初始标记(Initial Mark)
- 目标:识别老年代中可回收的 Region(标记垃圾占比高的 Region)。
3. 混合回收(Mixed GC)
- 触发条件:并发标记周期完成后(清理阶段结束)。
- 执行过程:
- STW 暂停:暂停应用线程。
- 混合回收:同时回收年轻代 Region(Eden/Survivor)和标记出的老年代 Region。
- 多轮执行:通过
-XX:G1MixedGCCountTarget
控制单次混合回收的轮次(默认 8 轮)。
- 特点:兼顾年轻代和老年代回收,减少 Full GC 风险。
4. 完全垃圾回收(Full GC)
- 触发条件(需尽量避免):
- 并发模式失败:并发标记未完成时堆已满。
- 晋升失败:存活对象无法晋升到老年代。
- 大对象分配失败:Humongous 对象找不到连续 Region。
- 执行过程:
- 单线程 STW 暂停:串行回收整个堆(类似 Serial GC),性能极差。
- 优化方向:通过调整
IHOP
、G1ReservePercent
等参数避免触发。
生命周期流程图
Young GC(常规) → [IHOP 阈值触发] → 并发标记周期 → Mixed GC → Young GC(循环)
↓
Full GC(异常兜底)
关键注意事项
- 并发标记周期是混合回收的前置条件,只有完成标记才能确定老年代可回收 Region。
- Mixed GC 是增量回收:通过多轮回收分摊停顿时间。
- Full GC 是最后的兜底机制,需通过监控(如 GC 日志)提前规避。
通过调整 -XX:MaxGCPauseMillis
(目标停顿时间)和 -XX:G1MixedGCLiveThresholdPercent
(回收阈值),可优化混合回收的效率和频率。
参考资料
https://blog.csdn.net/guyue35/article/details/124416121
arthas dashboard命令
https://zhuanlan.zhihu.com/p/383283993