news 2026/9/16 3:36:25

Java GC优化实战:从Full GC排查到JVM参数调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java GC优化实战:从Full GC排查到JVM参数调优

1. 从一个“卡死”的下午说起:我为什么开始认真做GC优化

先说个亲身经历。有一年我在负责一个订单履约系统,平时跑得好好的,结果一到午高峰就频繁报警:接口P99延迟从50ms直接飙到800ms,CPU居高不下,老年代GC每一轮都长达1.5秒。更离谱的是,局部Full GC把整个应用卡到健康检查超时,容器被反复重启,用户侧就是“转圈圈”、“按钮没反应”。我一开始怀疑是数据库慢查询,结果DBA告诉我数据库CPU才20%,锁等待也很低。折腾了两个小时,才想起来去看GC日志——好家伙,定时任务一次性扫了几十万条数据,把老年代直接塞爆了,CMS根本来不及回收,只能Full GC兜底。

那次事故之后,我才意识到一个事实:大部分应用系统的问题,表面上是“慢”,根子里是——内存管理失控,GC被拖垮了。从那以后,我养成一个习惯,凡是上线前的压测评审,必看GC曲线;凡是线上有莫名其妙的长尾延迟,第一件事就是拉GC日志。GC优化不是“调几个JVM参数再说”,而是一套从观测、分析、定位到参数调整的系统工程。

这篇文章我不打算上高大上的理论课,而是从一次典型的线上抖动排查入手,把手头积累的GC问题排查方法、参数调整思路、工具选型经验全部捋一遍。适合谁看呢?正在做Java服务性能调优的开发者、被线上Full GC折磨过的运维同学,以及那些刚接手系统、想建立一套“GC问题排查SOP”的后端工程师。

2. GC不是玄学:先把基础模型和关键术语白话一遍

很多人一看到GC(Garbage Collection,垃圾回收)就头大,因为名词太多,什么Young GC、Old GC、Full GC、Mixed GC,还有Parallel、CMS、G1、ZGC。网上资料天花乱坠,但落到自己的项目里,其实没那么复杂。先理清几个最基础的概念,后面讲的排查思路才接得上。

2.1 堆内存划分:年轻代、老年代到底在干什么

JVM的堆内存基本上可以分成两大块:年轻代(Young Generation)和老年代(Old Generation)。年轻代又细分为Eden区、两个Survivor区(S0/S1)。新创建的对象先在Eden区“孵着”,Eden满了就触发Minor GC(也叫Young GC),活下来的对象挪到Survivor区来回倒腾,达到年龄阈值之后晋升到老年代。

为什么要分这么细?因为绝大多数对象“朝生夕死”,比如一个HTTP请求里创建的中间对象,通常在方法栈帧退出后就没用了。把这种短命对象隔离在年轻代,GC时只需要扫描一小块区域,成本极低。真正要小心的是那些“漏网之鱼”——本该死掉的对象被无意当中引用住,一直在Survivor区倒腾,最终晋升到老年代。老年代一多,回收成本就上去了,还会引发更严重的连锁反应。

老年代的GC策略直接决定了服务的稳定上限。以前的老项目喜欢用CMS(Concurrent Mark Sweep),它就是“尽量不停顿地标记清除”的代名词。但CMS有个著名的毛病:浮动垃圾多、内存碎片化严重,一旦发生Concurrent Mode Failure,就会退化成本地冻结所有业务线程的Full GC。这块我后面结合故障案例细讲。

2.2 每次“卡顿”的本质:Stop The World

不管什么回收器,有一件事是绕不开的——STW(Stop The World,全局暂停)。GC本身要移动对象、压缩内存、清理引用,而这些操作会临时修改堆内存里的对象状态,如果业务线程还在同时读写这些对象,内存直接乱套。所以JVM只能在某个时刻暂停所有业务线程,让GC线程把活干完再放行。

年轻代GC因为区域小,STW时间通常只有几毫秒到几十毫秒,看起来没那么夸张。但老年代GC、Full GC动辄几百毫秒甚至秒级,这个卡顿对上线上高并发接口就是灾难。所以“GC优化”本质上是在做一件事:尽量减少STW的频次和单次时长,让GC停顿对业务的影响变得可控、可接受

2.3 不同回收器怎么选:G1、CMS、ZGC的适用场景

这么多年用下来,我的选择逻辑很简单:

  • 如果堆内存4G以内、追求最大吞吐量,用Parallel GC(JDK8默认)最简单粗暴。
  • 如果堆内存4G以上、停顿敏感,以前常用CMS,但JDK9之后官方已经弃用,现在基本都迁移到G1。
  • 如果堆内存几十G、上百G,对停顿有极苛刻要求(比如几十毫秒以内),ZGC和Shenandoah是方向。

G1是现在的主流。它把堆分成一个个Region,背后维护一个全局的“优先级列表”,哪边垃圾最多、回收收益最大,就先回收哪边。所以G1叫“Garbage First”,翻译过来就是“垃圾优先”。它的设计目标是让停顿时间可控,比如你设置-XX:MaxGCPauseMillis=200,G1会尽量控制在200ms以内。但注意,这只是目标,不是绝对保证,很多参数调不好反而会适得其反。

2.4 高频出现的“GC(G1 Old, All)”到底在说什么

网络热搜里有个词“gc(g1 old,all)”,很多人第一次看到会懵。其实这是G1回收器的日志分类写法:

  • G1 Young (Normal):正常的年轻代GC,G1会把Eden区和部分Survivor区一起回收。
  • G1 Old (Mixed):混合回收,G1在回收年轻代的同时,把老年代里“垃圾占比最高”的那部分Region一起收掉,所以叫Mixed GC。
  • G1 Old (All)或者G1 Full:表示老年代全面回收,通常意味着G1的增量回收跟不上对象分配速度了,只能全部扫一遍,代价非常大。

我后来排查线上问题时,看到GC日志里出现了G1 Old (All),心里大概就有数了:不是堆太小,就是有大对象竖着进老年代,或者内存泄漏在持续制造存活对象。这些场景后面会逐个拆。

提示:日常看GC日志,遇到G1 Old (Mixed)不用太紧张,它只是G1正常的混合回收节奏;但如果你观察到G1 Old (All)出现频率很高,且每次耗时都在几百毫秒以上,就要认真查内存分配路径了。

3. 典型故障复盘:一次由“大查询”引发的Full GC风波

理论归理论,用代码层面的事故来佐证才有说服力。下面这个案例我是把多种场景浓缩到一起,但每一个环节都是我真实踩过的坑,很多公司并发稍微上来一点就会复现同样的问题。

3.1 事故现象与初步猜测

背景是一个库存服务,Deploy到两节点,每节点分配4G堆内存,用的JDK8默认Parallel GC。某天大促预热流量一上来,运维收到告警:接口超时率超过15%,老年代内存占用呈现“锯齿状”冲到顶,GC日志里Full GC出现频次高达每5分钟一次,单次耗时平均2.3秒。

我当时的第一反应是:“这不就是堆内存给小了嘛,扩容呗。”但仔细一想不对,这个服务平时压测2000QPS都能稳住,突然变了,肯定是有个别的“大头”进入内存了。于是赶紧上Arthas去看内存里的对象分布。

3.2 定位过程:Arthas + MAT 两步走

第一步,用Arthas的dashboard命令看堆内存整体画像,再用memory命令查各区域用量。当时看到的情况是:Eden区一直在疯狂增长,老年代却已经占到了3.4G,配额总量只有2.6G——因为Parallel GC的老年代默认是堆的一半。然后我执行了heapdump把堆快照拉下来,用MAT(Memory Analyzer)分析。

MAT打开快照之后,看HistogramLeak Suspects。果然,有个ProductSkuDTO类型的对象居然有上百万个实例,占掉了2.1G内存。顺藤摸瓜一看,是某个商品详情批量接口,在查询SKU列表时一次性select *把整个商品表的全字段都捞了出来,然后批量封账成DTO,再丢进一个被Spring Singleton持有的本地缓存Map里。这个Map还设置了“永不过期”,只在收到MQ消息时才清理。

问题链条一下就通透了:大批量请求触发大批量查询,大批量DTO进入缓存且没有淘汰策略,老年代直接被打爆;老年代GC跟不上分配速度,最终退化Full GC。而且因为每个节点都在做同样的事情,重启还会“热缓存重建”,把事故进一步放大。

3.3 修复动作:不是只改参数,而是先改代码

这种问题靠调GC参数是救不回来的,纯靠堆内存硬扛只会越扛越贵。我当时做了三件事:

  1. 在MQ消费端增加全量商品的版本号校验,SKU查询接口只缓存“高频热点商品”的概要信息,并且给缓存加了一个定时批量清理的淘汰策略,保证缓存长度不超过2万条。
  2. 把批量查询从“全字段全量”改成“字段裁剪”,产品只需要名称、图片、价格,就只查这三列,SQL侧省掉大量中间对象。
  3. JVM参数上也做了配合:换到G1回收器,-XX:MaxGCPauseMillis=200,同时把-XX:G1NewSizePercent-XX:G1MaxNewSizePercent做了限制,避免年轻代无限变大,导致老年代被挤压。

这样改完再压测,Full GC消失了,老年代稳定在1.2G左右,接口P99从800ms降到90ms。这里最大的教训是:GC优化要先从代码和架构层面找“内存黑洞”,不要一上来就改参数。参数只是兜底,代码不收敛,改什么都白搭。

3.4 一次事故带来的排查SOP

从那以后,我给自己定了一个固定动作:遇到线上GC问题,严格按以下顺序排查。

  1. 先看GC日志:统计Full GC频率、单次耗时、GC前后堆内存变化。
  2. 再看堆内存快照:用MAT或者JProfiler找大对象、排查泄漏。
  3. 然后看线程栈:用Arthas的thread -n 3看CPU最高的线程在干什么,大概率能抓到请求入口。
  4. 最后结合业务代码确认:是缓存设计问题、批量查询问题,还是本地缓存没有淘汰策略。
  5. 修复完上线前,用压测流量再观测一轮GC曲线。

这套SOP看起来很简单,但真能坚持下来的人不多。很多人第一步都没走完就急着调参,结果越调越乱。

4. GC日志在说什么:手把手教你读一遍关键输出

GC日志是排查GC问题的最基础输入。以前JDK8时代靠-XX:+PrintGCDetails -XX:+PrintGCDateStamps打印到文件里,现在JDK11+用-Xlog:gc*:file=/path/gc.log。关键是日志拿到手,你要看得懂那几行英文背后代表什么。

4.1 读一行Young GC日志

我拿一段典型的Parallel GC日志举个例:

2025-03-10T12:00:01.123+0800: 1234.567: [GC (Allocation Failure) [PSYoungGen: 1048576K->102400K(1228800K)] 1048576K->512000K(4096000K), 0.0212345 secs] [Times: user=0.03 sys=0.01, real=0.02 secs]

拆开来看:

  • 2025-03-10T12:00:01.123+0800:这是触发时间点。
  • 1234.567:JVM启动到当前时间的秒数。
  • GC (Allocation Failure):因为年轻代没有足够空间分配新对象,才触发这次GC。
  • PSYoungGen: 1048576K->102400K(1228800K):年轻代从1024M降到100M,总容量是1200M。
  • 1048576K->512000K(4096000K):整个堆内存从1024M降到500M,堆总量是4G。注意,这里堆总量从“满”降到一半,说明年轻代回收之后,有部分对象还“活着”被晋了老年代。
  • 0.0212345 secs:本次GC花了21ms左右,这个停顿还在正常范围。

读GC日志的关键不在于记住每个字段,而在于关注一个比值:GC前后整个堆的内存下降了多少。如果每次Young GC之后,堆内存几乎没降多少,说明Survivor区扛不住,对象频繁晋升老年代,这很可能触发后续的老年代GC。

4.2 读一行G1日志

G1的日志格式更复杂一点,但核心信息是“分Region回收”。比如:

2025-03-10T12:00:01.456+0800: 2345.678: [G1Ergonomics (Mixed GCs) continue, ...] [GC pause (G1 Evacuation Pause) (young), 0.0456789 secs] [Eden: 512.0M(512.0M)->0.0B(512.0M) Survivors: 16.0M->16.0M Heap: 2.5G(4.0G)->1.2G(4.0G)]

这个信息里,G1 Evacuation Pause (young)表示这是一次年轻代“疏散暂停”;Eden从512M清到0,堆内存从2.5G降到1.2G,耗时45ms。如果看到g1 G1 Old (All)之类的字样,就意味着是整堆级别的全量回收,行动级别就要提级了。

4.3 一张速查表:GC日志高频术语对照

日志关键词含义风险等级
Allocation Failure年轻代分配失败触发GC低,正常现象
Promotion Failure晋升老年代失败,通常是老年代空间不足
Concurrent Mode FailureCMS/G1并发标记赶不上对象分配速度
Full GC整堆回收,涉及老年代
G1 Evacuation Pause (young)G1年轻代疏散暂停
G1 Evacuation Pause (mixed)G1混合回收,包含部分老年代Region
G1 Old (All)G1老年代全面回收
System.gc()业务代码主动触发GC视上下文而定

这个速查表我平时就贴在工位旁边,看到日志关键词基本就能判断下一步该做什么。

5. 参数优化实操:从默认参数到“调好为止”的完整过程

很多人上来就问:“线上G1到底该配什么参数?”这个问题的正确答案是“看你的业务场景”。但作为经验参考,我可以给一套比较通用、比较稳的配置基线,并讲讲每个参数背后的原则。

5.1 一套比较稳的G1启动参数

假设每个实例堆内存8G,目标是接口P99 < 200ms,GC停顿尽量控制在100ms以内。我经常用的模版是:

-Xms8g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=2 -XX:G1HeapRegionSize=8m -XX:+AlwaysPreTouch -Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100m

说下每个参数的设计意图:

  • -Xms8g -Xmx8g:初始堆和最大堆相等,避免运行时动态扩容带来的额外开销。这个几乎是线上必备。
  • -XX:MaxGCPauseMillis=200:G1会以这个为停顿目标制定回收计划。注意别设太小,比如50ms以下,G1会为了凑目标拼命增加回收频率,反而降低吞吐量。
  • G1NewSizePercent=5G1MaxNewSizePercent=60:年轻代占比的上下限。默认情况下G1的年轻代大小是动态的,但是为了不让它“放飞自我”把老年代挤没了,我会给它一个明确边界。
  • MaxTenuringThreshold=15:晋升老年代的年龄阈值。G1默认是15,但实际动态调整时经常到不了15,这个参数看情况放宽。
  • G1HeapRegionSize=8m:Region大小。堆8G时默认Region大小是4M,可以调成8M减少Region数量,对大对象分配有好处。但一定要小于大对象阈值(1M),否则大对象分配会走另外的路径。
  • AlwaysPreTouch:启动时把堆内存全部“压实”,避免运行时触发操作系统页分配导致抖动。这个对降低长尾延迟有帮助,但会增加启动时间。
  • GC日志输出:建议保留至少10份,每份100M,这样能回溯几天的GC历史。

注意:参数不是越多越好。有些参数是调试时才加的,比如-XX:+PrintHeapAtGC-XX:+UnlockDiagnosticVMOptions -XX:+G1SummarizeConcMark,平时线上别开,日志量爆炸,性能和存储都受不了。

5.2 参数调整的核心原则:先看对象速率,再调空间比例

我见过太多人调参失败,原因只有一个:不看对象分配速率,上来就乱改年轻代大小。

举个例子,如果你把到小年轻代调大,短期内Young GC的触发频率会降低,但每次STW的时间反而更长,因为要回收的Region更多;如果你把年轻代调小,GC频率上去了,但STW时间短,对象晋升得更快,老年代压力大了。这需要一个平衡点。

我最常用的分析方法是这样:用jstat -gcutil <pid> 1000连续观察1分钟,记录E(Eden)、O(Old)、YGCYGCTFGCFGCT这些指标。算一下:

  • Young GC平均频率 = YGC总数 / 观察时长。
  • Young GC平均耗时 = YGCT / YGC总数。
  • 老年代增长率 = (观察结束Old - 观察开始Old)/ 观察时长。

如果你的Young GC频率特别高(比如每秒好几次),且每次耗时很小,那就是对象分配过快,重点是优化业务代码减少创建对象,而不是调参数;如果Young GC频率正常,但老年代以肉眼可见的速度往上走,就要查是不是有大对象、缓存或者内存泄漏。

G1下还可以进一步看jstat -gccause,它会告诉你上次GC的原因,是Allocation Failure还是Metadata GC Threshold,帮助定位触发因素。

5.3 一个真实参数调整前后的对比

说回我在库存服务上的一次调整。最初用的是Parallel GC,默认4G堆,启动参数只有-Xms4g -Xmx4g。压测20分钟,Young GC平均耗时18ms,Full GC总共出现4次,最长一次2.1s。我把参数改为G1,并调整了新老年代比例和停顿目标,跑了同一轮压测:

指标Parallel默认G1 + 调整后
Young GC次数13589
Young GC平均耗时18ms12ms
Full GC次数40
最大STW2.1s160ms
吞吐量89%93%

对比很明显:GC总时间变低、最大停顿从秒级降到百毫秒级、吞吐量还有小幅提升。这说明对于8G以内的应用,如果抖动敏感,G1往往比Parallel更合适;但对于一个“批处理型”任务,Parallel反而吞吐量更高。参数好坏永远脱离不了业务场景

5.4 警惕“缓存型”系统的隐性问题:大对象直接进老年代

还有一种很常见的坑:本地缓存虽然不大,但存的都是“大对象”。比如你缓存了一个几MB的图片Base64字符串,或者缓存了一个很大的配置List。G1对超过Region大小一半的对象会直接分配到老年代的Humongous对象区域,不经过年轻代。这类大对象如果是“常驻”的,没问题;如果频繁更换,老年代里会堆一大堆废弃大对象,回收非常麻烦。G1做并发垃圾回收时,要把这些Humongous对象拷走或清理,代价比普通对象高很多。

遇到这类问题,我的建议很直接:一是尽量不要把大对象缓存到堆内,可以考虑堆外缓存或者改存对象引用;二是如果一定要在堆内,把G1HeapRegionSize适当调大,比如16M、32M,避开Humongous阈值,让大对象当普通对象走Region分配。但调大Region会增加扫描成本,也要权衡。

6. 工具链选型:从jstat到Arthas,再到MAT,谁说监控没用

很多人问我:“GC问题排查到底该用啥工具?”我的回答是:先学会免费的,再根据场景上收费的。不要一上来就堆全家桶,工具太多反而不知道看哪块。

6.1 jstat:最轻量的五分钟快检

jstat是JDK自带的,适合快速瞄一眼。常用命令:

jstat -gcutil <pid> 1000 30

它会每秒打印一次GC利用率概要,30次之后停下来。重点看EOYGCYGCTFGCFGCT几个字段。如果FGC在持续增长,且FGCT增幅巨大,说明Full GC很频繁,这是最基础的报警信号。不过jstat只给汇总数字,不给你对象级的原因,所以定位还是得靠堆转储。

6.2 Arthas:线上排查“神兵”

Arthas是我排查线上问题最常用的工具。不用改代码,Java进程直接attach。我常用的几个命令:

  • dashboard:看整体线程、内存、GC指标。
  • thread -n 3:看CPU使用率最高的三个线程,通常能抓到瓶颈线程。
  • heapdump /tmp/heap.hprof:把当前堆快照导出来,回头用MAT慢慢分析。
  • tt:追踪方法调用,看哪个方法的入参出参产生了大量对象。

曾经有一次线上问题,我靠tt命令追踪了一个处理订单的service方法,发现每次调用都new了一个2MB的临时byte[],用于拼接日志,结果把Eden区冲爆了。这就是典型代码问题引致的GC压力,没有Arthas这种工具很难在线上精准定位。

6.3 MAT:堆转储分析的黄金标准

MAT(Eclipse Memory Analyzer)是分析堆快照最经典的桌面工具。有几个核心操作:

  1. Histogram:按对象数量、保留内存大小排Rank,快速找大头。
  2. Dominator Tree:看哪个对象“支配”了一大块内存,通常能定位到缓存Map、全局集合这类的根持有者。
  3. Leak Suspects:自动给出一系列“可疑泄漏点”,虽然不能直接确认,但能大大缩小排查范围。

用MAT时,有一个容易踩的坑:堆快照文件别直接在线上进程里硬分析,容易把线上机器的内存也搞爆。最好是jmap或Arthas导出来,拉到本地或者专门的跳板机再分析。否则本来在排查Full GC,结果你先把机器整Full GC了。

6.4 监控大盘:GC指标纳入告警体系

到了平台期,GC监控一定要接入可视化大盘和告警。我现在的要求很简单:

  • GC次数、单次耗时、GC后堆内存变化,全部进监控埋点。
  • FGC次数 > 每分钟1次或者单次Full GC耗时 > 500ms的动作,触发告警通知。
  • 老年代使用率稳定在90%以上不回落,视为高风险泄漏信号。

平台不重要,Prometheus + Grafana、自研监控、云厂商监控都行,关键是“可追踪、可比较、可告警”。没有数据闭环的GC优化,基本等于盲人摸象。

7. 常见问题与排查技巧实录

写到这里,我把这些年见过的高频问题整理一下,按场景贴出来,希望大家能少走点弯路。

7.1 “为什么Full GC之后老年代还有大量对象被杀不掉?”

如果Full GC之后,老年代内存占用还是很高的,第一个怀疑方向是内存泄漏。我见过最常见的泄漏模式:

  • 把对象放进静态Map,却忘了移除。
  • 使用ThreadLocal保存大对象,线程池复用导致对象无法释放。
  • 监听器或回调注册后没有反注册。
  • 各种框架的Metaspace元数据持续膨胀(类加载器泄漏)。

排查方式就是抓两份堆快照,间隔15分钟到半小时,用MAT的Compare功能对比。如果某个类型的对象数量持续上升且被GC root引用,那基本就是泄漏了。

7.2 “GC参数改了,但线上STW还是很高怎么办?”

参数只是一部分,更大的可能性是锁竞争、IO等待或者CPU调度导致的“非GC停顿”。GC日志里显示的停顿时间,只是JVM暂停业务线程的时间;实际业务线程停顿还包括锁等待等。排查这类问题要看业务线程的调用栈,找锁、找IO,别死盯GC参数。

另外一个很容易忽略的点:机器CPU核数如果被其他容器争抢,GC线程本身也跑不快,停顿自然被拉长。如果你跑在容器化环境里,要注意-XX:ActiveProcessorCount-XX:ParallelGCThreads的配置,别让JVM默认值高估了你实际能用的CPU。

7.3 “G1设置了MaxGCPauseMillis=100,为什么还经常超?”

G1的停顿目标本质是“启发式”的,它会基于历史数据往这个目标靠,但遇到特殊情况(大对象分配、Humongous回收、Full GC退化)照样突破。要降低突破概率,建议:

  • 保持-Xms = -Xmx,避免堆扩容抖动。
  • 控制年轻代最大占比,避免Young GC一次收太多Region。
  • 减少Humongous对象,大对象能拆则拆。
  • 老年代占比超过65%左右时候,G1的Mixed GC会进入高负载模式,这时候要主要检查内存压力来源,而不是继续调目标停顿。

7.4 “线上GC日志里出现System.gc(),怎么回事?”

System.gc()触发Full GC在分布式系统里很危险。常见来源是JNI/NIO框架在申请堆外内存时调用System.gc()来触发堆内整理,比如老版本Netty的DirectBuffer回收路径。解决思路:

  • -XX:+DisableExplicitGC禁用显式GC调用(但这样做可能有堆外内存回收延迟的风险)。
  • 更好的做法是升级框架版本,或者手动管理DirectByteBuffer的池化和释放。
  • 排查时在GC日志里看[Full GC (System.gc()),基本就能定位是谁调用的。

7.5 一套速查表:GC问题 → 排查重点

现象优先排查方向
Young GC频繁代码大量创建短命对象、业务峰值流量冲击
老年代持续增长缓存未淘汰、线程数过多、批量查询产生大对象
Full GC频繁内存泄漏、大对象扎堆进入老年代、堆太小
GC停顿突增且导致接口超时锁竞争、磁盘IO抖动、GC线程受限于CPU
进程启动后堆内存瞬间占满启动加载缓存/数据量过大、元空间不足、ClassLoader泄漏

8. 最后的几条实操心法

项目做得多了,我越来越觉得GC优化不是某一项硬核技术,而是一套“内存功夫”——表面上是调JVM,内核是对业务代码的理解、对并发模型的控制、对容灾手段的把握。

我个人在实际操作中有几个很坚持的习惯:

第一,上线前必须压测并保留GC基线。没压测过的服务,做GC优化就像闭着眼开车。一次压测下来,把YGC、FGC、老年代增长率记录到文档里,作为后续调优和回归的基准值。

第二,每次只改一个变量。调GC参数的时候,千万别一次性改三四个参数,否则出了效果你根本不知道是哪个参数做的贡献。我的做法是改一个参数压测一轮,数据落表,再改下一个。

第三,GC日志要“永久开启”。哪怕日志量再大,也要保留至少一周的轮转文件。很多线上问题是“偶发”的,事后没日志就只能靠猜。

第四,遇到故障先复盘代码,再动参数。GC优化的顺序一定是从上往下:代码层 → 架构层 → JVM参数层。很多公司一遇到Full GC就靠“加内存”硬扛,那是治标不治本。你得先搞清楚老年代里到底装了什么,才有资格谈参数。

最后再分享一个屡试不爽的小技巧:当你实在定位不到老年代增长原因时,就在压测环境下给老年代设置一个很低的-XX:MaxTenuringThreshold=1,强制对象快速晋升。这样那些“短命对象”会更快暴露在老年代里,更容易通过两轮堆快照的对比找到元凶。等定位完,再把阈值调回来。这个方法听上去有点“暴力”,但排查效率极高,值得一试。

GC优化这件事,没有一劳永逸的银弹,但它绝对是一门可以靠方法论、工具和复盘不断逼近“零抖动”的硬功夫。希望这篇分享能帮你少踩几个我在线踩过的坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 3:35:09

COMSOL复现BIC拓扑荷:光子晶体超表面远场偏振涡旋计算全流程

做BICs和光子晶体超表面计算有一段时间了&#xff0c;最让我头疼也最上头的&#xff0c;就是复现文献里那种“围绕BIC动量点的远场偏振矢量涡旋图”。一句话说清楚的话&#xff0c;拓扑BICs&#xff08;连续谱束缚态&#xff09;在动量空间是一个偏振奇点&#xff0c;远场偏振矢…

作者头像 李华
网站建设 2026/9/16 3:34:32

51单片机霍尔转速测量与PWM调速设计详解

简介&#xff1a;面向单片机初学者及课程设计/毕业设计学生&#xff0c;这套基于51单片机的综合设计覆盖霍尔转速测量、DS18B20温度检测、LCD1602显示以及按键控制的PWM电机调速&#xff0c;通过L298N驱动电路实现启停、正反转、加减速&#xff0c;功能完整&#xff0c;可作为项…

作者头像 李华
网站建设 2026/9/16 3:33:23

STM32F429双CAN测试程序详解:从引脚映射到Bus Off恢复

简介&#xff1a;面向STM32F429嵌入式开发者的双CAN通道测试程序&#xff0c;用于验证CAN1与CAN2两个独立控制器的初始化配置、报文收发和中断处理&#xff0c;适合汽车电子、工业自动化领域的中初级开发者学习。程序基于HAL库实现&#xff0c;围绕ISO 11898协议标准&#xff0…

作者头像 李华
网站建设 2026/9/16 3:33:14

茶器艺科智造HarmonyOS应用实战-24-导出的数组和默认草稿都可被消费者改写:用readonly与工厂函数守住HAR边界

茶器艺科智造HarmonyOS应用实战-24-导出的数组和默认草稿都可被消费者改写&#xff1a;用readonly与工厂函数守住HAR边界 HAR 把 SHOP_FILTER_KEYS、预设标签、快捷指令、雷达参考值和 DEFAULT_CHAQI_DRAFT_STATE 统一导出后&#xff0c;消费者拿到的不只是“常量名字”&#x…

作者头像 李华
网站建设 2026/9/16 3:32:37

系统镜像制作实战:从dd到Clonezilla的Linux/Windows迁移指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:32:16

Linux WiFi设备驱动开发实战:从设备树到数据通路的完整指南

写WiFi驱动之前&#xff0c;先把我踩过的坑说在前面。别指望内核文档能救你&#xff0c;也别指望芯片原厂SDK能直接跑起来。大多数时候你面对的是一个只给了数据手册、几个补丁和一堆BSP的WiFi模组&#xff0c;然后要在Linux下让它稳定工作。这篇文章从驱动框架、环境搭建、设备…

作者头像 李华