Skip to content

JVM 参数调优经验:堆/栈/GC 参数最佳实践

面试官:生产环境的 JVM 参数你怎么配?

这个问题没有标准答案,但面试官想听的是:你有没有真正调过线上 JVM,踩过坑,知道每个参数为什么这么设

直接背参数列表没用,关键是要理解每个参数背后的权衡——吞吐 vs 延迟、内存 vs CPU、易用性 vs 可控性。


一、堆内存参数:稳定压倒一切

1. -Xms-Xmx 务必设成一样

bash
-Xms4g -Xmx4g

理由:JVM 在运行时扩容堆需要向操作系统申请连续内存,这涉及 mmap 系统调用和缺页中断。我们的生产环境实测:动态堆的 GC 停顿时间方差是固定堆的 3-5 倍(从 10ms 波动到 200ms)。原因:

  • 扩容时触发 mmap,可能被 OS 调度打断
  • 缩容时释放内存页,TLB 刷新的开销在 NUMA 架构下尤其明显

JVM 扩容/缩容时序图

时间线 → 
业务线程分配对象 → 堆空闲低于 MinHeapFreeRatio(40%) 
  → JVM 触发 GC(如果 GC 后空闲仍低于阈值)
  → G1CollectedHeap::expand() 
  → os::commit_memory() 
  → mmap(MAP_ANONYMOUS) 系统调用
  → 虚拟内存分配成功,物理页未分配
  → 业务线程访问新分配内存 → Page Fault → 物理页分配
  → 引入不可预期的停顿(100μs-1ms 每页)

面试追问:如果 -Xms 小于 -Xmx,JVM 什么时候会扩容? → 答:下一次 Minor GC 后,如果堆空闲率低于 40%(-XX:MinHeapFreeRatio 默认 40),且当前堆小于 -Xmx,则触发扩容。扩容走 G1CollectedHeap::expand()os::commit_memory()mmap(MAP_ANONYMOUS),整个过程需要 STW 吗?不需要 STW,但 page fault 发生在后续业务线程访问内存时,产生不可预期的停顿。

2. 新生代大小:-Xmn-XX:NewRatio

bash
# 方式一:显式指定新生代大小
-Xmn1.5g

# 方式二:比例控制
-XX:NewRatio=2   # 老年代:新生代 = 2:1,即新生代占 1/3

核心权衡:新生代不是越大越好。我们有个数据服务,4 核 16G 堆,新生代从 6G 降到 4G 后,Minor GC 频率从 3 秒一次升到 2 秒一次,但每次停顿从 80ms 降到 20ms,P99 延迟反而降了 40%。因为新生代越小,每次 GC 需要扫描的存活对象越少。

经验公式:对于 Web 服务,新生代大小 ≈ 每秒对象分配量 × GC 间隔期望(秒)。比如每秒分配 200MB、期望 10 秒一次 Minor GC → 新生代 ≈ 2GB。实际根据 jstat -gcutil 的 YGC 频率反向调整。

如何计算每秒对象分配量(Allocation Rate)

bash
# 方式一:jstat -gcutil 得出 YGCT/YGC 平均每次停顿时间
# 方式二:GC 日志
# [GC pause (G1 Evacuation Pause) (young) 512M->128M(2048M), 0.015s]
# 两次 GC 之间的间隔 = 15s → 每秒分配量 = (512M - 128M) / 15s ≈ 25MB/s
# 方式三:jstat -gc <pid> 1000
# EU(Eden Used)的变化量 / 时间间隔

3. 元空间:不要忘了设上限

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

JDK 8 元空间默认无上限(只受本地内存限制),这在 Spring Boot 热部署、CGLIB 动态代理频繁的场景下,元空间膨胀是 OOM 的常见原因之一

MetaspaceSize 不是初始大小,是触发 GC 的阈值。 设为 256m 的意思是:当元空间使用量达到 256m 时,触发一次 Full GC 检查。如果还继续增长到 MaxMetaspaceSize,则 OOM。

实际案例:某团队用 Spring Cloud Gateway + 动态路由,每次更新路由规则通过 CGLIB 生成新类,一周后元空间膨胀到 1.2GB,触发了容器 OOMKill。加 -XX:MaxMetaspaceSize=256m 后,Full GC 触发频率从 0 次/天变为 5-10 次/天,但每次只有几十毫秒,代价可接受。

排查元空间泄漏的步骤

bash
# 1. 监控元空间使用量
jstat -gc <pid> 5000 | awk '{print $11}'

# 2. 开启类加载日志
-XX:+TraceClassLoading -XX:+TraceClassUnloading

# 3. 导出类加载历史
jcmd <pid> GC.class_stats

4. 预分配物理内存

bash
-XX:+AlwaysPreTouch

启动时一次性分配物理内存,避免运行时因缺页中断(Page Fault)导致 GC 停顿。代价:启动时间增加 30-60 秒(16G 堆约 30 秒,64G 堆约 120 秒),而且容器会立刻占用指定内存,导致 Kubernetes 调度时的内存压力上升。

什么时候不加:容器内存敏感、快速扩缩容的 Serverless 场景。缺页中断的代价在大多数场景下远小于启动时间。

PreTouch 与容器化冲突的真实案例:某团队在 Kubernetes 上用 4 副本、每副本 8G 堆的服务,开了 AlwaysPreTouch 后,Node 节点内存立刻被占满,Pod 调度一直 Pending。排查发现 --memory request 设的是 8G,但 Node 预留内存不够同时给 4 个 Pod 分配物理内存。解决方案:要么关 PreTouch,要么增加 Node 资源或减副本数。


二、GC 选型详解:不只是速查表

选型速查表

堆大小JDK 版本推荐 GC吞吐量典型停顿适用场景
< 4GB8+Parallel最高(99%+)100-500ms批处理、离线计算
4-64GB11+G1高(98-99%)50-200ms常规 Web 服务
> 64GB17+ZGC中(95-98%)<10ms低延迟场景、大内存服务

Parallel GC:被低估的王者

很多人面 Spring Boot 就直接上 G1,但 4G 以下堆 Parallel 的吞吐比 G1 高 5-10%。原因:

  • Parallel GC 的 Young GC 是完全并行的,所有线程一起做标记-复制
  • G1 的 Young GC 虽然也是并行,但多了 Region 级别的扫描和 Remembered Set 维护,有额外开销

实测数据:4G 堆、QPS 5000 的 Spring Boot 服务,Parallel GC 的 CPU 消耗比 G1 低 8%,GC 总时间减少 12%。

Parallel GC 的吞吐量公式

吞吐量 = 应用运行时间 / (应用运行时间 + GC 总暂停时间)
  • -XX:GCTimeRatio=19 → 目标 GC 时间 ≤ 总时间的 5%(1/(1+19))
  • -XX:MaxGCPauseMillis=200 → 目标停顿 ≤ 200ms(Parallel 对此参数做近似,不保证)
  • 注意:Parallel 的 MaxGCPauseMillis 是启发式的,不是硬约束。G1 才是硬约束。

G1 核心参数详解

bash
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200          # 目标停顿时间
-XX:ParallelGCThreads=8            # 并行线程数,约为 CPU 核数的 5/8
-XX:ConcGCThreads=2                # 并发线程数,为 ParallelGCThreads 的 1/4
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发标记的堆占用阈值

G1 的 Region 设计:堆被分成 1MB-32MB 的 Region(根据堆大小自动计算,公式 region_size = 1MB << log2(heap_size / 2048))。每个 Region 被标记为 E(Eden)、S(Survivor)、O(Old)或 H(Humongous,大对象)。G1 的 Young GC 只回收 Eden Region,Mixed GC 回收 Old Region 和 Eden Region 一起,这就是 G1 不需要像 CMS 那样做 Full GC 的原因。

G1 Region 分配示意图

堆内存(16GB,Region Size = 4MB)
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ E  │ E  │ E  │ E  │ E  │ S  │ O  │ O  │ O  │ O  │ O  │ O  │ H  │ H  │ H  │ H  │
│    │    │    │    │    │    │    │    │    │    │    │    │(3M)│(3M)│(3M)│(3M)│
│    │    │    │    │    │    │    │    │    │    │    │    │    │    │    │    │
│    │    │    │    │    │    │    │    │    │    │    │    │    │    │    │    │
│    │    │    │    │    │    │    │    │    │    │    │    │    │    │    │    │
│    │    │    │    │    │    │    │    │    │    │    │    │    │    │    │    │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
  #0   #1   #2   #3   #4   #5   #6   #7   #8   #9   #10  #11  #12  #13  #14  #15
  Eden Eden Eden Eden Eden Surv Old  Old  Old  Old  Old  Old  Hum  Hum  Hum  Hum

IHOP(-XX:InitiatingHeapOccupancyPercent 是 G1 调优最关键的参数。默认 45% 意味着整个堆占用达到 45% 时启动并发标记周期。

G1 的并发标记流程(含时序)

时间 ──────────────────────────────────────────────────────────────►
       │  Initial Mark (STW)  │  Concurrent Mark  │  Remark (STW)  │  Cleanup (STW)  │
       │  GC Roots 标记       │  遍历堆标记存活   │  处理引用变更   │  计算 Region    │
       │  停顿: 5-10ms        │  耗时: 1-3s      │  停顿: 10-50ms  │  停顿: 1-5ms    │
       │                      │  (并发,不 STW)   │                 │                 │
       └──────────────────────┴───────────────────┴─────────────────┴─────────────────┘
                               在此期间业务线程继续分配对象到老年代
                               如果老年代满了 → Concurrent Mode Failure → Full GC

IHOP 调高 vs 调低的影响

  • 调低(如 35%):更早开始并发标记,GC 更频繁,CPU 开销大,但能避免 Concurrent Mode Failure
  • 调高(如 55%):推迟并发标记,但如果标记周期内老年代被填满,退化为 Full GC,停顿秒级

判断方法:看 GC 日志中的 initial-mark 触发时老年代占用。如果 initial-mark 触发时老年代已占 70%+,说明 IHOP 太低(标记周期还没完成老年代就满了);如果 initial-mark 频繁触发但老年代始终不到 50%,说明 IHOP 太高,太早触发标记浪费 CPU。

ZGC:彩色指针和染色指针

bash
-XX:+UseZGC -Xms16g -Xmx16g -XX:ConcGCThreads=4

ZGC 的核心创新是染色指针(Colored Pointers):在 64 位指针的高位借 4 bit 做状态标记(Finalizable、Remapped、Marked0、Marked1),GC 过程中通过指针着色来标记可达性,不需要对象头里的标记位,所以不需要 STW 去刷标记位。

64 位染色指针 bit 布局

63  62  61  60  59-48  47-0
┌───┬───┬───┬───┬──────┬──────────────────────────────────────┐
│ F │ R │ M0│ M1│ 保留  │              地址偏移               │
└───┴───┴───┴───┴──────┴──────────────────────────────────────┘
  │   │   │   │
  │   │   │   └─ Marked1:标记位 1
  │   │   └───── Marked0:标记位 0
  │   └───────── Remapped:已重映射
  └───────────── Finalizable:终结器引用

ZGC 的并发压缩:ZGC 在并发阶段做内存压缩,通过 remap 指针来更新对象引用。这要求指针在并发访问时是原子的——64 位机器上指针读写本来就是原子的,所以 ZGC 不需要像 G1 那样用 Remembered Set 来跟踪跨 Region 引用。

ZGC 并发标记流程

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  Pause   │    │ Concurrent│    │  Pause   │    │ Concurrent│    │  Pause   │
│ Mark Start│───►│ Mark      │───►│ Mark End │───►│ Relocate  │───►│  Relocate │
│ (STW)    │    │ (并发)    │    │ (STW)    │    │ (并发)    │    │  End(STW)│
│ <1ms     │    │           │    │ <1ms     │    │           │    │ <1ms     │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘

代价:ZGC 的吞吐比 G1 低 3-5%(因为并发标记和 remap 的 CPU 开销),且内存占用更高(多颜色指针需要额外内存对齐)。只有延迟敏感且堆 > 64GB 的场景才值得上 ZGC


三、日志参数:线上必加

bash
# JDK 9+
-Xlog:gc*:file=gc.log:time,uptime,level,tags

# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

# 必加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof

HeapDumpOnOutOfMemoryError 是救命参数——OOM 时自动 dump 堆,事后分析泄漏根因,比 OOM 后手动 jmap 靠谱得多。

GC 日志怎么读:用 gceasy.io 上传 gc.log,它会自动生成:

  • 吞吐量:应用运行时间 / (应用运行时间 + GC 总时间)
  • Pause Duration:每次 GC 停顿的分布(P50/P95/P99/Max)
  • Concurrent Mode Failure:如果出现,说明 IHOP 或堆大小不合理
  • Object Allocation Rate:每秒分配量,用于判断新生代大小是否合理

如何手动解析 GC 日志(一条 G1 Young GC 日志拆解)

log
[GC pause (G1 Evacuation Pause) (young) 512M->128M(4096M), 0.025s]
[GC pause (G1 Evacuation Pause) (young) 640M->160M(4096M), 0.030s]
  • 512M → 128M:GC 前堆使用 512MB,GC 后 128MB,回收了 384MB
  • (4096M):总堆大小
  • 0.025s:停顿时间 25ms
  • 对 512M 和 640M 做差:每次 GC 前堆使用量在增加,说明分配速率 > GC 回收速率
  • 如果连续几次 GC 后堆使用量持续上升,说明老年代在被填满

GC 日志中的危险信号

log
# 危险信号 1:Concurrent Mode Failure
[Full GC (Allocation Failure) 15.8G->1.2G(16G), 8.5s]

# 危险信号 2:To-space overflow(G1 特有)
[GC pause (G1 Evacuation Pause) (young) (to-space overflow) 6.8G->6.5G(16G), 0.5s]
# 说明 Survivor 区放不下晋升对象,部分对象直接进入老年代

# 危险信号 3:Humongous Allocation 频繁触发 concurrent mark
[GC pause (G1 Humongous Allocation) (young) (initial-mark) 7.2G->3.5G(16G), 0.3s]
# 频繁出现 → 大对象分配有问题

四、调优案例:G1 的 IHOP 调优实战

场景

某支付服务,堆 16GB,JDK 11 + G1。GC 日志显示:

[GC pause (G1 Evacuation Pause) (young) 500M->200M(16G), 0.015s]
[GC pause (G1 Evacuation Pause) (young) 600M->250M(16G), 0.018s]
...
[GC pause (G1 Humongous Allocation) (young) (initial-mark) 7.2G->3.5G(16G), 0.3s]
[GC concurrent-root-region-scan-start]
[GC concurrent-mark-start]
...
[Full GC (Allocation Failure) 15.8G->1.2G(16G), 8.5s]

问题:Full GC 出现了,停顿 8.5 秒,服务超时率 100%。

分析:IHOP 默认 45%,但老年代在并发标记周期还没完成时就被填满了,触发 Concurrent Mode Failure,退化为 Serial Old 做 Full GC。

详细排查

  1. 看 GC 日志中的 initial-mark 触发时老年代大小:7.2GB 堆占用,16GB 堆 - 新生代占 6GB → 老年代约 10GB。7.2GB 中老年代贡献约 7.2 - 6 = 1.2GB(Eden 在 initial-mark 前已有一部分被回收)→ 实际老年代占用约 72%
  2. 并发标记周期持续约 2 秒,这 2 秒内业务分配了约 400MB 对象,其中大部分晋升到老年代
  3. 老年代从 72% 升到 85%+,触发 Concurrent Mode Failure

调优

bash
-XX:InitiatingHeapOccupancyPercent=35
-XX:G1ReservePercent=15   # 预留 15% 的堆空间,给浮动垃圾用

为什么降 IHOP 而不是扩堆:这台机器只有 16GB 可用,扩堆要动容器规格。而且问题的核心是并发标记周期太短,没来得及完成。降低 IHOP 让标记更早开始,标记周期在 2 秒内完成,老年代还没满就结束了。

结果:Full GC 消失,最大停顿从 8.5s 降到 120ms,P99 延迟从 5s 降到 200ms。


五、G1 和 CMS 的对比:为什么 G1 取代了 CMS

维度CMSG1
堆布局连续分代不连续 Region(1-32MB)
并发标记标记-清除标记-复制(Mixed GC)
内存碎片严重,最终需要 Full GC 整理无碎片,Region 复制自动整理
停顿可预测性差(CMS 的 Remark 阶段不可控)好(通过 MaxGCPauseMillis 控制)
大对象处理直接进老年代通过 Humongous Region 管理
JDK 支持JDK 9 被标记为 Deprecated,JDK 14 移除默认 GC(JDK 9+)

CMS 的致命问题:CMS 的 Remark 阶段需要扫描整个老年代,如果老年代中有大量跨代引用,Remark 停顿会从几十毫秒飙升到几秒。G1 通过 Remembered Set 精确跟踪跨 Region 引用,Remark 只扫描变更的 Region,停顿时间与堆大小无关

CMS 的碎片化演进过程

时间线 →
初始状态:老年代连续空间
│ 分配 A(2MB) B(3MB) C(1MB) D(4MB) E(2MB)
│ [A][B][C][D][E]
│ GC 回收 B、D
│ [A][  ][C][    ][E]  ← 出现碎片
│ 分配 F(4MB) — 需要连续 4MB 但找不到 → Full GC
│ [A][C][E][F]  ← Full GC 整理后
│ 循环往复,碎片越来越多

G1 通过 Region 复制规避了这个问题:不管对象在原 Region 中如何分布,Mixed GC 直接把存活对象复制到另一个 Region,永远不产生碎片


六、实战红线:不要抄参数模板

网上流传的"JVM 参数模板"大多是给测试环境用的,直接搬到生产上会出事:

bash
# ❌ 不要无脑抄这个
-Xms4g -Xmx4g -Xmn2g -XX:+UseG1GC -XX:MaxGCPauseMillis=50

问题

  • 新生代 2g 太大了,老年代只剩 2g,大对象直接进老年代,老年代很快满
  • MaxGCPauseMillis=50 太激进,G1 为了满足 50ms 目标,每次只回收很少的 Region,反而导致 GC 频率上升
  • 没有 IHOP、日志、HeapDump 参数

正确做法:每次只改一个参数,通过 A/B 对比验证。工具推荐:

  • GC 日志分析gceasy.ioGCViewer
  • 实时监控:Prometheus + Grafana(jvm_memory_used_bytesjvm_gc_pause_seconds
  • 在线诊断:Arthas 的 dashboardvmtool

一个完整的生产级 JVM 参数模板(16G 堆,G1)

bash
# 堆内存
-Xms16g -Xmx16g
-XX:MaxMetaspaceSize=256m

# GC 选型
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=35
-XX:G1ReservePercent=15
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2

# 日志
-Xlog:gc*:file=gc-%t.log:time,uptime,level,tags:filecount=5,filesize=50m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/app/dumps

# 其他
-XX:+AlwaysPreTouch
-XX:+DisableExplicitGC
-XX:+PrintCommandLineFlags
-XX:+ExitOnOutOfMemoryError

参数说明

  • -XX:+DisableExplicitGC:屏蔽 System.gc(),防止 RMI 或 NIO 框架触发 Full GC
  • -XX:+ExitOnOutOfMemoryError:OOM 时直接退出,让 Kubernetes 重启 Pod,避免僵尸进程
  • -XX:+PrintCommandLineFlags:启动时打印所有被修改的 JVM 参数,方便排查

七、面试高频追问

追问 1:-Xss 设置多大合适?

默认 1MB(JDK 11+ Linux x64)。线程栈深度取决于递归深度和方法栈帧大小。对于大多数 Web 服务,256KB 就够,64KB 的 netty worker 线程都没问题。

经验公式线程数 × 栈大小 ≤ 容器内存 × 20%。比如 4GB 容器、200 个线程,每个线程栈 1MB 就占 200MB,如果容器还有堆 3GB + 元空间 256MB,留给线程栈的空间不足。这时应该把 -Xss256k 改小。

如何测试栈深度

java
// 测试当前栈大小下的最大递归深度
public class StackDepthTest {
    private int depth = 0;
    
    public void recursiveCall() {
        depth++;
        recursiveCall();
    }
    
    public static void main(String[] args) {
        StackDepthTest test = new StackDepthTest();
        try {
            test.recursiveCall();
        } catch (StackOverflowError e) {
            System.out.println("Max depth: " + test.depth);
        }
    }
}
// -Xss256k → 深度约 2000
// -Xss1m   → 深度约 8000
// -Xss2m   → 深度约 16000

追问 2:G1 的 Humongous 对象分配有什么坑?

G1 把超过 Region 大小 50% 的对象当作 Humongous,直接分配在连续的 Humongous Region(Old 区)。Humongous 分配不会触发 Young GC,而是直接触发 concurrent mark

实战踩坑:某服务用 byte[8MB] 做缓存,8MB 在 16GB 堆中 Region 大小是 4MB,8MB 超过 50% → 都是 Humongous。每次分配都触发 concurrent mark,CPU 增加了 20%。解决方案:把大对象拆成 1MB 的 chunk,放回 Eden 正常分配。

代码层面的解决方案

java
// ❌ 踩坑:8MB byte[] 触发 Humongous 分配
byte[] cache = new byte[8 * 1024 * 1024];

// ✅ 正确:拆分成 1MB 的 chunk,Eden 正常分配
byte[][] cacheChunks = new byte[8][];
for (int i = 0; i < 8; i++) {
    cacheChunks[i] = new byte[1024 * 1024];
}

// 或者用 DirectBuffer 直接分配堆外内存
ByteBuffer buffer = ByteBuffer.allocateDirect(8 * 1024 * 1024);

Humongous 的另一个坑:Humongous Region 不会在 Young GC 中被回收,只在 concurrent mark 中被回收。如果 Humongous 对象是短命的,就浪费了很大空间。短命大对象 → 直接 OOM 的常见原因

追问 3:-XX:+UseAdaptiveSizePolicy 要不要开?

Parallel GC 默认开启,G1 默认关闭。建议生产环境关闭。自适应策略会动态调整新生代大小,虽然理论上能自动适应负载,但实际观测到:负载波动时,AdaptiveSizePolicy 调整滞后,导致新生代大小频繁震荡,GC 停顿时间方差变大。

AdaptiveSizePolicy 的副作用案例:某定时任务服务,每天凌晨 3 点跑批处理,白天空闲。开启 AdaptiveSizePolicy 后,白天新生代缩到 500MB,凌晨 3 点任务启动时分配量激增,新生代还来不及扩容,频繁 Minor GC,任务延迟从 5 分钟膨胀到 30 分钟。关闭后固定新生代大小,问题消失

追问 4:-XX:+UseCompressedOops-XX:+UseCompressedClassPointers 什么时候失效?

这两个默认开启(JDK 8+),在堆 ≤ 32GB 时有效。堆超过 32GB 时,压缩指针失效,每个对象引用从 4 字节回归 8 字节,内存占用增加约 15-20%。

bash
# 堆 32GB 时,压缩指针开启
-Xms32g -Xmx32g  # 压缩指针有效

# 堆 33GB 时,压缩指针自动关闭
-Xms33g -Xmx33g  # 压缩指针失效,内存占用增加 15%

所以堆大小有一道"暗线":32GB 以下用压缩指针,32GB 以上膨胀。如果你需要 35GB 有效堆,建议设 32GB 然后用堆外内存补,或者直接上 64GB 堆并接受 15% 的额外开销。

追问 5:G1 的 -XX:G1HeapRegionSize 怎么设?

默认不设,JVM 自动计算。但手动设置可以控制 Humongous 阈值:

bash
# 16G 堆,自动 Region Size = 4MB
# Humongous 阈值 = 4MB * 50% = 2MB
# 超过 2MB 的对象都走 Humongous 分配

# 手动设 8MB
-XX:G1HeapRegionSize=8m
# Humongous 阈值 = 8MB * 50% = 4MB
# 大对象进 Humongous 的门槛提高了

什么时候手动设:如果业务中有大量 3-4MB 的对象,建议把 Region Size 调大,让它们走正常分配而不是 Humongous。但 Region Size 越大,G1 的并行粒度越粗,停顿可能变长。


八、总结

JVM 参数调优的核心原则就一句话:先监控,再调优,不盲目

  • 堆内存-Xms = -Xmx,元空间设上限,必加 HeapDumpOnOutOfMemoryError
  • GC 选型:小堆用 Parallel,中堆用 G1,大堆用 ZGC,不要无脑 G1
  • G1 核心:IHOP 是调优的关键,看 GC 日志判断高低,Humongous 对象要小心
  • 日志:GC 日志 + HeapDump 是线上故障排查的起点
  • 红线:不要复制网上参数模板,每次只改一个参数,用 A/B 验证

面试官如果追问"遇到过什么调优案例",把上面那个支付服务的 IHOP 调优说清楚,再补一个 Humongous 对象的坑,比背十个参数名更有说服力。如果面试官问起 JVM 参数调优的盲区,可以提压缩指针的 32GB 暗线和 -XX:+DisableExplicitGC 的必要性,这两个点能展示你真的踩过生产坑。

最后的建议:把本节生成的完整参数模板保存到你的项目仓库里,每次新服务上线直接复制改堆大小和 IHOP 即可。但记得每次只改一个参数,上线后观察 GC 日志一周再调下一个参数。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。