Java 开发者可以不懂 JVM 写代码,但遇到内存溢出、性能瓶颈时,不懂 JVM 就无从下手。这篇文章从 JVM 内存结构讲起,到 GC 调优和问题排查,覆盖日常开发中最常见的场景。
一、JVM 内存结构
JVM 将内存分为以下几个区域:
┌─────────────────────────────────┐
│ 方法区(Metaspace) │ ← 类信息、常量、静态变量
├─────────────────────────────────┤
│ 堆(Heap) │ ← 对象实例(GC 主要区域)
│ ┌───────┬───────┬────────────┐ │
│ │ 新生代 │ 老年代 │ │ │
│ │ Eden │ S0/S1 │ Old Gen │ │
│ └───────┴───────┴────────────┘ │
├─────────────────────────────────┤
│ 虚拟机栈(VM Stack) │ ← 每个方法对应一个栈帧
├─────────────────────────────────┤
│ 本地方法栈(Native Stack) │ ← native 方法
├─────────────────────────────────┤
│ 程序计数器(PC Register) │ ← 线程私有
└─────────────────────────────────┘
| 区域 | 存储内容 | 是否 GC | 线程私有? |
|---|---|---|---|
| 堆(Heap) | 对象实例、数组 | ✅ 是 | ❌ 共享 |
| 方法区(Metaspace) | 类信息、常量 | ✅ 是 | ❌ 共享 |
| 虚拟机栈 | 局部变量、方法调用 | ❌ 否 | ✅ 私有 |
| 本地方法栈 | native 方法 | ❌ 否 | ✅ 私有 |
| 程序计数器 | 执行地址 | ❌ 否 | ✅ 私有 |
二、堆内存详解
堆是 JVM 管理中最大的一块内存,也是 GC 的主要战场。
# 年轻代(Young Gen):占堆的 ~1/3
# - Eden:新对象分配的地方
# - Survivor 0 / Survivor 1:GC 后的存活对象
# 老年代(Old Gen):占堆的 ~2/3
# - 长期存活的对象
对象分配流程
新对象 → Eden 区
↓ (Minor GC)
Eden 满 → 存活对象移入 S0
↓ (下次 Minor GC)
S0 满 → 存活对象移入 S1(年龄+1)
↓ (年龄达到阈值,默认15)
移入老年代
三、GC 算法与垃圾回收器
1. 常用 GC 算法
| 算法 | 原理 | 适用 |
|---|---|---|
| 标记-清除 | 标记存活对象,清除未标记的 | 老年代 |
| 标记-复制 | 将存活对象复制到另一块区域 | 新生代 |
| 标记-整理 | 标记存活对象,整理到一端 | 老年代 |
2. JVM 默认垃圾回收器
| Java 版本 | 默认回收器 |
|---|---|
| Java 8 | Parallel GC |
| Java 11 | G1 GC |
| Java 17+ | ZGC |
# 查看当前 JVM 默认 GC
java -XX:+PrintCommandLineFlags -version
# 手动指定 GC
-XX:+UseParallelGC # 并行 GC
-XX:+UseG1GC # G1 GC
-XX:+UseZGC # ZGC(JDK 17+)
四、JVM 参数配置实战
1. 堆内存设置
# 典型配置(4核8G服务器)
java -Xms2g -Xmx2g \ # 堆初始/最大 2GB
-Xmn512m \ # 新生代 512MB
-XX:MetaspaceSize=256m \ # 元空间
-XX:MaxMetaspaceSize=256m \
-jar app.jar
| 参数 | 说明 | 建议 |
|---|---|---|
-Xms | 堆初始大小 | 设为与 -Xmx 相同,避免动态调整 |
-Xmx | 堆最大大小 | 不超过服务器内存的 70% |
-Xmn | 新生代大小 | 堆的 1/3 ~ 1/4 |
-XX:MetaspaceSize | 元空间初始 | 256MB 起 |
2. GC 日志
# 打印 GC 日志(必备)
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=10M
3. 内存溢出自动 dump
# OOM 时自动导出堆快照
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/
五、实战:OOM 排查
场景:线上服务报 OutOfMemoryError
第一步:查看 GC 日志
# 看 GC 日志中 Full GC 频率
grep "Full GC" gc.log | head -10
# 如果频繁 Full GC → 堆太小或内存泄露
# 如果 Full GC 后内存不降 → 内存泄露
第二步:分析堆转储文件
# 使用 jhat 分析(JDK 自带)
jhat dump.hprof
# 使用 MAT(推荐,可视化)
# Eclipse Memory Analyzer:下载 MAT 打开 dump 文件
第三步:常见原因
| 现象 | 原因 | 解决 |
|---|---|---|
| 频繁 Full GC,内存不释放 | 内存泄露 | 检查集合类、IO 流是否关闭 |
| GC 后内存释放,但很快又满 | 堆太小 | 调大 -Xmx |
| 元空间 OutOfMemory | 动态类加载过多 | 调大 MetaspaceSize |
| 直接内存 OutOfMemory | NIO 使用不当 | 检查 ByteBuffer 释放 |
六、常用 JVM 排查命令
# 1. 查看 Java 进程
jps -l
# 2. 查看堆内存使用
jstat -gcutil <pid> 1000 5 # 每秒打印一次,共5次
# 3. 查看线程状态
jstack <pid> > thread.dump
# 4. 查看 JVM 参数
jinfo -flags <pid>
# 5. 导出堆转储
jmap -dump:live,format=b,file=dump.hprof <pid>
# 6. 实时查看 GC
jstat -gc <pid> 1000
七、常见 JVM 面试题
1. 哪些情况会导致 Full GC?
- 老年代空间不足
- 元空间不足
- System.gc() 被调用
- GC 参数配置不合理
2. 如何判断对象是否可回收?
- 引用计数法(主流 JVM 不用)
- 可达性分析(JVM 采用):从 GC Roots 出发,不可达的对象可回收
3. GC Roots 有哪些?
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 native 方法引用的对象
总结
JVM 调优不需要背参数,记住三条:
- 先发现问题 —— 用 jstat 看 GC 频率,用 jmap 看堆使用
- 对症下药 —— Full GC 频繁调大堆或排查内存泄露
- 不要过度优化 —— 绝大多数应用默认参数就够了
如果对你有帮助,欢迎点赞、评论、关注【张老师技术栈】,持续分享 Java/Python/爬虫 实战干货。

267

被折叠的 条评论
为什么被折叠?



