JVM 原理与调优
JVM 原理与调优
JVM 内存模型、GC 算法、G1/ZGC/Shenandoah 对比、JVM 调优实战、OOM 排查——阿里 P6/P7、美团必考核心模块。
核心原理
JVM 内存模型(JDK 8+)
┌──────────────────────────────────────────────────────┐
│ JVM 内存 │
│ ┌────────────────────────────────────────────────┐ │
│ │ 堆(Heap) │ │
│ │ ┌───────────────────┐ ┌────────────────────┐ │ │
│ │ │ 新生代(Young) │ │ 老年代(Old) │ │ │
│ │ │ Eden:S0:S1=8:1:1 │ │ 长期存活对象 │ │ │
│ │ └───────────────────┘ └────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ ┌────────────────┐ ┌───────────────────────────┐ │
│ │ 方法区/元空间 │ │ JVM 栈(每线程一个) │ │
│ │ 类信息/常量池 │ │ 栈帧(局部变量/操作数) │ │
│ └────────────────┘ └───────────────────────────┘ │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ 本地方法栈 │ │ 程序计数器(每线程私有) │ │
│ └──────────────┘ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────┘下图展示了 JVM 各内存区域之间的层次关系与职责划分:
JDK 8 关键变化: 永久代(PermGen)移除,改为元空间(Metaspace),使用本地内存(Native Memory),不再受 JVM 堆大小限制,默认无上限(可用 -XX:MaxMetaspaceSize 限制)。
各区域 OOM 类型:
| 内存区域 | OOM 类型 | 常见原因 |
|---|---|---|
| 堆 | java.lang.OutOfMemoryError: Java heap space | 对象过多、内存泄漏、集合无限增长 |
| 元空间 | java.lang.OutOfMemoryError: Metaspace | 动态生成大量类(CGLib、Groovy脚本) |
| 栈 | java.lang.StackOverflowError | 无终止条件递归 |
| 直接内存 | java.lang.OutOfMemoryError: Direct buffer memory | NIO DirectByteBuffer 未释放 |
GC 算法原理
标记-清除(Mark-Sweep): 标记存活对象,清除未标记对象。优点:实现简单;缺点:内存碎片。
复制算法(Copying): 将存活对象复制到另一半空间,清空原空间。优点:无碎片,速度快;缺点:内存利用率仅 50%。新生代 Eden+Survivor 使用此算法(8:1:1 比例,有效利用率 90%)。
标记-整理(Mark-Compact): 标记存活对象后将其移动到内存一端,清除边界外空间。无碎片但移动对象代价高,适合老年代。
分代收集: 新生代用复制(Minor GC),老年代用标记整理(Major GC/Full GC)。
对象晋升老年代的条件
- 年龄达到
-XX:MaxTenuringThreshold(默认 15) - Survivor 区中相同年龄所有对象大小超过 Survivor 空间的 50%(动态年龄判断)
- 大对象直接进入老年代(
-XX:PretenureSizeThreshold) - Minor GC 后 Survivor 放不下,直接进老年代
下图以时序图展示了对象从 Young GC 到晋升 Old 区的完整过程:
高频面试题
Q: G1、ZGC、Shenandoah 如何选择?(阿里 P7 必问)
G1(Garbage First,JDK 9 默认 GC):
- 将堆划分为等大小的 Region(默认 2048 个),动态分配 Eden/Survivor/Old/Humongous
- 优先回收垃圾最多的 Region(Garbage First 名称由来)
- 停顿目标:
-XX:MaxGCPauseMillis=200(默认 200ms)- 适用:堆 4GB ~ 32GB,对停顿有一定要求的通用场景
ZGC(JDK 15 正式,JDK 21 分代 ZGC):
- 停顿时间 < 1ms(与堆大小无关!)
- 基于着色指针(Colored Pointers)和读屏障(Load Barrier)实现并发整理
- JDK 21 引入分代 ZGC(
-XX:+UseZGC -XX:+ZGenerational),吞吐量大幅提升- 适用:超大堆(TB 级),对延迟极度敏感的场景(实时交易、游戏)
Shenandoah(RedHat 研发,JDK 12+ 实验,JDK 17 正式):
- 与 ZGC 类似,基于Brooks Pointer(转发指针)实现并发整理
- 停顿时间 < 10ms
- 吞吐量略低于 ZGC,但 JDK 版本要求较低
- 适用:中大堆,JDK 12-16 环境下替代 ZGC
| 收集器 | STW 停顿 | 吞吐量 | 堆大小 | JDK 版本 | 推荐场景 |
|---|---|---|---|---|---|
| G1 | 100-200ms | 高 | 4G-32G | 9+ | 通用生产环境 |
| ZGC | < 1ms | 中 | 8G-TB | 15+ | 低延迟、超大堆 |
| Shenandoah | < 10ms | 中 | 4G+ | 17+ | 低延迟、JDK17+ |
下图对比了三款低停顿收集器在关键维度上的选择决策路径:
Q: 如何排查 OOM 问题?(美团必问)
排查步骤:
Step 1:快速定位 OOM 类型
# 查看 GC 统计,观察 FGC 频率和老年代使用率 jstat -gc <PID> 1000 10 # 输出:S0C S1C S0U S1U EC EU OC OU MC MU ... YGC YGCT FGC FGCT GCTStep 2:查看存活对象分布(快速,不 Dump)
jmap -histo:live <PID> | head -30 # 输出:num / instances / bytes / class name # 找出实例数或字节数异常多的类Step 3:导出堆快照(详细分析)
jmap -dump:format=b,file=/tmp/heap.hprof <PID> # 或者 JVM 启动参数自动触发: # -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprofStep 4:用 MAT(Eclipse Memory Analyzer)分析
- Dominator Tree:找出占用内存最大的对象树
- Leak Suspects Report:MAT 自动分析内存泄漏嫌疑点
- Retained Heap:某对象被 GC 后能释放的总内存量
常见 OOM 原因:
// 1. 集合无限增长(最常见) static Map<String, Object> cache = new HashMap<>(); // 解决:改用 Caffeine/Guava Cache 并设置大小上限 // 2. ThreadLocal 未 remove(内存泄漏) ThreadLocal<byte[]> tl = new ThreadLocal<>(); tl.set(new byte[1024 * 1024]); // 必须在 finally 中 tl.remove() // 3. 大量动态生成类(元空间 OOM) // 解决:-XX:MaxMetaspaceSize=256m 并检查 CGLib 代理是否缓存
Q: JVM 调优常用参数有哪些?(字节)
堆内存设置(最基础):
-Xms4g # 初始堆大小(建议与 Xmx 相等,避免动态扩缩) -Xmx4g # 最大堆大小 -Xmn1g # 新生代大小(G1 下不建议设置) -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512mGC 选择:
# G1(推荐,JDK 9+ 默认) -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16m # Region 大小,1-32M,2的幂 # ZGC(JDK 21,低延迟) -XX:+UseZGC -XX:+ZGenerational # GC 日志(生产必备) -Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=5,filesize=20mOOM 时自动 Dump:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof -XX:+ExitOnOutOfMemoryError # OOM 时立即退出,让 K8s 重启 Pod
Q: Minor GC、Major GC、Full GC 的区别?触发条件是什么?(腾讯)
- Minor GC(新生代 GC): Eden 区满时触发,使用复制算法,速度快(毫秒级),STW 短
- Major GC(老年代 GC): 老年代空间不足时触发,使用标记整理,速度较慢
- Full GC: 整个堆(包括新生代、老年代、元空间)的 GC,耗时最长,尽量避免
Full GC 触发条件:
- 调用
System.gc()(仅建议,不强制)- 老年代空间不足(晋升失败)
- 元空间内存不足
- 空间担保失败(新生代 Minor GC 前检查老年代剩余空间)
- Concurrent Mode Failure(CMS 特有,G1/ZGC 已解决)
Q: class 文件的加载过程?双亲委派模型是什么?如何破坏?(阿里)
类加载生命周期: 加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
双亲委派模型: 类加载器收到加载请求时,优先委托父加载器处理,只有父加载器无法完成时才自己加载。
- BootstrapClassLoader(核心 JDK)→ PlatformClassLoader(扩展模块)→ AppClassLoader(应用类)
作用: 保证核心类(如
java.lang.Object)只被 BootstrapClassLoader 加载一次,避免类重复加载和安全漏洞。破坏双亲委派的场景:
SPI 机制(JDBC Driver):BootstrapClassLoader 加载java.sql.Driver接口,但实现在 classpath,通过 Thread Context ClassLoader 绕过委派- Tomcat:每个 Web 应用有独立的
WebAppClassLoader,优先加载应用自身的类(Web 应用隔离)- OSGi/模块热部署:自定义加载顺序,支持动态替换类
知识星球深度内容
完整大厂面经实录(字节/阿里/腾讯/美团)、简历 1v1 修改、每周高频题精讲,扫码加入「AI 工程师加速社区」知识星球 👉 立即加入
