JVM调优实战及常量池详解
一、阿里巴巴 Arthas 工具
Arthas 是 Alibaba 开源的 Java 诊断工具(支持 JDK6+),采用命令行交互,可快速定位线上问题,核心内容如下:
1. 下载与启动
# GitHub下载
wget https://alibaba.github.io/arthas/arthas-boot.jar
# Gitee下载(国内更快)
wget https://arthas.gitee.io/arthas-boot.jar
- 启动步骤:
- 执行
java -jar arthas-boot.jar
,工具自动识别当前机器所有 Java 进程; - 输入进程对应的序号(如
1
),进入该进程的 Arthas 交互界面。
- 执行
2. 核心命令与功能
命令格式 | 功能描述 | 实战场景示例 |
---|---|---|
dashboard |
实时展示进程的线程、内存、GC、运行环境信息(如 % CPU、堆内存 used/total) | 快速定位高 CPU 线程(如 Thread-0 占 CPU 97%) |
thread |
查看所有线程状态;thread <线程ID> 查看指定线程堆栈;thread -b 检测死锁 |
用thread -b 发现 Thread-1 与 Thread-2 互锁资源(分别持有 resourceA/resourceB) |
jad <类全限定名> |
反编译线上类,验证代码版本是否正确 | jad com.tuling.jvm.Arthas 查看线上 Arthas 类的实际代码 |
ognl "@类名@属性.方法()" |
操作类的静态属性 / 方法(如添加数据到静态集合) | ognl "@com.tuling.jvm.Arthas@hashSet.add('test123')" 往静态 hashSet 加数据 |
3. 实战案例
public class ArthasTest {private static HashSet hashSet = new HashSet();public static void main(String[] args) {//模拟CPU过高cpuHigh();// 模拟线程死锁deadThread();// 不断的向 hashSet 集合增加数据addHashSetThread();}/*** 不断的向 hashSet 集合添加数据*/public static void addHashSetThread() {// 初始化常量new Thread(() -> {int count = 0;while (true) {try {hashSet.add("count" + count);Thread.sleep(1000);count++;} catch (InterruptedException e) {e.printStackTrace();}}},"thread1").start();}public static void cpuHigh() {new Thread(() -> {while (true) {}},"thread2").start();}/*** 死锁*/private static void deadThread() {/** 创建资源 */Object resourceA = new Object();Object resourceB = new Object();// 创建线程Thread threadA = new Thread(() -> {synchronized (resourceA) {System.out.println(Thread.currentThread() + " get ResourceA");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread() + "waiting get resourceB");synchronized (resourceB) {System.out.println(Thread.currentThread() + " get resourceB");}}},"threadA");Thread threadB = new Thread(() -> {synchronized (resourceB) {System.out.println(Thread.currentThread() + " get ResourceB");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread() + "waiting get resourceA");synchronized (resourceA) {System.out.println(Thread.currentThread() + " get resourceA");}}},"threadB");threadA.start();threadB.start();}
}
Arthas
测试类,模拟三类问题,用 Arthas 排查:
- CPU 过高:
cpuHigh()
方法创建空循环线程,通过dashboard
发现该线程(Thread-0)% CPU 达 97%,thread 8
(线程 ID)定位到空循环代码; - 线程死锁:
deadThread()
方法中 ThreadA 锁 resourceA 等 resourceB,ThreadB 锁 resourceB 等 resourceA,thread -b
直接检测到死锁及锁持有关系; - 动态数据添加:
addHashSetThread()
方法每秒往静态 hashSet 加数据,用ognl
可实时操作该集合,验证数据添加逻辑。
二、GC 日志详解
通过配置 JVM 参数打印 GC 日志,分析 GC 原因与性能瓶颈,核心内容如下:
1. GC 日志配置参数
参数名称 | 作用 | 说明 |
---|---|---|
-Xloggc:./gc-%t.log |
指定 GC 日志输出路径,%t 为时间戳 |
避免日志覆盖,如生成gc-20240520.log |
-XX:+PrintGCDetails |
打印详细 GC 信息(区域内存变化、耗时) | 必配参数,核心分析依据 |
-XX:+PrintGCDateStamps |
打印 GC 发生的具体日期时间(如 2019-07-03T17:28:24) | 便于定位时间点相关问题 |
-XX:+PrintGCTimeStamps |
打印 GC 发生时 JVM 启动后的耗时(如 0.613 秒) | 计算 GC 频率 |
-XX:+UseGCLogFileRotation |
启用 GC 日志轮转(避免单日志过大) | 配合以下两个参数使用 |
-XX:NumberOfGCLogFiles=10 |
日志文件最大数量为 10 个 | 超过后覆盖旧日志 |
-XX:GCLogFileSize=100M |
单个日志文件最大大小为 100MB | 达到大小后生成新日志 |
2. GC 日志解读(以 Parallel GC 为例)
示例日志片段:
2019-07-03T17:28:24.889+0800:0.613:[GC (Allocation Failure) [PSYoungGen:65536K->3872K(76288K)]65536K->3888K(251392K), 0.0042006 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
- 时间信息:
2019-07-03T17:28:24.889+0800
(具体时间)、0.613
(JVM 启动后 0.613 秒); - GC 类型与原因:
GC (Allocation Failure)
(Minor GC,原因是内存分配失败); - 内存变化:
PSYoungGen:65536K->3872K(76288K)
:年轻代 GC 前占用 65536K,GC 后 3872K,总大小 76288K;65536K->3888K(251392K)
:堆内存 GC 前 65536K,GC 后 3888K,总大小 251392K;
- 耗时:
0.0042006 secs
(GC 总耗时,单位秒)。
3. 日志分析与优化
- 常见问题定位:若日志中频繁出现
Full GC (Metadata GC Threshold)
,说明元空间不足,需调整参数-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
; - 可视化工具:通过gceasy.io上传日志,生成可视化报告(如年轻代 / 老年代内存分配、GC 时长趋势),并提供智能优化建议(如 G1 GC 的
-XX:InitiatingHeapOccupancyPercent
调整)。
三、JVM 参数查看命令
通过以下命令查看 JVM 参数的默认值与运行时生效值,用于验证参数配置:
命令格式 | 功能描述 | 应用场景 |
---|---|---|
java -XX:+PrintFlagsInitial |
打印所有 JVM 参数的默认值 | 了解参数默认配置(如InitialHeapSize 默认值) |
java -XX:+PrintFlagsFinal |
打印所有 JVM 参数在运行时的生效值 | 验证参数是否正确生效(如-Xms10M 是否生效) |
四、常量池详解
常量池分为 Class 常量池、运行时常量池、字符串常量池,核心差异与特点如下:
1. Class 常量池与运行时常量池
- Class 常量池:
- 位置:Class 文件中,是 Class 文件的 “资源仓库”;
- 内容:存放编译期生成的字面量(如
1
、"zhuge"
)和符号引用(类全限定名、字段 / 方法名称及描述符); - 查看方式:
javap -v 类名.class
(生成可读字节码,展示Constant pool
section)。
- 运行时常量池:
- 位置:类加载后进入内存(JDK1.6 在永久代,JDK1.7 + 在堆);
- 功能:将 Class 常量池的符号引用转为直接引用(动态链接,如
compute()
方法符号引用转为内存地址)。
2. 字符串常量池
-
核心设计:JVM 为优化字符串创建效率,开辟独立的字符串常量池(类似缓存),创建字符串时优先复用池中对象。
-
位置变化(关键差异):
JDK 版本 字符串常量池位置 intern () 方法行为 JDK1.6 及之前 永久代(PermGen) 池中无该字符串时,复制堆对象到永久代,返回永久代引用 JDK1.7 及之后 堆(Heap) 池中无该字符串时,直接指向堆对象,返回堆引用 -
三种创建方式对比:
创建方式 常量池是否创建对象 堆是否创建对象 返回引用指向 String s = "zhuge";
无则创建 不创建 常量池对象 String s = new String("zhuge");
无则创建 必创建 堆对象 s.intern()
无则关联堆对象 不创建 常量池引用 -
特殊案例:
String s1 = new StringBuilder("ja").append("va").toString(); System.out.println(s1 == s1.intern()); // JDK1.7+输出false
原因:“java” 是关键字,JVM 初始化时已放入字符串常量池,
s1
指向堆对象,
s1.intern()
指向常量池对象,故不相等。
五、基本类型包装类与对象池
为优化基本类型包装类的创建效率,部分包装类实现对象池技术,核心规则如下:
1. 对象池实现情况
包装类类型 | 是否实现对象池 | 生效范围 | 示例代码与结果 |
---|---|---|---|
Byte | 是 | 所有值(-128~127) | Byte b1=127; Byte b2=127; System.out.println(b1==b2); // true |
Short | 是 | 值≤127 | Short s1=127; Short s2=127; System.out.println(s1==s2); // true |
Integer | 是 | 值≤127(默认范围,可通过参数调整) | Integer i1=127; Integer i2=127; System.out.println(i1==i2); // true |
Long | 是 | 值≤127 | Long l1=127; Long l2=127; System.out.println(l1==l2); // true |
Character | 是 | 值≤127 | Character c1='a'; Character c2='a'; System.out.println(c1==c2); // true |
Boolean | 是 | 所有值(true/false) | Boolean bool1=true; Boolean bool2=true; System.out.println(bool1==bool2); // true |
Float | 否 | 无 | Float f1=1.0f; Float f2=1.0f; System.out.println(f1==f2); // false |
Double | 否 | 无 | Double d1=1.0; Double d2=1.0; System.out.println(d1==d2); // false |
2. 关键注意点
- 用
new
创建包装类对象时,不使用对象池(如new Integer(127)
会新创建对象,==
比较为 false); - 整型包装类的对象池范围可通过 JVM 参数
-XX:AutoBoxCacheMax=<size>
调整(仅 Integer 支持)。
关键问题
问题 1:在 JDK1.8 环境下,如何用 Arthas 完整排查 “线上 Java 进程 CPU 占用过高” 的问题?请结合文档步骤说明。
答案:需通过 “定位高 CPU 线程→查看线程堆栈→关联代码” 三步排查,具体如下:
-
启动 Arthas 并进入进程:
执行
java -jar arthas-boot.jar
,输入高 CPU 进程对应的序号(如1
),进入交互界面; -
定位高 CPU 线程:
输入命令
dashboard
,查看 “% CPU” 列,找到 CPU 占比最高的线程(如 Thread-0,% CPU=97%),记录其线程 ID(如8
); -
查看线程堆栈:
输入命令
thread 8
(线程 ID),查看该线程的堆栈信息,定位到具体代码行(如com.tuling.jvm.Arthas.Lambda$cpuHigh$1(Arthas.java:39)
,发现是空循环导致 CPU 过高); -
验证代码(可选):
若怀疑代码版本问题,输入
jad com.tuling.jvm.Arthas
反编译线上类,确认cpuHigh()
方法是否存在空循环逻辑,最终定位问题根源。
问题 2:字符串常量池在 JDK1.6 与 JDK1.7 + 的位置和intern()
方法行为有何核心差异?请结合示例代码说明。
答案:核心差异体现在 “常量池位置” 和 “intern () 对象处理逻辑”,具体如下:
对比维度 | JDK1.6 及之前 | JDK1.7 及之后 |
---|---|---|
常量池位置 | 永久代(PermGen) | 堆(Heap) |
intern () 行为 | 池中无该字符串时,复制堆对象到永久代,返回永久代引用 | 池中无该字符串时,直接指向堆对象,返回堆引用 |
示例代码验证:
String s1 = new StringBuilder("zhuge").toString(); // 堆创建对象,常量池无"zhuge"
String s2 = s1.intern();
System.out.println(s1 == s2);
- JDK1.6 输出
false
:s1
指向堆对象,s2
指向永久代中复制的新对象,地址不同; - JDK1.7 + 输出
true
:s1
指向堆对象,s2
直接指向该堆对象(常量池关联堆引用),地址相同。
问题 3:Java 中 8 种基本类型的包装类中,哪些实现了对象池技术?其生效范围是什么?为何浮点数包装类未实现对象池?
答案:
-
实现对象池的包装类及生效范围:
共 6 种包装类实现对象池,具体如下:
包装类 生效范围 核心说明 Byte 所有值(-128~127) 范围固定,无调整空间 Short 值≤127 仅小值复用,大于 127 时新创建对象 Integer 值≤127(默认,可通过 -XX:AutoBoxCacheMax
调整)最常用,默认范围覆盖多数场景 Long 值≤127 同 Short,仅小值复用 Character 值≤127(ASCII 码范围内) 覆盖常用字符(如字母、数字) Boolean 所有值(true/false) 仅两个值,完全复用 -
浮点数包装类(Float、Double)未实现对象池的原因:
浮点数的取值范围极广(如 Float 可表示约 3.4×10³⁸的数),且小值的复用概率远低于整型(如业务中很少频繁使用
1.0
、2.0
等固定浮点数),实现对象池的 “收益(内存节省)” 远小于 “成本(池维护开销)”,因此 JVM 未为其实现对象池技术。