Featured image of post Java 面试合集:JVM 内存模型与调优

Java 面试合集:JVM 内存模型与调优

JVM 面试合集:运行时数据区、堆内存分代、GC 算法(CMS/G1/ZGC)、类加载机制、OOM 排查、JVM 调优实战

本文写于 2015 年 12 月——JDK 8 普及一年,JDK 9 即将发布(2017-09),G1 正式成为默认 GC。

一、JVM 运行时数据区

1.1 整体架构

1
2
3
4
5
6
7
8
┌──────────────────────────────────────┐
│  PC Register(程序计数器)              │  线程私有
│  Native Method Stack(本地方法栈)      │  线程私有
│  VM Stack(虚拟机栈)                  │  线程私有
│  ─────────────────────────────────── │
│  Heap(堆)                           │  线程共享
│  Method Area(方法区 / Metaspace)    │  线程共享
└──────────────────────────────────────┘

1.2 各区域作用

区域作用线程异常
PC 寄存器当前线程执行的字节码行号私有
VM 栈Java 方法调用栈(栈帧:局部变量表 + 操作数栈 + 动态链接 + 方法出口)私有StackOverflowError / OOM
本地方法栈Native 方法调用栈私有StackOverflowError / OOM
对象实例 + 数组共享OOM
方法区类信息 + 常量 + 静态变量 + JIT 代码共享OOM(PermGen / Metaspace)
运行时常量池字面量 + 符号引用共享OOM
直接内存NIO DirectByteBuffer共享OOM

1.3 面试高频追问

Q1:堆和栈的区别?

维度
存储对象实例局部变量 + 方法调用
线程共享私有
GC
异常OOMStackOverflow
速度

Q2:方法区在 JDK 8 的变化?

  • JDK 7:方法区 = PermGen(永久代)
  • JDK 8+:方法区 = Metaspace(元空间,使用本地内存
  • 字符串常量池:JDK 7 移出 PermGen,JDK 8 仍在堆

二、堆内存分代

2.1 分代模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
┌──────────────────────────────────────┐
│             Young(年轻代)           │
│  ┌────────────┬─────────────┐         │
│  │  Eden      │  Survivor   │         │
│  │  (8/10)    │  (1/10 × 2) │         │
│  └────────────┴─────────────┘         │
├──────────────────────────────────────┤
│            Old(老年代)              │
│             (2/3)                    │
├──────────────────────────────────────┤
│        Metaspace(元空间)             │
│        (本地内存)                     │
└──────────────────────────────────────┘

2.2 对象生命周期

关键参数

1
2
3
4
5
6
7
8
# 年轻代大小
-Xmn2g

# Eden : Survivor
-XX:SurvivorRatio=8

# 对象进入老年代的年龄阈值
-XX:MaxTenuringThreshold=15

2.3 大对象直接进入老年代

避免大对象在 Eden 和 Survivor 之间频繁复制。

1
2
# 大对象阈值
-XX:PretenureSizeThreshold=1m  # 1MB 以上的对象直接进老年代

三、GC 算法

3.1 三大经典算法

算法原理优点缺点
标记-清除标记存活对象,清除未标记简单内存碎片
标记-整理标记存活对象,移到一端无碎片移动成本
复制分两区,存活对象复制到另一区无碎片 + 高效内存利用率 50%

3.2 分代 GC 组合

分代算法原因
年轻代复制对象 90% 死亡,复制量少
老年代标记-整理存活率高,无内存碎片

3.3 主流垃圾收集器

收集器算法目标适用
Serial复制客户端单核 + 小堆
ParNew复制多核配合 CMS
Parallel Scavenge复制吞吐量优先后台计算
CMS标记-清除停顿时间互联网应用(已废弃)
G1分区 + 标记-整理可预测停顿JDK 9+ 默认
ZGC读屏障 + 染色指针亚毫秒停顿JDK 11+ 实验,JDK 15+ 生产
Shenandoah读屏障 + 并发整理亚毫秒停顿OpenJDK

3.4 G1 GC 详解

设计目标:可预测的停顿时间(默认 200ms)

核心思想

  • 堆划分为多个 Region(1MB-32MB)
  • 每个 Region 独立回收
  • 优先回收价值最大的 Region(回收性价比)

关键参数

1
2
3
4
5
6
7
8
# 启用 G1
-XX:+UseG1GC

# 最大停顿时间
-XX:MaxGCPauseMillis=200

# Region 大小
-XX:G1HeapRegionSize=4m

回收阶段

  1. 初始标记(STW):标记 GC Roots
  2. 并发标记:遍历对象图
  3. 最终标记(STW):处理 SATB 记录
  4. 筛选回收(STW):选择 Region 回收集

3.5 ZGC 详解

设计目标:亚毫秒级停顿(< 10ms),支持 TB 级堆

核心技术

  • 读屏障:并发转移对象
  • 染色指针:64 位指针中存储元数据
  • 内存多重映射:将多个虚拟地址映射到同一物理地址

关键参数

1
2
3
4
5
# 启用 ZGC
-XX:+UseZGC

# 并发线程数
-XX:ConcGCThreads=4

四、类加载机制

4.1 类加载 5 阶段

1
加载 → 验证 → 准备 → 解析 → 初始化
阶段动作
加载读字节码到内存,生成 Class 对象
验证文件格式、元数据、字节码、符号引用验证
准备静态变量分配内存 + 默认值(public static int v = 1 → 准备阶段后 v = 0)
解析符号引用替换为直接引用
初始化执行 <clinit> 静态代码块 + 静态变量赋值

4.2 类加载器

四种类加载器

加载器加载路径父加载器
Bootstrap<JAVA_HOME>/lib/rt.jar
Extension<JAVA_HOME>/lib/ext/*.jarBootstrap
ApplicationclasspathExtension
Custom自定义路径Application

4.3 双亲委派模型

1
2
3
4
5
ClassLoader.loadClass("java.lang.String")
   AppClassLoader
     ExtClassLoader
       BootstrapClassLoader  // 找到 rt.jar 中的 String
         返回 Class 对象

为什么需要双亲委派

  • 避免类重复加载
  • 保证 JDK 核心类的安全(防止伪造 java.lang.String

破坏双亲委派的场景

  • Tomcat WebAppClassLoader(不同 WebApp 隔离)
  • OSGi 模块化
  • JDBC SPI 加载(线程上下文类加载器)

五、OOM 排查

5.1 常见 OOM 类型

OOM 类型原因解决
Java heap space内存泄漏 / 堆太小加内存 / 排查泄漏
GC overhead limit exceeded98% 时间 GC,回收 < 2%加内存 / 优化代码
PermGen space类太多 / 静态变量升级 JDK 8+ 用 Metaspace
Metaspace动态生成类过多-XX:MaxMetaspaceSize
Direct buffer memoryNIO DirectByteBuffer-XX:MaxDirectMemorySize
unable to create new native thread线程过多减少线程 / 调 -Xss

5.2 排查步骤

1. 生成堆转储

1
2
3
4
5
6
# OOM 时自动生成
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof

# 主动生成
jmap -dump:format=b,file=heapdump.hprof <pid>

2. 分析工具

  • Eclipse MAT:分析内存泄漏(dominator tree / leak suspects)
  • VisualVM:实时监控堆 / 线程 / GC
  • JProfiler:商业级分析工具
  • async-profiler:火焰图 + 内存分配

5.3 实战案例:内存泄漏

现象:应用运行 3 天后 OOM,重启后恢复

排查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. 看 GC 情况
jstat -gcutil <pid> 1000

# 2. 看堆内存占用
jmap -heap <pid>

# 3. 找最大对象
jmap -histo:live <pid> | head -20

# 4. 生成 heapdump
jmap -dump:format=b,file=heap.hprof <pid>

MAT 分析

  1. Open Heap Dump → Leak Suspects Report
  2. Dominator Tree → 找到占用最大的对象
  3. GC Roots → 找到谁在持有

常见内存泄漏

  • ThreadLocal 没 remove
  • 静态集合不断 add
  • 未关闭的连接(数据库 / HTTP / Socket)
  • 监听器没解注册

六、JVM 调优实战

6.1 调优步骤

6.2 推荐配置(2020+ 生产环境)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 8G 堆典型配置
JAVA_OPTS="
-Xms8g
-Xmx8g
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/var/log/gc-%t.log
"

6.3 调优目标

指标目标
GC 频率Young GC 每分钟 < 5 次,Full GC 0 次
GC 停顿Young GC < 50ms,Full GC < 200ms
吞吐量> 95%
延迟 P99< 200ms

七、写在最后

JVM 面试要点

  1. 基础:运行时数据区、堆分代、GC 算法
  2. 进阶:G1 / ZGC 原理、类加载机制、OOM 排查
  3. 实战:JVM 参数调优、MAT 分析 dump 文件
  4. 前沿:ZGC / Shenandoah / GraalVM

参考资料

使用 Hugo 构建
主题 StackJimmy 设计