news 2026/9/16 5:18:32

JVM GC日志分析实战:从Full GC到内存泄漏的完整排查方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM GC日志分析实战:从Full GC到内存泄漏的完整排查方法

那阵子我们线上有个支付渠道服务,晚上高峰期总是时不时冒出几个超时报警。CPU不高、内存看着也没满,就是接口偶尔卡一下,每次卡个几百毫秒到一秒不等。排查了很久没头绪,最后把GC日志翻出来,才发现每两三分钟就有一个Full GC,Old区回收完还是占了80%以上。顺着GC日志往里挖,定位到某个缓存对象在循环里被反复拼接成大数组,修掉之后整个服务安静得让人不适应。

从那以后,我养成了一个习惯:不管线上出什么问题,先把GC日志抓到手里。GC日志就是JVM的黑匣子,它不会告诉你业务为什么慢,但它能告诉你内存里正在发生什么,回收器在替你扛什么。这篇文章就把我平时做GC日志分析的一套方法完整写出来,从怎么开日志、怎么看日志,到怎么透过日志判断问题,再到常见故障怎么定位,争取让看完的人能直接拿去用。

1. 为什么我把GC日志当作JVM的“黑匣子”

很多做Java开发的人对GC日志的态度是:知道有这个东西,但从来不看。应用能跑就行,报错了再打日志,慢了就加内存,实在不行重启一下。这种操作方式在小项目里确实能撑一阵子,但一旦流量上来,GC日志往往是第一个告诉你危险的信号。

GC日志记录的是JVM进行垃圾回收时的行为,包括什么时候回收、回收了哪些区域、回收前花了多少内存、回收后剩多少、停顿了多久、是Minor GC还是Full GC。这些信息可以直接回答几个很要命的问题:

  • 系统变慢,是因为GC停顿还是业务代码本身就慢?
  • 内存是不是在悄悄增长,最后拖垮整个JVM?
  • Eden区是不是频繁被打满,导致Minor GC过于频繁?
  • Old区为什么一直在涨,对象是怎么晋升上去的?

这些问题在应用日志里几乎找不到答案,但在GC日志里都有明确记录。准确说,GC日志是目前观察JVM堆内运行情况最直接、最可靠的入口,没有之一。

什么时候需要重点看GC日志?我的经验是这三类场景必须看:

  1. 接口RT抖动,但业务日志里没有明显异常,代码走查也看不出问题;
  2. 服务莫名其妙频繁Full GC,或者时不时出现OutOfMemoryError;
  3. 上线新功能或调整JVM参数后,想确认内存模型和回收行为是否符合预期。

说白了,GC日志不是出了问题才翻的工具,它应该成为Java服务上线前的标配观测项。我见过太多线上事故,其实早在大规模宕机之前,GC日志里已经连续出现异常趋势了,只是没人去读。

2. 不同JDK版本的GC日志开启方式:从PrintGCDetails到统一日志

GC日志怎么开,首先要看你用的JDK版本。不同版本之间的参数差异很大,这个坑如果不注意,线上很容易配了没效果,或者配完日志文件疯狂膨胀把磁盘打满。

2.1 JDK 8及之前的传统参数

JDK 8是当前存量系统里最常见的一个版本,配套的GC日志参数是传统风格:

-Xloggc:/data/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps

这几个参数各管一摊事:

  • -Xloggc:指定GC日志输出到文件,路径要确保存在且应用有写权限;
  • -XX:+PrintGCDetails:输出每次GC的详细内存变化,这是分析的核心数据;
  • -XX:+PrintGCDateStamps:在每条日志前加上绝对日期时间,方便跟业务日志对照;
  • -XX:+PrintGCTimeStamps:加上JVM启动以来的相对秒数,用来计算GC发生的时间间隔。

如果还想看对象晋升和存活年龄的分布,可以追加:

-XX:+PrintTenuringDistribution

这个参数会打印各年龄对象的大小分布,对分析“对象过早晋升”这类问题很有用。另外有一个参数我建议非特殊情况不要开,就是-XX:+PrintHeapAtGC,它会在每次GC前后把整个堆的详细输出打一遍,信息量虽然大,但日志体量会爆炸,对排障来说通常没必要。

JDK 8还有一个很容易被忽略的多余参数:-XX:+PrintGC。很多人以为开它就够了,但它只输出最简略的一行,根本没有细节,对分析帮助不大。既然用了-XX:+PrintGCDetails,就不需要再写-XX:+PrintGC了。

2.2 JDK 9及之后的统一日志

进入JDK 9之后,JEP 158把JVM日志做成了统一框架,老参数虽然还兼容但会报警告,新项目的正确做法是使用-Xlog

-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags

这段的意思是:输出所有带gc标签的日志到文件,时间格式包含系统时间和JVM启动秒数,同时显示日志级别和标签。注意gc*的星号不能省,它代表匹配gc以及gc下面的子标签,比如gc+heapgc+agegc+ergo这样细分的类别。

如果只想看GC暂停和堆变化,可以简化成:

-Xlog:gc:file=/data/logs/gc.log:time,uptime

但做分析最好还是用gc*,不然很多有用的子标签信息看不到。

需要特别注意,JDK 9+的-Xlog参数位置有讲究,它和普通-XX参数不一样,不能随便放在-jar命令的后面。正确用法是放在java命令和-jar之间。我之前见过有人把它当成普通JVM参数追加到最后,结果Java进程直接报错无法启动。

2.3 日志滚动配置:别让GC日志把磁盘写爆

GC日志如果不做滚动,运行时间长了之后文件会非常大,轻则占满磁盘,重则拖垮整个应用。JDK 8老参数阵营里,滚动靠两个参数:

-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M

JDK 9+的-Xlog可以配合文件大小和数量参数,比如:

-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=20m

我习惯按5个文件、每个20MB来配置,既能覆盖突发高峰期几天的日志,又不至于占太多磁盘。对GC日志来说,没必要追求存太久,它最重要的作用是出事的时候能往前回溯一段,够用就行。

生产环境改完这些参数必须重启才能生效,因为GC日志开关是在JVM启动时确定的。有些中间件支持通过JMX动态调一部分参数,但GC日志开关这块我不建议依赖动态方案,稳妥做法是发布时把参数固化到启动脚本里,并且配置完看一眼生成的日志文件,确认确实在写。

3. 拆解一行GC日志:回收前后到底发生了什么

日志开了之后,一堆格式各异的文本出现了。很多人第一步就卡在这里:看不懂。其实GC日志的格式是高度结构化的,拆开来看并不复杂。关键是你得知道自己用的是哪个垃圾回收器,不同回收器打出来的内容长得很不一样。

3.1 传统回收器(Serial/Parallel/CMS)的日志格式

以Parallel Scavenge为例,一个典型的Minor GC长这样:

2019-04-11T15:02:32.452+0800: 745.482: [GC (Allocation Failure) [PSYoungGen: 261120K->26112K(304640K)], 0.0323159 secs] [Times: user=0.06 sys=0.00, real=0.03 secs]

逐个字段拆:

  • 2019-04-11T15:02:32.452+0800:绝对时间,来自PrintGCDateStamps
  • 745.482:JVM启动后经过的秒数,来自PrintGCTimeStamps
  • GC:表示这是Minor GC。如果这里是Full GC,就是整堆回收;
  • Allocation Failure:触发原因,意思是Eden区分配新对象失败,这是最常见的Minor GC触发原因;
  • PSYoungGen:回收的区域,这里是Parallel Scavenge的年轻代;
  • 261120K->26112K(304640K):回收前占用261120K、回收后占用26112K、该区域总容量304640K;
  • 0.0323159 secs:本次GC耗时,这个值对应第一次停顿时间;
  • [Times: user=0.06 sys=0.00, real=0.03 secs]:CPU时间user、sys和实际墙钟时间real。user大于real说明GC使用了多线程并行回收。

再看Full GC:

2019-04-11T15:02:33.431+0800: 746.461: [Full GC (Metadata GC Threshold) [PSYoungGen: 26112K->0K(304640K)] [ParOldGen: 700026K->682381K(700240K)] 726138K->682381K(1004880K), [Metaspace: 20361K->20361K(1069056K)], 0.0876950 secs] [Times: user=0.08 sys=0.00, real=0.09 secs]

这里能看到年轻代和老年代分别的变化,然后是整堆的变化726138K->682381K(1004880K),以及Metaspace的变化。Full GC的原因Metadata GC Threshold代表元空间达到了阈值触发回收,这个问题后面会重点讲。

3.2 G1回收器的日志格式

JDK 8之后默认回收器逐渐切换到G1,G1的日志格式比传统回收器多了一个分阶段信息:

2025-01-10T10:00:00.123+0800: 125.123: [GC pause (G1 Evacuation Pause) (young) [Parallel Time: 20.0 ms, GC Workers: 8] [Eden: 1024.0M(1024.0M)->0.0B(1024.0M) Survivors: 16.0M->16.0M Heap: 2048.0M(4096.0M)->1986.0M(4096.0M)] [Times: user=0.05 sys=0.02, real=0.01 secs]

首先看GC pause (G1 Evacuation Pause) (young)。G1的回收活动叫“暂停”,可以是young(年轻代)或mixed(年轻代+老年代)。Parallel Time是并行回收阶段的总耗时,如果这个值大,说明单次GC时间很长。

堆变化那行是重点:Eden: 1024.0M(1024.0M)->0.0B(1024.0M)表示Eden区从1024M清到0,Survivor区从16M变16M,整个Heap从2048M降到1986M。G1的容量单位可能是K或M,这个要看具体版本和配置。

还要留意G1日志里的Humongous相关字样。G1把超过Region大小50%的对象称为“巨型对象”,分配巨型对象有时候会直接触发一次GC,比如:

[Humongous Register: 5.0M] [Humongous Reclaim: 0.2M]

如果日志里频繁出现Humongous,基本可以断定代码里在频繁创建大对象,这是个重要信号。

3.3 JDK 9+统一格式的日志怎么读

JDK 9+的输出经过了重新整理,比老版本干净很多:

[0.345s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 23M->1M(63M) 2.013ms [0.345s][info][gc,cpu] GC(0) User=0.00s Sys=0.00s Real=0.00s

GC(0)是GC序号,方便日志里搜索某一GC。后面是类型、堆变化和耗时。相比JDK 8,统一格式更紧凑,但也少了部分细节。要看更细的内容,可以加标签:

-Xlog:gc+heap=debug:file=/data/logs/gc.log:time,uptime,level,tags

这样会输出每次GC前后的堆详细数据。日常分析和排查,标准gc*级别就够了,不用过度追求detail。

4. 判断GC是否健康:频率、停顿、内存趋势与吞吐量四个维度

拿到一堆日志之后,不能只看有没有关键字,要从四个维度做整体评估。这四个维度是我日常分析的固定套路,也是判断一套JVM参数配置是不是合理的核心标准。

4.1 GC频率

先看GC发生的频率。统计一下单位时间内Minor GC和Full GC各发生了多少次。

怎么统计?如果日志是JDK 8格式,可以用grepwc快速统计:

grep -c "GC (Allocation Failure)" gc.log grep -c "Full GC" gc.log

然后结合日志覆盖的时间范围算出每分钟频率。正常情况下:

  • Minor GC可以频繁,但一般以每几秒一次到每几十秒一次居多,具体取决于Eden区大小和对象分配速率;
  • Full GC应该非常少,很多健康服务甚至可以几天才一次,如果一天好几次甚至一小时好几次,那就必须介入;
  • 如果Minor GC每秒都要来好几次,说明新生代空间太小或者对象分配速率太高。

频率是趋势指标,单独看一次GC没意义,一定要看一段时间的分布。

4.2 GC停顿时间

停顿时间是用户体验最直接的指标。每次GC都会有一段STW(Stop The World,暂停所有业务线程)的时间,那段real=0.03 secs就是停顿时长。

判断标准我给一个经验值:

  • Minor GC停顿在几十毫秒内正常,超过100ms就要关注;
  • Full GC停顿如果超过1秒,线上基本会有可感知的抖动;
  • G1和ZGC的目标是控制停顿时间,如果G1的real值经常超过200ms,需要检查-XX:MaxGCPauseMillis设置和堆大小。

读取停顿时间时要注意,JDK 8日志里real不一定等于真正的暂停时间,某些回收器在部分阶段是并发的,real是整段GC日志的时间跨度。所以更严谨的方式是看回收器自己上报的停顿时间字段,比如G1日志里的Pause Time

4.3 内存趋势

这是我最看重的一个维度。GC日志里每一行都有“回收前->回收后(总容量)”,把多个时间点的数据连起来,就能画出堆内存使用趋势。核心看两点:

  • 每次GC后堆剩余大小是否在缓慢爬升。如果Old区回收后占用率越来越高,从50%到60%再到70%,这是内存泄漏的典型信号;
  • GC频率是否随运行时间变得密集。如果昨天一天只有20次Full GC,今天就到了50次,明天到100次,直线上升就是危险的信号。

内存趋势分析不需要特别复杂的工具,用脚本把Heap:或整堆变化的回收后数值提取出来,按时间排序就能看出曲线。GCViewer工具也有这个功能,后面章节会说。

4.4 GC吞吐量

吞吐量的定义是:

吞吐量 = 业务运行时间 / (业务运行时间 + GC总耗时)

计算方式很简单,把一段时间内的GC耗时全部加起来,然后用总时间减去GC耗时就是业务运行时间。举个例子,一个服务运行了3600秒,GC总耗时36秒,那么GC开销占比是1%,吞吐量是99%。

业内一般建议GC开销控制在5%以内。如果超过这个值,说明有大量CPU时间被GC消耗掉了,表面上看业务代码很忙,实际上都在帮回收器搬对象。这个指标可以透过工具自动算,也可以简单用脚本统计:

grep -o 'real=[0-9.]* secs' gc.log | awk -F= '{sum+=$2} END {print sum}'

real耗时全部加起来就是GC总耗时。接着除以日志时间跨度,再换算成百分比。

这四个维度不是孤立看的。频率高但停顿短,和频率低但停顿长,是完全不同的调优方向。前者要考虑扩容年轻代或降低对象分配,后者要考虑换回收器或调整堆大小。只看单一维度很容易做出错误判断。

5. 通过GC日志定位问题的实际案例:从现象到根因的完整链路

理论说完了,接下来用几个真实的故障场景走一遍分析流程。这些场景都是我实际在项目里处理过或复盘过的,虽然在具体数字上做了脱敏,但思路完全一致。

5.1 场景一:Full GC频繁,Old区回收后占比还是居高不下

现象:服务运行一两天后,Full GC频率越来越高,从每10分钟一次发展到每2分钟一次,接口超时明显增加。

GC日志里的关键特征:

[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(304640K)] [ParOldGen: 689432K->682401K(700240K)] 689432K->682401K(1004880K), 0.0988760 secs]

注意一个细节:Young区回收前后都是0K,说明已经没有对象在年轻代存活了。Old区回收前689M,回收后682M,几乎没降下去。整堆回收后仍然占到总容量的68%。

这个数据说明老年代里存在大量无法被回收的对象,而且这些对象占用的空间非常大。接下来就不会再看GC日志了,而是把堆dump出来,分析到底是哪些对象占了Old区。

jmap -dump:format=b,file=heap.hprof <pid>

然后用MAT或者VisualVM打开dump,看支配树,通常很快就能找到占大头的对象。我之前遇到过的情况是某个内存缓存Map没有清理机制,Key一直累积,导致Old区被撑爆。修复之后Full GC直接消失,服务稳定运行了一个月,GC日志里只有Minor GC。

这类问题的判断逻辑很清晰:Full GC后Old区回收效果差,重点查堆,不要浪费时间调参数。

5.2 场景二:Minor GC极其频繁,但每次耗时很短

现象:应用整体响应还行,但CPU占用偏高,线程Dump里大量线程都在正常执行业务代码,看不出阻塞。

GC日志特征:

[GC (Allocation Failure) [PSYoungGen: 191744K->5184K(195328K)], 0.0089650 secs] [GC (Allocation Failure) [PSYoungGen: 192032K->6304K(195328K)], 0.0078860 secs] [GC (Allocation Failure) [PSYoungGen: 191936K->5472K(195328K)], 0.0090250 secs]

三行GC都在几百毫秒内发生,每次耗时不到10ms,但抵不住频率高,GC总CPU开销非常可观。这里的信息点在于Eden区总容量约192M,回收前占用约191M,也就是说Eden刚刚被填满就立刻触发回收。回收后Young区还剩5M左右,说明大量短生命周期对象在Minor GC后就无法回收,晋升到了Old区。

这类问题通常是业务代码里创建了大量短期对象,比如循环里做字符串拼接、JSON序列化、频繁创建临时集合。解法有两个方向:一是改代码,减少对象创建;二是在不改代码的情况下调大年轻代,提高Eden区容量,降低Minor GC频率。

如果决定调参,可以设置:

-Xmn512m

把新生代从默认比例调大,配合观察GC频率曲线。但这不是长久之计,根治还是要回到代码层面。

5.3 场景三:Metaspace触发Full GC

现象:服务运行一段固定时间后,出现规律性的Full GC,而且发生时间点高度一致。

GC日志特征:

[Full GC (Metadata GC Threshold) [PSYoungGen: 8920K->0K(460800K)] [ParOldGen: 30205K->25618K(50176K)] 39125K->25618K(511976K), [Metaspace: 20480K->20480K(1069056K)], 0.0128450 secs]

注意触发原因是Metadata GC Threshold,而且Metaspace回收前后都是20480K,没降下来。这说明元空间达到了触发阈值,但类加载器本身并没有大量卸载,Metaspace容量还在正常范围。

这类情况在JDK 8比较常见,因为MetaspaceSize默认值不大,类加载到一定程度就触发Metaspace GC。处理方式是在启动参数里把MetaspaceSize设置到一个合理值,让它不要过早触发回收。

一般建议:

-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

具体值要看应用实际加载的类数量。设置之后观察GC日志,这类Full GC通常会消失。注意不要为了省事把MaxMetaspaceSize设得过大,真出现类加载器泄漏时反而会掩盖问题。

5.4 场景四:G1日志中频繁出现巨型对象

现象:使用G1回收器的服务,在流量高峰期频繁出现GC暂停,但堆整体占用并不高。

GC日志特征:

[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0359870 secs] [Humongous Register: 68.0M] [Eden: 512.0M(512.0M)->0.0B(512.0M) Survivors: 32.0M->32.0M Heap: 1024.0M(2048.0M)->956.0M(2048.0M)]

Humongous Register显示这次GC注册了68M的巨型对象,如果你知道G1的Region大小是1M,那就意味着有大量超过0.5M的对象一直在被创建。巨型对象的分配在G1里成本很高,而且因为无法在年轻代正常复制,容易触发连续的GC。

顺着这个线索查代码,最后定位到某段逻辑会构造一个很大的二维数组,高峰期并发一多就把G1逼得快疯了。改造方案是把大数组对象池化或者换成堆外存储,改完之后Humongous Register基本上不再出现,GC暂停也恢复到了20ms以内。

5.5 场景五:CMS的Concurrent Mode Failure

这个场景在存量老系统里还很常见。GC日志出现过类似这样的内容:

[Full GC (Concurrent Mode Failure) ... 0.9769980 secs]

Concurrent Mode Failure本质是CMS回收器在并发标记阶段,老年代空间就被新对象填满,导致CMS来不及完成并发回收,被迫降级成Serial Old进行全停顿回收。停顿时间通常会超过1秒,线上影响非常明显。

根因通常是老年代容量设置偏小,或者对象晋升速度过快。调整方向包括:

  • 调大老年代空间,比如-XX:OldSize-Xmx
  • 提前触发CMS回收,把-XX:CMSInitiatingOccupancyFraction从默认值调低,比如68,配合-XX:+UseCMSInitiatingOccupancyOnly(手动设定启动阈值);
  • 如果条件允许,直接迁移到G1或ZGC。

碰到CMS问题,别只盯参数,还得看晋升速率。用-XX:+PrintTenuringDistribution观察各年龄对象大小,如果高龄对象占比异常,往往意味着晋升阈值设置不合理。

6. 分析工具怎么选:GCViewer、GCeasy,还有自己动手统计

日志量大了之后,纯靠肉眼一行行看肯定不现实。尤其是线上服务跑几天,GC日志可能上万行。我日常会用工具做初步筛查,再用脚本做定点分析。

6.1 GCViewer:本地离线分析首选

GCViewer是一个开源的桌面工具,可以直接把gc.log文件拖进去,自动生成各种曲线图。它能直观展示:

  • 堆使用随时间变化的曲线;
  • GC频率直方图;
  • 每次GC的停顿时间分布;
  • 吞吐量和GC开销的统计值。

我个人用的版本是GitHub上的chewiebug/GCViewer,一个jar包就能跑:

java -jar gcviewer.jar gc.log

界面虽然朴素,但信息密度非常高。尤其适合用来回答“整体趋势怎么样”这类问题。看一眼曲线如果发现每次GC后堆占用在稳步抬高,基本可以判定内存泄漏方向,不用再慢慢翻日志时间戳。

GCViewer对JDK 8传统格式和G1格式支持得都不错,但JDK 9+统一格式的解析偶尔会有偏差,它现在也支持由新型日志生成。如果发现数据不对,还是拿原始文本验证。

6.2 GCeasy:网页版快速分享

GCeasy是一个在线GC日志分析平台,把日志文件传上去,它能自动生成一份排版很友好的报告。适合不太想折腾本地工具的人,也可以直接把报告链接发给同事一起看。

使用时要特别注意数据安全。GC日志不包含业务数据,但包含堆内存信息,理论上存在一定泄露风险,敏感项目不要用在线工具,自己用GCViewer或者脚本分析更稳妥。

6.3 自己动手写个简单统计脚本

工具能给出图表,但有些计算还是要自己来。比如把日志按小时分组统计GC次数,或者统计某一段时间内停顿总时长,写个小脚本比打开工具更快。

JDK 8格式下,可以用awk统计每次GC间隔:

awk '/2019-04/{print $2}' gc.log | awk -F: '{print $1":"$2}' | uniq -c

这条命令按小时统计GC日志条数,能快速看出一天内哪个时段GC最频繁。

如果要把停顿时间累加,处理Times:字段:

grep -o 'real=[0-9.]*' gc.log | sed 's/real=//' | awk '{sum+=$1} END {print sum}'

这样就能算出日志覆盖时间内的总停顿秒数,除以运行时长得到GC开销占比。很多看起来很高大上的监控数据,其实用这几条命令就能算出来。

6.4 辅助命令:jstat做实时观察

GC日志是事后分析,如果想实时观察,配合jstat看当前JVM的GC行为很有用:

jstat -gcutil <pid> 1000 10

每秒打印一次各区域使用率和GC累计时间。jstat看到的是当前快照,能够和GC日志相互印证。比如GC日志说Full GC频繁,jstat里老年代使用率应该也居高不下,两边对得上才能确认。

实时监控体系完整的团队,也可以接入Prometheus的JMX Exporter,把GC次数和耗时指标可视化。不过GC日志依然是底层原始数据,可视化指标出问题时,最终还是要回来翻GC日志的细节。

7. GC日志分析里最容易被忽略的几个细节

文章最后,把我在多次实战中踩过的坑和一些关键心得整理一下。

  • GC日志必须结合业务流量看。同一套JVM参数,白天高峰期和凌晨低峰期表现完全不同。分析时一定要先确认日志时间段对应的流量情况,否则容易把正常的阶段性行为误判成故障。

  • 不要一看到Full GC就慌。Minor GC、Major GC、Full GC在日志里的含义和触发条件不同,有些Full GC一共也就几十毫秒,对业务没影响。真正危险的是停顿时间长、回收效果差的Full GC。

  • 调JVM参数要有“单变量原则”。一次只改一个参数,改完跑一段时间再看GC日志。很多人一次调了五六个参数,出了问题根本不知道是谁引起的。

  • GC日志文件不要放在系统盘或数据盘根目录,建议放在独立目录并做logrotate。GC日志虽然做了滚动,但总容量依然在增长,磁盘告警在关键时刻可能比GC本身更致命。

  • 使用G1时要理解它和传统回收器的调优思路完全不同。G1追求的是可预测的停顿时间,堆越大Region越多,调整-XX:MaxGCPauseMillis只影响它内部的行为目标,不代表实际停顿一定能压到这个值。想真正稳定还是靠观察日志里的实际数据。

  • 遇到长时间无法定位的GC异常,先想想最近上线了什么。多数GC问题的触发源在业务代码,不在GC本身。一个线程池没关闭、一条大查询没分页、一个缓存没限流,都可能让GC日志变得非常难看。调参数只是延缓症状,找到根因才是治疗。

我现在处理线上GC问题的固定流程是:先对比告警时间段和GC日志,确认是GC引起还是业务引起的;然后按GC频率、停顿、内存趋势三个维度快速定位问题方向;再看对应区域的详细数据,判断是分配过多、晋升过快还是回收不动;最后决定是客户端加参数还是提代码问题。整个流程走下来基本不超过半小时,这套方法在多个项目里都验过,希望对你也有用。

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

街景语义解析实战:从DeepLabV3+选型到TensorRT部署全流程

简介&#xff1a;面向计算机视觉与深度学习初学者&#xff0c;提供街景图像语义解析的完整项目源码&#xff0c;可用于道路、建筑、车辆、行人等类别的像素级分割研究。实现上采用 DenseASPP、MobileNetDenseASPP 等网络&#xff0c;覆盖 FCN、U-Net、SegNet 常见结构&#xff…

作者头像 李华
网站建设 2026/9/16 5:17:54

基于Wazuh搭建主机入侵检测实验室:从部署到实战的完整指南

干安全这行&#xff0c;最怕的不是没工具&#xff0c;而是工具太多。去年年中我被要求在一周内给部门搭一套能够常态化运行的检测能力&#xff0c;当时手头的局面是这样的&#xff1a;OSSEC负责主机侧文件完整性检查&#xff0c;ELK负责日志检索&#xff0c;Suricata负责流量侧…

作者头像 李华
网站建设 2026/9/16 5:17:12

微软OAuth 2.0授权流程实战:从PKCE到access_token全链路解析

1. 这不是“点一下授权就完事”的流程——微软登录OAuth 2.0到底在解决什么问题&#xff1f;你有没有遇到过这样的场景&#xff1a;在一款国产笔记App里点击“用微软账号登录”&#xff0c;跳转到一个带微软Logo的页面&#xff0c;输入邮箱密码、点同意、再跳回App——整个过程…

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

agent-skills:AI Agent能力单元的可复用工程化设计范式

1. 项目概述&#xff1a;一个被严重低估的“技能中枢”设计范式“agent-skills”这个词乍看像某个开源库的包名&#xff0c;或是某篇技术文档里的小节标题&#xff0c;但如果你在Node.js、TypeScript和Nx生态里摸爬滚打超过三年&#xff0c;就会立刻意识到——它不是功能模块&a…

作者头像 李华
网站建设 2026/9/16 5:15:17

领域事件落地方案:从同步调用链到事件驱动架构的完整实践

前阵子帮一个团队排查线上问题&#xff0c;他们的下单接口平均耗时从最初的 200 毫秒一路涨到接近 4 秒。一开始所有人都怀疑是数据库慢查询&#xff0c;结果一轮排查下来&#xff0c;发现耗时主因根本不在 SQL&#xff0c;而是下单成功之后挂在主链路上的一串同步动作&#xf…

作者头像 李华
网站建设 2026/9/16 5:15:10

银行开户业务合规性验证测试框架:接口自动化与数据驱动实践

做银行核心系统测试这几年&#xff0c;我最怕听到一句话就是“开户流程改了一下&#xff0c;你帮忙回归一下”。开户这个动作看起来简单&#xff0c;但它背后挂着一大串监管硬性要求&#xff1a;客户身份识别、资料真实性核验、黑名单命中筛查、反洗钱可疑交易判断、风险等级评…

作者头像 李华