JVM调优实战及常量池详解
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 poolsection)。 
 - 运行时常量池:
- 位置:类加载后进入内存(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 未为其实现对象池技术。 

                
            
        
浙公网安备 33010602011771号