Java性能优化JVM篇
Java性能优化 · JVM篇
关于 JVM 有一本非常经典的书籍,周志明老师的《深入理解 Java 虚拟机》,本篇的内容在该书中几乎都能找到,我也写了关于本书的读书笔记,经典图书-深入理解Java虚拟机。
本来想重新整理一下,想了想还是 Don’t repeat yourself,关于内存模型、即时编译、垃圾回收和 GC,在读书笔记中都整理过了。

因此这里不再重复,只介绍下 Java 内存问题的排查路径,当然还是要先介绍一些前置知识。
对象生存周期
- 新建对象优先分配到 Eden 区
- 对象有对象年龄计数器(
-XX:MaxTenuringThreshold设置) - Eden 空间不足时,JVM 执行 Minor GC,将存活对象转移到 Survivor 中,年龄 +1
- 对象在 Survivor 中经历 Minor GC,每经过一次 Minor GC,年龄 +1
- 可通过
-XX:PetenureSizeThreshold设置直接被分配到老年代的最大对象阈值
JVM 堆内存分配默认值
1 | |
JDK 1.7 默认比例:
- 年轻代 : 老年代 = 1 : 2(可通过
-XX:NewRatio重置) - Eden : To Survivor : From Survivor = 8 : 1 : 1(可通过
-XX:SurvivorRatio重置)
JDK 1.8:
- 默认开启
-XX:+UseAdaptiveSizePolicy,JVM 动态调整 Java 堆各区域大小以及进入老年代的年龄 - 开启自适应策略后,
-XX:NewRatio和-XX:SurvivorRatio将会失效 - **不要随便关闭
UseAdaptiveSizePolicy**,除非已对堆内存/年轻代/老年代/Eden/Survivor 有非常明确的规划,否则 JVM 将分配最小堆内存,默认比例分配未必是最佳配置
JVM 内存分配不合理最直接的表现是频繁的 GC,导致上下文切换等性能问题,降低系统吞吐量、增加响应时间。在线上或性能测试时发现频繁 GC(且是正常的对象创建和回收),就需要考虑调整 JVM 内存分配。
Linux 命令行工具
top 命令:
1 | |
实时显示 CPU 使用率、内存使用率、系统负载等信息。
vmstat 命令:
- 指定采样周期和次数的功能性监测工具
- 不仅统计内存使用,还可观测 CPU 使用率、swap 使用情况
- 经常被用来观察进程的上下文切换
关键字段:r(等待运行进程数)、b(非中断睡眠进程数)、swpd(虚拟内存)、free(空闲内存)、si/so(交换页数量)、bi/bo(块设备块数)、in(每秒中断数)、cs(每秒上下文切换次数)、us/sy/id/wa/st(CPU 时间)
pidstat 命令(Sysstat 组件,yum install sysstat):
1 | |
| 参数 | 说明 |
|---|---|
-u |
默认参数,显示各进程 CPU 使用情况 |
-r |
显示各进程内存使用情况 |
-d |
显示各进程 I/O 使用情况 |
-w |
显示每个进程的上下文切换情况 |
-p |
指定进程号 |
-t |
显示进程中线程的统计信息 |
关键指标:Minflt/s(次要错误)、Majflt/s(主要错误)、VSZ(虚拟地址大小)、RSS(常驻集合大小)
JDK 工具
jstat 命令:
1 | |
-class:显示 ClassLoad 信息-compiler:显示 JIT 编译信息-gc:显示和 gc 相关的堆信息-gccapacity:显示各代容量及使用情况-gcmetacapacity:显示 Metaspace 大小-gcnew:显示新生代信息-gcnewcapacity:显示新生代大小和使用情况-gcold:显示老年代和永久代信息-gcoldcapacity:显示老年代大小-gcutil:显示垃圾收集信息-gccause:显示 GC 相关信息和诱因-printcompilation:输出 JIT 编译方法信息
jstat -gc 输出字段:
S0C/S1C:年轻代 To/From Survivor 容量S0U/S1U:年轻代 To/From Survivor 已使用空间EC/EU:年轻代 Eden 容量/已使用空间OC/OU:Old 代容量/已使用空间MC/MU:Metaspace 容量/已使用空间YGC/YGCT:年轻代 GC 次数/总时间FGC/FGCT:Full GC 次数/总时间GCT:GC 用的总时间
jstack 命令:
1 | |
线程堆栈分析工具,查看线程堆栈信息、线程 ID、状态(wait/sleep/running)、是否持有锁,常与 top -Hp 或 pidstat -p -t 结合使用,常用于排查死锁异常。
jmap 命令:
1 | |
注意:dump 出的文件用 MAT(Memory Analyzer Tool) 工具分析。
内存溢出 vs 内存泄漏
| 概念 | 说明 |
|---|---|
| 内存泄漏 | 不再使用的对象无法及时回收,持续占用内存空间,造成浪费(如 Java 6 substring 复用原 char 数组导致大字符串无法回收) |
| 内存溢出(OOM) | 发生 OutOfMemoryException,堆内存、栈、方法区空间不足时发生 |
| 关系 | 内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致 |
关键调优参数
内存相关
| 参数 | 说明 |
|---|---|
-Xms |
堆初始大小 |
-Xmx |
堆最大值 |
-Xmn |
年轻代大小 |
-XX:NewRatio |
年轻代与老年代比例 |
-XX:SurvivorRatio |
Eden 与 Survivor 区比例 |
-XX:MaxTenuringThreshold |
对象年龄计数器阈值 |
-XX:PetenureSizeThreshold |
直接分配到老年代的最大对象阈值 |
-XX:MetaspaceSize |
元空间大小 |
-XX:+UseAdaptiveSizePolicy |
开启自适应大小策略(JDK 1.8 默认开启) |
JIT 编译相关
| 参数 | 说明 |
|---|---|
-client / -server |
强制指定 JIT 模式(Java 8 后无效) |
-XX:+TieredCompilation |
开启分层编译(默认) |
-XX:-TieredCompilation |
关闭分层编译,只用 C2 |
-XX:TieredStopAtLevel=1 |
分层编译时只用 C1 |
-Xint |
强制只有解释器 |
-Xcomp |
强制只有 JIT |
-XX:CompileThreshold=N |
方法调用计数器阈值 |
-XX:OnStackReplacePercentage=N |
回边计数器阈值 |
-XX:MaxFreqInlineSize=N |
经常执行方法的内联大小(默认 325 字节) |
-XX:MaxInlineSize=N |
非经常执行方法的内联大小(默认 35 字节) |
JIT 优化相关
| 参数 | 说明 |
|---|---|
-XX:+DoEscapeAnalysis |
开启逃逸分析(默认) |
-XX:+EliminateLocks |
开启锁消除(默认) |
-XX:+EliminateAllocations |
开启标量替换(默认) |
-XX:+PrintCompilation |
打印编译过程信息 |
-XX:+UnlockDiagnosticVMOptions |
解锁 JVM 诊断选项 |
-XX:+PrintInlining |
打印内联方法 |
GC 相关
| 参数 | 说明 |
|---|---|
-XX:+PrintGC |
输出 GC 日志 |
-XX:+PrintGCDetails |
输出 GC 详细日志 |
-XX:+PrintGCTimeStamps |
输出 GC 时间戳(基准时间) |
-XX:+PrintGCDateStamps |
输出 GC 时间戳(日期形式) |
-XX:+PrintHeapAtGC |
GC 前后打印堆信息 |
-Xloggc:./gclogs |
日志文件输出路径 |
-XX:InitiatingHeapOccupancyPercent=N |
G1 触发 Mix GC 的堆占用阈值(默认 45) |
-XX:MaxGCPauseMillis=N |
G1 目标停顿时间(默认 200ms) |
OOM Dump 相关
| 参数 | 说明 |
|---|---|
-XX:+HeapDumpOnOutOfMemoryError |
堆内存异常时 dump |
-XX:HeapDumpPath=/tmp/heapdump.hprof |
dump 文件路径 |
一定要注意:JVM 调优先观察后动手,默认配置在大部分场景下效果都不错,一定要基于监控数据(top、vmstat、pidstat、jstat、jstack、jmap)和 GC 日志调整。