尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

JVM调优实战:从Full GC频繁到性能稳定,一次线上事故的排查复盘

发布时间:2026/9/29 2:39:04

资讯中心
01
ARTICLE

JVM调优实战:从Full GC频繁到性能稳定,一次线上事故的排查复盘

JVM调优实战:从Full GC频繁到性能稳定,一次线上事故的排查复盘
一次线上事故引发的调优思考为什么我劝你别急着改参数先说个真实场景。两年前我接手过一个订单查询服务平时响应时间稳定在 30 毫秒以内结果每次到了整点结算时段TP99 能直接冲到 3 秒以上后台时不时还会出现GC overhead limit exceeded的报错。一开始团队第一反应是加机器但加了四台之后发现情况只是缓解并没有根治。后来我把 GC 日志打开才看到真相JVM 的 Full GC 在高峰期几乎每两分钟就触发一次单次 Stop The World 最长达到 4.6 秒。这篇文章就是想把那次从“GC 频繁”到“性能稳定”的完整过程拆开讲清楚包括 JVM 内存模型里哪些区域最容易出问题、GC 日志和监控工具怎么用、参数调优到底该调哪些、以及最后如何通过堆转储揪出内存泄漏的根因。无论你是刚接触 JVM 的初级开发还是整天被线上 GC 问题折磨的运维或后端负责人这篇内容都能给你一套可以照着走的排查路径。很多人一听说 JVM 调优脑子里蹦出来的就是-Xmx、-Xms这类堆大小参数。但实际上90% 的调优问题都出在对内存模型和 GC 触发机制理解不够导致参数改得越多系统反而越不稳定。所以咱们先从最关键的底层概念说起。1. 为什么频繁 Full GC 会拖垮吞吐量先理解 Stop The World 的代价1.1 GC 停顿到底停了什么JVM 在做垃圾回收的时候为了保证对象引用关系的正确性很多时候必须暂停所有业务线程这个动作叫 Stop The WorldSTW。你以为停顿只是“停一下垃圾回收”实际影响的是整个应用在这段时间内完全无法处理任何请求。Minor GC 通常停顿很短几十毫秒级别用户几乎无感知但 Full GC 如果频繁发生每次停顿几百毫秒甚至几秒就会直接把接口 RT 拉爆。有一次我通过 GC 日志看到峰值阶段出现连续三次 Full GC每次停顿都超过 2 秒。换算成业务损失就是这 6 秒里所有新进来的请求全部在排队集群里其他节点承担不了直接流量溢出雪崩就是这样发生的。1.2 吞吐量和延迟是两个互相打架的指标很多人调优时只盯着“停顿时间越小越好”这是误区。JVM 的 GC 调优本质上是在两个指标之间找平衡一个是吞吐量也就是“业务线程运行时间占整体时间的比例”另一个是延迟也就是“GC 停顿造成的最坏影响”。如果堆给得很大GC 频率会降低但每次 Full GC 清扫的对象多停顿时间反而变长。如果堆给得小单次停顿短了回收频率却上来了吞吐量照样掉。所以调优的第一步不是改参数而是明确当前系统的核心痛点到底是吞吐量不足、延迟过高还是两者都差。明确目标之后再谈参数才有意义。1.3 线上事故里最常见的三类 GC 异常模式根据我处理过的几十个线上问题GC 异常大致可以分成三类异常模式典型表现常见根因频繁 Minor GCEden 区每几秒就回收一次CPU 空转明显新生代太小、对象创建速率过高频繁 Full GC老年代持续增长Major GC/Full GC 密集出现内存泄漏、晋升阈值不合适、老年代容量不足单次停顿过长Full GC 后单次 STW 超过 1 秒堆太大但 GC 算法不当、存活对象过多导致标记时间过长有了这张表你在看监控时就能快速对应到问题类别而不是一上来就各种参数堆叠。接下来我们补一下背后的内存模型原则因为这决定了后续每一步排查的方向。2. 调优前的基本功JVM 内存模型的关键区域和 GC 触发机制2.1 堆内存的分代设计为什么把堆拆成年轻代和老年代JVM 堆内存有两个核心区域年轻代Young Generation和老年代Old Generation。年轻代内部又分成 Eden 区、Survivor 区通常有两个S0 和 S1。这种设计基于一个统计事实绝大多数对象都是“朝生夕死”生命周期极短。把短命对象集中到年轻代用复制算法快速回收把熬过多次 GC 还活着的对象晋升到老年代用标记整理或标记清除来管理效率会高很多。对象晋升的过程大致是这样新对象出生在 Eden 区Eden 满了触发 Minor GC存活对象先复制到 S0下一次 Minor GC 时S0 存活对象复制到 S1同时年龄加 1。当对象年龄超过-XX:MaxTenuringThreshold设定的阈值或者 Survivor 区放不下时就直接进入老年代。这里要特别注意一点大对象比如很大的数组或字符串会绕过年轻代直接进入老年代。如果系统里频繁创建大对象老年代会被快速塞满Full GC 自然频繁。这也是为什么光调堆大小不够还得看一下是不是代码层面的对象分配习惯有问题。2.2 年轻代、老年代和元空间分别什么时候触发 GC做调优之前一定要把各类 GC 的触发条件记熟否则连日志都没法判断。Minor GC 发生在年轻代空间不足时最常见的就是 Eden 区被新对象占满。此时 JVM 尝试分配新对象失败就会触发一次 Minor GC。Minor GC 的适用范围是年轻代老年代和元空间不受影响。Major GC 和 Full GC 的区别很多文章说不清。严格来说Major GC 是针对老年代的回收而 Full GC 通常包含年轻代、老年代、元空间等多区域的回收。但出于习惯很多人把两者混着用。在分析日志时你只要看到日志开头是Full GC基本就代表一次完整的 STW 回收停顿影响最大。老年代 GC 的触发条件包括老年代空间不足、System.gc()被显式调用、晋升来的对象放不进老年代、元空间Metaspace达到阈值触发空间扩容等。其中元空间这个点特别容易踩坑。JDK 8 之后永久代被元空间取代元空间不再使用 JVM 堆内存而是占用本地内存。如果你用到了大量动态生成类的框架比如 CGLIB、反射生成类、某些 ORM元空间占用会逐渐变大默认的初始阈值和最大阈值如果不调整就会导致频繁触发 Full GC同时伴随本地内存耗尽的风险。2.3 别只盯着堆线程栈、元空间和直接内存也可能拖垮系统JVM 运行时的内存不只是堆。每个线程都有自己的虚拟机栈栈里存放栈帧、局部变量表、操作数栈等数据栈大小默认在 1MB 左右不同平台有差异。如果系统线程数非常多比如用线程池无脑创建线程即使堆只用了 30%操作系统内存也可能被栈空间吃光最终表现为 OOM 启动不了或进程被系统杀掉。此外还有直接内存配合 NIO 使用的时候会通过ByteBuffer.allocateDirect()分配堆外内存。直接内存不受堆大小限制但受操作系统整体内存限制。很多人只调-Xmx结果调完进程占用的总内存依然超出容器限制被 OOM Killer 杀掉。所以在调参之前先通过top、free -h和容器配置把整体内存分布看清这一步很多文章不会提但线上排查时非常重要。我把这些基础摆在前面的用意是调优不是玄学每一个参数背后都有对应的问题场景。理解了触发机制你才能明白为什么下一章要反复强调“先量化、后动手”。3. 用数据说话GC 日志怎么开、监控指标怎么读3.1 不同 JDK 版本下打开 GC 日志的参数写法JDK 8 及以前常见的打开方式是这样java -Xloggc:/opt/logs/gc.log -XX:PrintGCDetails -XX:PrintGCDateStamps -jar app.jar-Xloggc指定 GC 日志输出路径PrintGCDetails输出详细回收信息PrintGCDateStamps在每条日志前加可读时间戳。这里我强烈建议加时间戳否则你很难把 GC 时间和业务高峰对上。如果你是 JDK 11 及以上日志参数已经换了统一写法java -Xlog:gc*:file/opt/logs/gc.log:time,uptime,level,tags -jar app.jarJDK 9 开始PrintGCDetails这种老参数被废弃用-Xlog替代。很多网上抄的启动脚本还在用老参数在新的 JDK 版本上实际不生效这个坑你一定要踩过才会记得。另外推荐两个高价值参数-XX:HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath/opt/logs/heapdump.hprof。作用是当 OOM 发生时自动生成堆转储文件。这个参数不占正常运行开销但发生问题后能让你立刻拿到现场数据强烈建议所有服务默认打开。3.2 jstat、VisualVM 和 Arthas 的组合用法日志打开后不要干等它慢慢积累还要主动看实时数据。最常用的命令jstat是 JDK 自带的小工具jstat -gcutil pid 1000 10这条命令每隔 1 秒输出一次内存区域的使用率一共输出 10 次。看输出的E、O两列就够了Eden 区使用率和老年代使用率。如果老年代使用率一直缓慢爬升且 Full GC 后也降不回低位那基本可以判定有对象晋升异常或者内存泄漏的嫌疑。VisualVM 适合本地开发和测试阶段做可视化观察能看到堆各区域曲线、线程状态、CPU 使用。但我个人更推荐线上用 Arthas因为它是命令行工具不需要图形界面在容器环境里也能直接跑。Arthas 里的dashboard命令一行就能看全局内存、GC 次数、线程状态非常直观。想深入看某个类加载或者调用耗时还能用trace、watch命令定位热点的分配压力。3.3 读懂一段 GC 日志背后的信息下面给出一段 JDK 8 环境里的典型日志片段我用注释标出关键信息点[GC (Allocation Failure) [PSYoungGen: 61440K-8192K(61440K)] 102400K-15360K(163840K), 0.5123456 secs] [Times: user0.35 sys0.12, real0.51 secs]第一段GC (Allocation Failure)说明这是一次 Minor GC触发原因是内存分配失败也就是 Eden 区不够了。PSYoungGen: 61440K-8192K(61440K)表示年轻代从 61440K 回收后降到 8192K括号里是年轻代总容量。紧接着的102400K-15360K(163840K)表示整个堆从 102400K 降到 15360K堆总容量 163840K。有经验的工程师会立刻算出两个关键信息这次回收释放了大约 87MB 的内存说明大量短命对象被清理掉但年轻代总容量只有 60MB 且回收后还占 8MB说明有部分对象可能正在向老年代移动。如果你连续看多行这样的日志发现每次 Minor GC 之后整体堆只能升不能降那就要小心老年代的净增长趋势。另外注意Times: user0.35 sys0.12, real0.51 secs。real是实际墙钟时间如果usersys明显大于real说明 GC 过程用了多线程并行回收如果real很大而user很小可能是在等待锁或 IO这种异常在容器环境下尤其值得关注。3.4 从日志里定位问题计算 GC 频率和晋升速率日志不光是拿来“看”的还要会“算”。拿一段时间内的日志做统计重点算三个数据Minor GC 平均间隔、Full GC 平均间隔、老年代平均增长速率。举个例子假设系统每分钟发生 3 次 Minor GC、每 5 分钟发生 1 次 Full GC。每次 Minor GC 之后老年代使用率都涨 1%那 5 分钟就会涨 15% 左右这几乎必然导致 Full GC 跟上来。造成这种局面的常见原因有两个一是新生代设置太小对象来不及在年轻代被回收就晋升二是对象本身晋升阈值有问题本来该被回收的对象因为 Survivor 区放不下被“提前”推到老年代。说到这里顺带回应一下热门搜索词里反复出现的“jvm 内存泄露查看工具”。命令行层面jmap -dump:formatb,file/tmp/heap.hprof pid可以手动导出堆转储图形化或内存分析层面Eclipse MAT 是首选它能自动计算可疑对象、支配树和泄漏报告。后面第五部分我会用实际案例演示这两个工具怎么配合。4. 参数调优的核心动作堆大小、收集器选型与常用参数组合4.1 堆该设多大-Xms、-Xmx 与容器内存的关系堆大小的设定算是参数调优最基础但现在依然有人犯错的地方。先说结论生产环境建议直接把-Xms和-Xmx设置成同一个值避免 JVM 在运行过程中动态扩容。动态扩容虽然省启动时间但扩容过程会触发一次 Full GC而且堆大小忽大忽小也很难让 GC 行为保持稳定。堆大小的上限一般参考物理内存或容器内存的四分之一到二分之一。举个具体例子一台 4C8G 的容器分给 JVM 的堆建议在 3GB 到 4GB 之间。预留的内存要覆盖元空间、线程栈、直接内存、JIT 编译器以及系统自身消耗。如果你把 8G 全给堆操作系统本地内存不足进程可能直接被杀。这里要批评一种网上常见的说法“-Xmx尽量设大反正内存便宜”。内存便宜没有错但堆越大Full GC 的单次停顿时间会越长。除非你用的是 ZGC 这类新一代收集器否则默认的 Parallel GC 在 16GB 堆上 Full GC 停顿达到数秒是家常便饭。合理做法是根据实际压测结果让堆刚好能容纳“日常活跃对象峰值 20%~30% 缓冲”而不是盲目堆大。4.2 新生代比例与阈值比想象中更容易出效果先记住三组常用参数-Xmn指定新生代大小。如果不显式设JVM 默认用NewRatio控制。-XX:NewRatio老年代与新生代的比例。默认2表示老年代是新生代的 2 倍。-XX:SurvivorRatioEden 区与单个 Survivor 区的比例。默认8也就是说 Eden:S0 8:1。举个例子如果一个 JVM 的堆为 2GB新生代通过-Xmn设为 1GB那么 Eden 区约占 800MBS0/S1 各占约 100MB。这个比例下年轻代有足够空间承接短命对象同时 Survivor 区能容纳一定晋升准备。线上调优时我的经验是多用-Xmn而不是依赖默认比例因为Xmn给的值更直接。新生代占总堆的比例通常在 1/3 到 1/2 之间。如果业务对象生命周期短、创建频繁比例可以偏大一些但如果老年代本身就有大量长生命周期对象新生代比例过大会挤压老年代空间结果 Young GC 少了、Full GC 反而更频繁。-XX:MaxTenuringThreshold是另一个容易出效果但容易被忽略的参数默认值为 15。作用是控制对象在年轻代熬过多少轮 Minor GC 后晋升到老年代。如果你通过日志发现每次 Minor GC 后 Survivor 区溢出对象过早晋升可以适当调大这个阈值。但要注意阈值不是越大越好因为 Survivor 区空间有限放不下时依然会提前晋升。4.3 收集器怎么选Parallel、CMS、G1 和 ZGC 的适用边界收集器选型是调优里影响最大也最容易产生争议的部分。我的判断标准可以总结成一张表收集器适用场景核心优势主要缺点Parallel GCJDK 8 默认重视吞吐量的批处理/后台任务高吞吐充分利用多核停顿时间长不适合低延迟场景CMS互联网业务常见于 JDK 7/8 的延迟敏感服务并发标记减少停顿内存碎片化、CPU 消耗高已被淘汰G1JDK 9 默认大堆服务器通用场景可预测停顿目标分区回收调参复杂小堆优势不明显ZGC超大堆、超低延迟场景停顿时间低于 10ms 级别需要较新 JDK对 CPU 内存有一定额外开销如果你还在用 JDK 8 且业务对响应时间敏感-XX:UseG1GC是值得尝试的切换方向。开启 G1 之后可以通过-XX:MaxGCPauseMillis100给 JVM 一个停顿目标暗示。注意是“暗示”不是“保证”G1 会尽力让停顿控制在目标附近但如果堆压力太大目标依然可能无法达成。CMS 在 JDK 9 里已经正式废弃JDK 14 移除了。如果你接手的项目还是 CMS建议尽快升级 JDK 并测试 G1。很多老项目长期用 CMS不是因为它好而是因为没人敢动。实际上 G1 在多数服务器场景下都能做到比 CMS 更稳定的停顿表现。ZGC 我目前只在低延迟核心交易链路上用过效果确实惊艳但需要 JDK 11 以上且指针压缩之类的小参数可能限制大堆的其他优化。如果你不是特别极端的低延迟需求G1 已经足够应对大多数问题。4.4 哪些参数该动、哪些不该动三件套思路很多网上的“调优干货”喜欢列一长串-XX参数甚至有人直接抄别人的启动脚本。我的观点是JVM 参数应该遵循最小化改动原则至少应该把握“停顿时间、吞吐量、资源占用”这三个维度去判断改动是否有效我把这称为调优三件套。改动任何参数之前先问自己三个问题这个参数是直接影响 GC 停顿还是影响吞吐量还是影响内存占用如果一次改动同时动了三个维度后面出问题你根本不知道是哪个参数引起的。真正靠谱的做法是一次只改一个参数跑一轮压测对比日志再改下一个。还要特别注意一类“老旧黑科技”参数比如-XX:UseConcMarkSweepGC搭配-XX:CMSInitiatingOccupancyFraction70这些在 CMS 时代确实有用但在 G1 时代已经不适用。还有一类参数如-XX:UseCompressedOops默认开启且一般建议保持默认手动改动可能导致指针寻址变慢。没有充分理由不要动这些底层优化参数。最后说一下-XX:AlwaysPreTouch。这个参数会让 JVM 启动时把所有堆内存提前向操作系统申请并初始化避免运行时突发分配页面导致的性能毛刺。它的代价是启动时间变长、物理内存立刻占用。对于追求稳定响应的线上服务这个参数通常值得加但如果你在容器环境里跑要确保容器内存资源足够否则直接启动失败。5. 案例从每天几十次 Full GC 到整周无一次这中间都做了什么5.1 问题复现和初始环境参数说回开头提到的订单查询服务。初始环境是 JDK 8、Parallel GC启动参数大致是java -Xms2g -Xmx2g -Xmn512m -XX:MaxTenuringThreshold15 -Xloggc:/opt/logs/gc.log -XX:PrintGCDetails -XX:PrintGCDateStamps -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/opt/logs/heapdump.hprof -jar order-query.jar从参数看不算离谱堆 2GB新生代 512MB。但压测时一打开 GC 日志问题就暴露了Full GC 不到 3 分钟就来一次单次停顿平均 1.8 秒高峰期甚至飙到 4 秒。Minor GC 每秒都有但每次回收后老年代使用率不降反增。5.2 第一轮调整堆参数与 GC 日志复查拿到日志后我没有直接调参而是先算了一组数字每次 Minor GC 前后老年代使用率差多少。结果发现平均每次 Minor GC 老年代增加 6MB。按照一分钟约 30 次 Minor GC 来算老年代每分钟净增 180MB。这可不得了2GB 堆里老年代约 1.5GB几分钟就会被填满。这个增长速率背后只有两种可能要么应用实时创建大量长生命周期对象要么有对象被错误晋升。我先改小风险动作把新生代从512M加大到768M同时把MaxTenuringThreshold从 15 改成 10希望让短命对象有更多机会死在年轻代。改完跑了半小时Full GC 频率确实从 3 分钟一次变成了 8 分钟一次但并没有根治。老年代的净增长速率只是降了一半说明年轻代大小调整缓解了部分晋升压力但根子不在这。这时我判断需要深入看对象分配情况而不是继续在参数上死磕。5.3 第二路排查内存泄漏的定位与修复于是我用jmap -dump:formatb,file/tmp/heap.hprof pid导出了一份堆转储然后用 MAT 打开。MAT 的 Leak Suspects 报告直接列出两个可疑对象集合一个ArrayList实例被某个静态Map持有另一个是线程池里堆积了未处理的请求任务。顺着支配树往下看最终定位到业务代码里一个很隐蔽的问题每次查询订单时都会把查询结果放入一个static MapString, ListOrderDTO的缓存对象里但是这个缓存只负责写入没有设置过期和容量上限。高并发压测时几十个不同的查询参数组合不断产生新的 key缓存里的订单对象越堆越多。这些订单对象都是长生命周期数据直接被老年代承接老年代自然一路飙升。修复方式很简单把这个缓存改成Caffeine本地缓存设置最大容量 1000、写入后过期 60 秒。同时把原先静态 Map 里积压的历史数据在发布时清理掉。代码修复之后重新压测整个 GC 曲线迅速趋于健康。5.4 调优前后的对比数据表三次操作后的结果我用表格对比一下指标调优前调整新生代后修复缓存后Minor GC 频率约 30 次/分钟约 20 次/分钟约 8 次/分钟Full GC 频率约 1 次/3 分钟约 1 次/8 分钟0 次/连续 7 天Full GC 最大停顿4.2 秒2.1 秒0.5 秒以下老年代净增长速率约 180MB/分钟约 80MB/分钟基本持平接口 TP99超过 3 秒900 毫秒80 毫秒看到没有真正让 GC 稳定下来的不是某个神奇参数而是先通过日志量化问题、再逐步验证、最后在代码层面除掉根本原因。参数调优能做的是优化回收效率但内存泄漏这类问题参数只能是“治标不治本”。这次复盘给我的教训非常深调优优先看根因其次才是调参数。6. 顺着“JVM 面试题”再理一遍内存区域、JRE 关系、泄漏与 MySQL 调优的相通点6.1 JDK、JRE、JVM 三者的实际关系这方面的高频面试题看着简单但回答得能不能让面试官满意很考验认知深度。JDK 是 Java 开发工具包包含编译器javac、调试工具、类库和 JREJRE 是 Java 运行环境包含 JVM 和核心类库JVM 是 Java 虚拟机负责把字节码翻译成机器码并执行。回答时最好补一句“跨平台的关键在 JVM”因为同一份字节码在不同平台上由对应版本的 JVM 解释执行所以 Java 程序才能一次编译到处运行。面试官听完这句会觉得你不是在背概念而是理解了这个体系的层次关系。6.2 运行时内存区域的面试标准答法JVM 运行时数据区按是否线程共享可以分成两类。线程共享的是堆和方法区JDK 8 之后叫元空间 Metaspace线程私有的是虚拟机栈、本地方法栈和程序计数器。堆是对象分配的主战场也是 GC 重点关注区域元空间存类元数据、常量池等虚拟机栈管理 Java 方法调用程序计数器记录当前执行字节码的行号也是唯一不会 OOM 的区域。回答时如果能以“各区域分别什么时候会 OOM、哪些和 GC 相关”作为延伸比单纯背列表会好很多。比如栈区域 OOM 往往对应无限递归栈溢出堆 OOM 则可能是内存泄漏或堆不足。6.3 内存泄漏与内存溢出一字之差定位方法完全不同内存泄漏Memory Leak指对象已经不会再使用但 GC 无法回收导致可用内存不断减少内存溢出OutOfMemoryError是可用内存不足导致新对象无法分配。内存泄漏长期积累会导致内存溢出但内存溢出不一定是内存泄漏造成的也可能是单次分配对象过大。定位时如果怀疑是泄漏用jmap导出堆、MAT 找支配树如果怀疑是瞬时流量过大导致临时对象积压直接增大堆或优化对象结构可能更有效。区分方法很简单观察 GC 后老年代是否能回到低位。能回来说明对象被正常回收是容量不足回不来说明有对象被长期持有是泄漏。这里可以顺带提一句热搜词里的“jvm 内存区域”很多人会把线程栈和堆搞混。实际上线程栈存放的是局部变量和栈帧信息对象实例本身始终在堆上。面试或排查时先分清数据在哪一块区域比什么都重要。6.4 和 MySQL 性能调优共通的“指标先行”思路热搜词里还有一个“mysql性能调优”这其实是很有意思的对比。MySQL 调优第一步往往是通过慢查询日志、EXPLAIN找出问题 SQL再针对性加索引或改 SQLJVM 调优第一步则是通过 GC 日志和监控数据找出问题点再针对性调整参数或改代码。两者的思路完全一致数据先行、定位根因、最小化改动、持续验证。很多时候把数据库调优和 JVM 调优放在一起思考反而能帮你更快养成调优的系统性思维。碰到任何性能问题先拉监控再定量分析最后动手改而不是上来直接套模板。这套方法论放之四海而皆准。7. 最后想说的几点实操体会调完上面那个订单服务之后我又陆陆续续调过好几个系统。这里分享几个个人经验不一定适合所有项目但大概率能让你少走弯路。第一任何调优操作都要有回滚方案。改 JVM 参数前先记录当前值压测验证没问题后再推全量。我见过有人线上直接改-Xmx导致 OOM 更频繁最后连旧参数都想不起来整晚都在救火。第二GC 日志不是“出了事才开”。默认开启并做日志轮转比如用-Xloggc配合 Logrotate 或容器 stdout 采集平时几乎零成本出事时却是你离现场最近的数据来源。第三最容易被忽略的是“开机自启脚本里的隐藏参数”。Tomcat 里设置 JVM 参数通常通过catalina.sh里的JAVA_OPTS完成比如常见的JAVA_OPTS-Xms512m -Xmx1024m。如果你在外部加了参数但没拼到JAVA_OPTS上怎么改都不生效。排查这类问题时先确认进程的实际启动命令行用ps -ef | grep java或者jcmd pid VM.flags看一下。第四工具链不要贪多。jstat、jmap、jcmd、Arthas、MAT 这五个配合好已经能覆盖绝大多数线上 GC 排查场景。VisualVM 可以留作本地分析但在生产环境用命令行工具会更轻量更可靠。最后再分享一个小技巧每次调参之后留一份“参数变更记录”在发布文档里标注改了哪些参数、为什么改、压测结果如何。JVM 调优是一个需要长期跟踪数据的过程今天调的参数可能在下一次版本迭代后就失效了。有记录你才能回头审视当时的判断到底对不对。从 GC 频繁到性能稳定这条路没有一劳永逸的参数也没有放之四海皆准的模板。它更像一次次数据、代码、参数之间的反复验证。但只要你肯先从日志和监控入手把根因看清楚稳定往往是水到渠成的事。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。