JVM 复习

一、JVM 核心原理详解(Java 8)

1.1 JVM 整体架构

JVM(Java Virtual Machine)是 Java 程序运行的核心,其主要职责包括:

  • 字节码加载与验证
  • 内存管理(分配与回收)
  • 执行引擎(解释/编译执行)
  • 线程管理
  • 安全控制

JVM 主要由以下组件构成:

复制代码
类加载器子系统 → 运行时数据区 → 执行引擎 → 本地方法接口 → 本地方法库

1.2 运行时数据区(Java 8)

Java 8 是一个重要的分水岭,永久代(PermGen)被元空间(Metaspace)取代

内存区域详细说明:
区域 线程私有 作用 是否可能 OOM Java 8 特点
程序计数器 记录当前线程执行的字节码指令地址 每个线程独立
虚拟机栈 存储栈帧(局部变量表、操作数栈、动态链接、方法出口) -Xss 控制栈大小
本地方法栈 Native 方法调用(JNI) 通常与虚拟机栈合并
堆(Heap) 对象实例、数组分配的主要区域 GC 主战场
元空间(Metaspace) 类的元数据信息(替代永久代) 使用本地内存
直接内存 NIO 的 DirectByteBuffer 不受 -Xmx 限制

💡 关键理解

  • 堆内存只是 JVM 内存的一部分
  • 元空间使用的是操作系统本地内存,不是 JVM 堆内存
  • 直接内存泄漏很难发现,因为不在堆中

1.3 堆内存结构(分代收集理论)

Java 8 的堆内存采用 分代收集 策略:

复制代码
                    ┌─────────────────┐
                    │    老年代        │ ← 长期存活对象
                    │ (Old Generation)│
                    └─────────────────┘
┌─────────────────────────────────────────────┐
│              新生代 (Young Generation)       │
├───────────┬───────────┬─────────────────────┤
│   Eden    │ Survivor0 │      Survivor1      │
│           │   (From)  │       (To)          │
└───────────┴───────────┴─────────────────────┘
  • Eden 区:新对象首先分配在 Eden 区
  • Survivor 区:Minor GC 后存活的对象进入 Survivor 区(S0/S1 轮换)
  • 老年代:对象年龄达到阈值(默认 15)或大对象直接进入老年代

1.4 垃圾回收算法

1.4.1 标记-清除(Mark-Sweep)
  • 优点:实现简单
  • 缺点:产生内存碎片,效率不高
  • 使用场景:CMS 的并发清除阶段
1.4.2 复制(Copying)
  • 原理:将内存分为两块,每次只使用一块,存活对象复制到另一块
  • 优点:无碎片,效率高
  • 缺点:内存利用率 50%
  • 使用场景:新生代回收(Eden + Survivor)
1.4.3 标记-整理(Mark-Compact)
  • 原理:标记存活对象,然后向一端移动,清理边界外内存
  • 优点:无碎片
  • 缺点:移动对象开销大
  • 使用场景:老年代回收(Serial Old, Parallel Old)
1.4.4 分代收集(Generational Collection)
  • 核心思想:不同代采用不同算法
  • 新生代:复制算法(高效处理大量短命对象)
  • 老年代:标记-整理或标记-清除(处理少量长命对象)

二、Java 8 主流 GC 算法详解

2.1 Serial GC(串行收集器)

  • 特点:单线程,Stop-The-World
  • 适用场景:单 CPU、客户端应用、嵌入式设备
  • 参数-XX:+UseSerialGC
  • 生产使用基本不用

2.2 Parallel GC(并行收集器)- 吞吐量优先

  • 新生代:Parallel Scavenge(多线程复制)
  • 老年代:Parallel Old(多线程标记-整理)
  • 特点
    • 吞吐量高(用户代码执行时间 / 总时间)
    • STW 时间较长但频率低
  • 适用场景:后台计算、批处理任务
  • 参数-XX:+UseParallelGC(Java 8 默认)
  • 大厂使用:较少用于 Web 服务

2.3 CMS(Concurrent Mark Sweep)- 低延迟优先

  • 目标:最小化停顿时间

  • 回收过程(4个阶段):

    1. 初始标记(Initial Mark)- STW,标记 GC Roots 直接关联对象
    2. 并发标记(Concurrent Mark)- 并发,标记所有存活对象
    3. 重新标记(Remark)- STW,修正并发标记期间变化
    4. 并发清除(Concurrent Sweep)- 并发,清除垃圾对象
  • 优点:停顿时间短(通常 < 100ms)

  • 缺点

    • CPU 敏感:并发阶段占用 CPU 资源
    • 浮动垃圾:并发清除期间产生的垃圾只能下次回收
    • 内存碎片:标记-清除算法导致
    • Concurrent Mode Failure:老年代空间不足时触发 Full GC
  • 适用场景:Web 应用、响应时间敏感系统

  • 参数-XX:+UseConcMarkSweepGC

  • 大厂使用:阿里、腾讯早期广泛使用(现已逐步迁移到 G1)

2.4 G1(Garbage First)- 兼顾吞吐与延迟

  • 核心思想

    • 将堆划分为多个 Region(默认 2048 个)
    • 每个 Region 可以是 Eden、Survivor 或 Old
    • Remembered Set(RSet):记录跨 Region 引用,避免全堆扫描
  • 回收过程

    1. Young GC:回收年轻代 Region
    2. Concurrent Marking:并发标记整个堆
    3. Mixed GC:回收部分老年代 Region + 年轻代 Region
  • 优点

    • 可预测停顿时间(-XX:MaxGCPauseMillis
    • 避免内存碎片(采用复制+整理)
    • 适合大堆(4G+)
  • 缺点

    • CPU 开销比 CMS 高
    • 小堆(< 4G)性能不如 CMS
  • 适用场景:大内存、低延迟要求的 Web 服务

  • 参数-XX:+UseG1GC

  • 大厂使用:美团、B站、字节跳动主流选择

三、Java 8 生产环境 JVM 参数配置

3.1 基础参数配置(所有服务必备)

复制代码
# 堆内存设置(建议物理内存的 50%~70%)
-Xms8g                    # 初始堆大小(等于最大堆,避免运行时扩容抖动)
-Xmx8g                    # 最大堆大小

# 新生代设置
-Xmn3g                    # 新生代大小(建议占堆的 1/3 ~ 1/2)
# 或使用比例:-XX:NewRatio=2(老年代:新生代 = 2:1)

# 元空间设置(防止动态类加载导致 OOM)
-XX:MetaspaceSize=256m    # 触发 Metaspace GC 的初始阈值
-XX:MaxMetaspaceSize=512m # 元空间最大大小(必须设置!)

# 虚拟机栈大小
-Xss256k                  # 每个线程栈大小(默认 1M,可适当调小节省内存)

# 禁用显式 GC(防止 System.gc() 触发 Full GC)
-XX:+DisableExplicitGC

# GC 日志(必须开启!)
-Xloggc:/data/logs/gc-%t.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=100M

.2 CMS GC 参数配置(Java 8 经典方案)

复制代码
# 启用 CMS
-XX:+UseConcMarkSweepGC

# 新生代使用 ParNew(CMS 必须搭配 ParNew)
-XX:+UseParNewGC

# CMS 并发线程数(默认 (CPU+3)/4)
-XX:ConcGCThreads=4

# 老年代使用率达到多少时触发 CMS(默认 68%)
-XX:CMSInitiatingOccupancyFraction=70

# CMS 不动态调整策略(避免频繁 GC)
-XX:+UseCMSInitiatingOccupancyOnly

# Full GC 前进行碎片整理(默认 true)
-XX:+UseCMSCompactAtFullCollection

# 多少次 Full GC 后进行碎片整理(默认 0,每次 Full GC 都整理)
-XX:CMSFullGCsBeforeCompaction=5

# 预留内存给并发阶段(避免 Concurrent Mode Failure)
-XX:CMSReservePercent=10

⚠️ CMS 关键调优点

  • CMSInitiatingOccupancyFraction 必须配合 UseCMSInitiatingOccupancyOnly 使用
  • 设置过低会导致 GC 频繁,过高会导致 Concurrent Mode Failure

3.3 G1 GC 参数配置(Java 8 推荐方案)

复制代码
# 启用 G1
-XX:+UseG1GC

# 目标最大停顿时间(JVM 会尽量满足,但不保证)
-XX:MaxGCPauseMillis=200

# 并发 GC 级线程数(默认约 CPU 核数的 1/4)
-XX:ConcGCThreads=4

# 并行 GC 线程数(默认 CPU 核数)
-XX:ParallelGCThreads=8

# 混合 GC 触发阈值(老年代占用达到此比例触发 Mixed GC)
-XX:InitiatingHeapOccupancyPercent=45

# 每次 Mixed GC 回收的 Region 数量目标
-XX:G1MixedGCCountTarget=8

# Region 大小(默认根据堆大小自动计算,大堆可手动设置)
-XX:G1HeapRegionSize=16m

# 避免大对象直接进入老年代
-XX:G1HeapWastePercent=10

3.4 针对特定场景的参数优化

场景1:高并发 Web 服务(Tomcat/Spring Boot)
复制代码
# 堆内存
-Xms4g -Xmx4g
-Xmn1536m

# CMS 配置
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly

# 元空间
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=384m

# 线程栈
-Xss256k
复制代码
# 大堆内存
-Xms16g -Xmx16g
-Xmn6g

# G1 配置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=300
-XX:InitiatingHeapOccupancyPercent=35

# 元空间
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=1g
场景3:微服务(容器化部署)
复制代码
# 容器内存限制 2G
-Xms1536m -Xmx1536m
-Xmn512m

# G1(小堆也适用)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100

# 元空间(容器环境类较少)
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m

# 线程栈(节省内存)
-Xss192k

四、JVM 调优方法论与故障排查

4.1 监控指标体系

必须监控的核心指标:
指标 工具 告警阈值
Minor GC 频率 jstat / Prometheus > 10次/秒
Full GC 频率 GC 日志 > 0(立即告警)
GC 停顿时间 GC 日志 Max Pause > 500ms
老年代使用率 jstat > 70% 持续增长
Metaspace 使用率 jstat > 80%
线程数 jstack > 2000

4.2 故障排查四板斧

1. jstat -gc(实时 GC 状态)
bash 复制代码
# 每秒输出一次,持续监控
jstat -gc 12345 1000

# 关键字段解读:
# S0C/S1C: Survivor0/1 容量(KB)
# S0U/S1U: Survivor0/1 使用量(KB)
# EC/EU: Eden 容量/使用量(KB)
# OC/OU: 老年代容量/使用量(KB)
# MC/MU: Metaspace 容量/使用量(KB)
# YGC/YGCT: Young GC 次数/总时间
# FGC/FGCT: Full GC 次数/总时间
2. jmap -histo:live(对象分布分析)
bash 复制代码
# 查看存活对象的实例数和内存占用
jmap -histo:live 12345 | head -20

# 输出示例:
# num     #instances         #bytes  class name
# ----------------------------------------------
#    1:       1000000       80000000  java.lang.String
#    2:        500000       40000000  com.example.CacheEntry
3. jmap -dump:format=b,file=heap.hprof(堆转储)
bash 复制代码
# 生成堆转储文件(谨慎使用,会触发 Full GC)
jmap -dump:format=b,file=/tmp/heap.hprof 12345

# 使用 Eclipse MAT 分析:
# - Histogram:查看对象分布
# - Dominator Tree:查看谁在阻止对象回收
# - Leak Suspects:自动检测内存泄漏
4. Arthas(阿里开源)(线上诊断神器)
bash 复制代码
# 安装 Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 常用命令:
dashboard          # JVM 实时监控
thread -n 3        # 查看 CPU 占用最高的 3 个线程
heapdump           # 生成堆转储
ognl '@com.example.Config@getInstance().getCacheSize()'  # 动态调用方法

4.3 典型问题解决方案

问题1:频繁 Full GC,老年代持续增长

根因:内存泄漏 or 老年代过小

解决步骤

  1. jstat -gc 确认 OU(老年代使用量)持续增长
  2. jmap -histo:live 找出异常对象
  3. 分析堆转储,确认是否为缓存未清理、静态集合持有等
  4. 代码层面:WeakHashMap 替代 HashMap,及时 clear 缓存
  5. JVM 层面:适当增大老年代,调整 GC 参数
问题2:CMS Concurrent Mode Failure

现象:日志中出现 "Concurrent mode failure"

根因:CMS 并发阶段老年代空间不足

解决方案

bash 复制代码
# 降低触发 CMS 的阈值
-XX:CMSInitiatingOccupancyFraction=60
-XX:+UseCMSInitiatingOccupancyOnly

# 增加预留空间
-XX:CMSReservePercent=15
问题3:Metaspace OOM

现象java.lang.OutOfMemoryError: Metaspace

根因:动态代理类过多(Spring AOP、MyBatis Mapper)

bash 复制代码
# 必须设置上限!
-XX:MaxMetaspaceSize=512m
-XX:MetaspaceSize=256m

# 根本解决:减少不必要的动态代理
# - 合理使用 @Transactional
# - 避免过度使用 CGLib 代理
问题4:直接内存 OOM

现象java.lang.OutOfMemoryError: Direct buffer memory

根因:Netty、Kafka Client 等使用 DirectByteBuffer

解决方案

bash 复制代码
# 限制直接内存大小(默认等于 -Xmx)
-XX:MaxDirectMemorySize=2g

# 监控直接内存使用
# -XX:+PrintGCDetails 会输出 DirectMemory 信息

五、高级面试与深度理解

5.1 关于 GC 算法选择

"CMS 和 G1 的本质区别在于:CMS 是基于'代'的收集器,而 G1 是基于'Region'的收集器。G1 通过 Remembered Set 实现了精确的跨代引用跟踪,从而能够在并发标记阶段避免扫描整个老年代,这是 G1 能够支持大堆且保持低延迟的关键。"

5.2 关于调优哲学

"JVM 调优不是追求最小停顿时间,而是在延迟、吞吐量、CPU 消耗之间找到最佳平衡点。盲目设置 MaxGCPauseMillis=50ms,可能导致 GC 线程占用 50% 的 CPU 资源,反而降低了整体吞吐量。"

5.3 关于内存泄漏

"Java 中真正的内存泄漏很少见,更多是'内存囤积'(Memory Hoarding)------对象已经不再需要,但由于编程错误未能被释放。最常见的三种情况:静态集合类持有对象引用、ThreadLocal 未调用 remove()、监听器未注销。"

5.4 关于监控重要性

"没有 GC 日志的 JVM 就像没有仪表盘的飞机。我们要求所有线上服务必须开启详细的 GC 日志,并接入 ELK 实现自动化分析。当 Full GC 发生时,应该在 5 分钟内收到告警并开始排查。"

5.5 关于参数设置原则

"生产环境的 JVM 参数设置必须遵循三个原则:一是堆内存初始值等于最大值,避免运行时扩容;二是必须设置 Metaspace 上限,防止动态类加载失控;三是必须开启 GC 日志,这是故障排查的生命线。"


六、总结:Java 8 JVM 调优 Checklist

基础配置

  • -Xms = -Xmx(避免堆扩容抖动)
  • 设置 -XX:MaxMetaspaceSize(防止 Metaspace OOM)
  • 开启详细 GC 日志(故障排查必备)

GC 选择

  • Web 服务:CMS(精细调参)或 G1(推荐)
  • 批处理:Parallel GC
  • 大堆(> 8G):G1

监控告警

  • 监控 Minor GC 频率、Full GC 次数、老年代使用率
  • Full GC > 0 立即告警
  • 老年代使用率 > 70% 持续增长告警

应急手段

  • 熟练使用 jstatjmapjstack
  • 掌握 Arthas 线上诊断
  • 堆转储分析流程(MAT → Dominator Tree)

最后忠告
80% 的性能问题源于代码层面,而非 JVM 参数。在进行 JVM 调优前,务必先用 Arthas 或 Profiler 工具确认是否存在 N+1 查询、大循环 new 对象、不当缓存等代码问题。JVM 调优是最后的优化手段,而不是第一选择。

相关推荐
Tony Bai12 小时前
Go 安全新提案:runtime/secret 能否终结密钥残留的噩梦?
java·开发语言·jvm·安全·golang
不会代码的小猴21 小时前
C++的第十一天笔记
java·前端·jvm
Unstoppable2221 小时前
八股训练营第 38 天 | 类加载机制介绍一下?介绍一下双亲委派机制?说一说你对 Spring AOP 的了解?说一说你对 Spring 中 IoC 的理解?
java·jvm·spring
博语小屋1 天前
线程同步与条件变量
linux·jvm·数据结构·c++
javadaydayup1 天前
Pagehelper触发 JVM 类校验失败,Idea 却因 -noverify 藏了雷
jvm
没有bug.的程序员2 天前
Async Profiler:最精准的火焰图工具
java·jvm·spring·对象分配·async profiler
小帅学编程2 天前
JVM学习记录
jvm·学习
Yweir2 天前
Linux性能监控的工具集和分析命令工具
java·linux·jvm