当前位置: 首页 > news >正文

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
  • 启动步骤
    1. 执行java -jar arthas-boot.jar,工具自动识别当前机器所有 Java 进程;
    2. 输入进程对应的序号(如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 线程→查看线程堆栈→关联代码” 三步排查,具体如下:

  1. 启动 Arthas 并进入进程

    执行java -jar arthas-boot.jar,输入高 CPU 进程对应的序号(如1),进入交互界面;

  2. 定位高 CPU 线程

    输入命令dashboard,查看 “% CPU” 列,找到 CPU 占比最高的线程(如 Thread-0,% CPU=97%),记录其线程 ID(如8);

  3. 查看线程堆栈

    输入命令thread 8(线程 ID),查看该线程的堆栈信息,定位到具体代码行(如com.tuling.jvm.Arthas.Lambda$cpuHigh$1(Arthas.java:39),发现是空循环导致 CPU 过高);

  4. 验证代码(可选)

    若怀疑代码版本问题,输入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 输出falses1指向堆对象,s2指向永久代中复制的新对象,地址不同;
  • JDK1.7 + 输出trues1指向堆对象,s2直接指向该堆对象(常量池关联堆引用),地址相同。

问题 3:Java 中 8 种基本类型的包装类中,哪些实现了对象池技术?其生效范围是什么?为何浮点数包装类未实现对象池?

答案

  1. 实现对象池的包装类及生效范围

    共 6 种包装类实现对象池,具体如下:

    包装类 生效范围 核心说明
    Byte 所有值(-128~127) 范围固定,无调整空间
    Short 值≤127 仅小值复用,大于 127 时新创建对象
    Integer 值≤127(默认,可通过-XX:AutoBoxCacheMax调整) 最常用,默认范围覆盖多数场景
    Long 值≤127 同 Short,仅小值复用
    Character 值≤127(ASCII 码范围内) 覆盖常用字符(如字母、数字)
    Boolean 所有值(true/false) 仅两个值,完全复用
  2. 浮点数包装类(Float、Double)未实现对象池的原因

    浮点数的取值范围极广(如 Float 可表示约 3.4×10³⁸的数),且小值的复用概率远低于整型(如业务中很少频繁使用1.02.0等固定浮点数),实现对象池的 “收益(内存节省)” 远小于 “成本(池维护开销)”,因此 JVM 未为其实现对象池技术。

http://www.hskmm.com/?act=detail&tid=20976

相关文章:

  • Cisco Identity Services Engine (ISE) 3.5 - 基于身份的网络访问控制和策略实施系统
  • 03-控制台项目创建与结构说明
  • 赋能智慧应急:国标GB28181平台EasyGBS视频技术如何成为气象灾害预警新工具
  • NET各个版本新增的特性和语法糖
  • xinference推理embedding等小模型
  • day15-项目上线
  • opencv学习记录6
  • 努力的轨迹,通往成长的旅程——赵欣彤的自我介绍
  • 第2章 day02 requests基础
  • 线性代数_工程实践-计算实现numpy
  • 在HAL库使用printf打印串口信息
  • 第4章 day04 防盗链
  • 第3章 day03 xpath+反爬虫
  • 002- 学习环境搭建
  • 第10章 day10 DrissionPage详细教程
  • 求局部最小值
  • Element-UI的transfer穿梭框组件数据量大解决方案
  • 第9章 day09 hook插件
  • nginx 一致性hash和流量检查模块
  • 深入解析:10月底实习准备-Mysql(按面试频率准备)
  • 机器学习概述 - -一叶知秋
  • CEXE的%你赛5-题解
  • C++语言(1)
  • Windows多人共享文件夹全流程,附2025新共享文件快90%
  • 第11章 day11-day12关于json请求体/逆向爬虫实战
  • 容斥与二项式反演
  • react useCallback Hook详解
  • 从Docker构建失败到CRA被淘汰:一个React项目的ES模块探索记录
  • 充气泵PCBA方案中数字传感器和模拟传感器的差异
  • 实用指南:小米17手机的上市公司供应商