程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Java底层 - JVM

    • JVM - Java体系结构
    • JVM - 类加载子系统
    • JVM - 运行时数据区概述及线程
    • JVM - 程序计数器
    • JVM - 虚拟机栈
    • JVM - 本地方法接口
    • JVM - 本地方法栈
    • JVM - 堆 (Heap)
    • JVM - 方法区
    • JVM - 对象实例化内存布局
    • JVM - 直接内存管理
    • JVM - 执行引擎
    • JVM - 字符串常量池 (StringTable)
    • JVM - 垃圾回收概述
    • JVM - 垃圾回收相关算法
    • JVM - 垃圾回收相关概念
    • JVM - 垃圾回收器
    • JVM - Class文件结构
    • JVM - 字节码指令集与解析
    • JVM - 类的加载过程详解
    • JVM - 再谈类的加载器
    • JVM - 调优概述
    • JVM - 监控及诊断工具cmd
      • 1. 概述:为何需要命令行工具?
      • 2. jps:轻量级 JVM 进程状态查看器
      • 3. jstat:深入 JVM 运行时统计信息
      • 4. jinfo:实时查看与调整 JVM 参数
      • 5. jmap:导出堆转储快照与分析内存使用
      • 6. jhat:交互式堆转储快照分析工具 (已移除)
      • 7. jstack:捕获 JVM 线程快照 (Thread Dump)
      • 8. jcmd:多功能 JVM 诊断命令
      • 9. jstatd:开启远程 JVM 监控
    • JVM - 监控及诊断工具GUI
    • JVM - 运行时参数
    • JVM - 分析GC日志
  • Java底层
  • Java底层 - JVM
scholar
2024-01-31
目录

JVM - 监控及诊断工具cmd

# 1. 概述:为何需要命令行工具?

软件性能诊断是工程师日常工作中不可或缺的一环。尤其在追求极致用户体验的今天,高效解决应用性能问题能带来显著的业务价值。Java 作为广泛应用的编程语言,其性能诊断一直备受关注。导致 Java 应用性能问题的原因多种多样,如线程管理不当、磁盘 I/O 瓶颈、数据库访问缓慢、网络延迟、垃圾收集(GC)效率低下等。要精准定位这些问题,掌握并善用性能诊断工具至关重要。

核心理念:

  • 数据驱动:用具体的数据指标来发现和量化问题。
  • 知识分析:结合 JVM 原理和应用知识来分析数据背后的原因。
  • 工具辅助:利用专业的工具来高效地收集数据和处理问题。
  • 监控先行:没有监控就没有调优的基础。

基础命令行工具概览

除了我们最熟悉的 javac (编译) 和 java (运行) 命令,JDK 的 bin 目录下还包含了一系列用于监控和诊断 JVM 的命令行工具。这些工具能帮助我们获取目标 JVM 在不同方面、不同层级的详细信息,有效解决 Java 应用程序的疑难杂症。

image-20220131152127843 图:JDK bin 目录下的部分命令行工具

这些工具的官方源码可以参考 OpenJDK 的仓库,例如 JDK 11 的路径:http://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/jdk.jcmd/share/classes/sun/tools

# 2. jps:轻量级 JVM 进程状态查看器

jps (Java Process Status) 命令用于显示指定系统内当前正在运行的所有 HotSpot 虚拟机进程。它可以快速列出 Java 进程的本地虚拟机唯一 ID (LVMID),以及进程启动的主类名或 Jar 文件名。

重要说明:对于本地运行的 Java 虚拟机进程,其 LVMID 与操作系统分配的进程 ID (PID) 是一致且唯一的。这使得我们可以方便地将 jps 的输出与其他需要 PID 的命令(如 jstat, jmap, jstack)关联起来。

基本语法:

jps [options] [hostid]
1

代码示例:

假设我们运行以下简单的 Java 程序,它会等待用户输入:

import java.util.Scanner;

/**
 * 一个简单的 Java 程序,用于演示 jps 命令。
 * 程序启动后会阻塞,等待控制台输入。
 */
public class ScannerTest {
    public static void main(String[] args) {
        System.out.println("ScannerTest 正在运行,等待输入...");
        Scanner scanner = new Scanner(System.in);
        // next() 方法会阻塞,直到用户在控制台输入内容并按下回车
        String info = scanner.next();
        System.out.println("输入内容: " + info);
        scanner.close();
        System.out.println("ScannerTest 运行结束。");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行该程序后,在另一个命令行窗口中执行 jps:

jps
1

输出可能类似:

image-20220131153051380 图:jps 命令的基本输出

这里 20472 就是 ScannerTest 进程的 LVMID (也是 PID),ScannerTest 是启动的主类名。19812 Jps 是 jps 命令自身的进程。

options 参数详解:

jps 支持多个选项来显示更详细的信息:

  • -q (Quiet): 仅显示 LVMID,省略主类名、Jar 文件名和传递给 main 方法的参数。适用于只需要进程 ID 的场景。
  • -l (Long): 输出应用程序主类的全限定名;如果进程是通过 -jar 参数执行的 Jar 包,则输出 Jar 文件的完整路径。
  • -m (Main arguments): 输出传递给主类 main() 方法的参数。
  • -v (Verbose / VM arguments): 列出虚拟机进程启动时显式指定的 JVM 参数(例如 -Xms, -Xmx, -XX:NewRatio 等)。

参数组合使用示例:

jps -l -m -v
1

输出可能类似:

image-20220131153248445 图:jps -l -m -v 参数组合使用示例

注意:如果目标 Java 进程启动时使用了 -XX:-UsePerfData 参数关闭了性能统计数据的收集,那么 jps(以及后续介绍的 jstat)将无法探测到该 Java 进程。

hostid 参数:

hostid 用于指定远程主机的信息,格式通常是 [protocol:][[//]hostname][:port][/servername]。要实现远程监控,远程主机上需要运行 jstatd 服务。

  • 安全考虑:在对安全性要求较高的网络环境中,直接暴露 jstatd 可能存在风险。可以配置 JMX 安全策略文件来限制访问,但这可能受到 IP 欺诈等攻击。最安全的方式通常是在目标主机上本地执行 jps 和 jstat,而不是运行 jstatd 提供远程访问。

# 3. jstat:深入 JVM 运行时统计信息

官方文档参考 (Java 8): https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

jstat (JVM Statistics Monitoring Tool) 是一个强大的命令行工具,用于实时监视 Java 虚拟机(JVM)的各种运行时状态信息。它可以显示本地或远程 JVM 进程中的**类加载、内存(堆、元空间)、垃圾收集(GC)以及即时编译(JIT)**等详细的运行数据。

在没有图形用户界面(GUI)的环境下(例如纯文本控制台的服务器),jstat 是定位 JVM 运行时性能问题(特别是 GC 问题和内存泄漏)的首选工具。

基本语法:

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
1
  • <option>: 指定要查询的统计信息类型(详见下文)。
  • -t: 在输出的第一列显示时间戳(从 JVM 启动开始的秒数)。
  • -h<lines>: 每输出 lines 行数据后,重新打印一次表头。
  • <vmid>: 目标 Java 进程的 LVMID(即 jps 输出的进程 ID)。
  • <interval>: 连续输出的时间间隔,单位是毫秒(ms)或秒(s)。例如 1000ms 或 1s。
  • <count>: 总共输出的次数。如果省略 count 但指定了 interval,jstat 会持续输出,直到目标 JVM 进程终止或手动停止命令。

可以通过 jstat -h 或 jstat -help 查看所有可用选项。

option 参数详解:

jstat 的核心在于其丰富的 option 参数,用于指定不同的监控维度:

类加载相关 (-class)

  • -class: 显示类加载器(ClassLoader)的相关统计信息。

    • Loaded: 已加载的类的数量。
    • Bytes: 已加载类占用的总空间(字节)。
    • Unloaded: 已卸载的类的数量。
    • Bytes: 已卸载类释放的总空间(字节)。
    • Time: 类加载和卸载操作所消耗的时间(秒)。

    示例:查看进程 13968 的类加载信息:

    jstat -class 13968
    
    1

    image-20220131153919947

JIT 编译相关 (-compiler, -printcompilation)

  • -compiler: 显示 HotSpot JIT 编译器编译活动的相关统计信息。

    • Compiled: 已完成编译任务的数量。
    • Failed: 编译失败的任务数量。
    • Invalid: 无效的编译任务数量。
    • Time: 编译任务消耗的时间(秒)。
    • FailedType: 最后一次失败编译的类型。
    • FailedMethod: 最后一次失败编译的方法名。

    示例:查看进程 13968 的 JIT 编译统计:

    jstat -compiler 13968
    
    1

    image-20220131154943898

  • -printcompilation: 输出最近被 JIT 编译过的方法信息。

    • Compiled: 最近编译的方法数量。
    • Size: 方法字节码的大小。
    • Type: 编译类型。
    • Method: 方法名(类名.方法名)。

    示例:查看进程 13968 最近编译的方法:

    jstat -printcompilation 13968
    
    1

    image-20220131155122971

垃圾收集 (GC) 相关 (重点)

这是 jstat 最常用的功能,有多个选项可以从不同角度观察 GC 和内存情况。

  • -gc: 显示堆内存各个区域的容量、已使用空间以及 GC 的统计信息。这是最常用的 GC 监控选项之一。

    示例代码 (GCTest.java):一个不断创建对象的程序,用于模拟内存增长和 GC。

    import java.util.ArrayList;
    
    /**
     * 用于演示 jstat -gc* 相关命令的示例程序。
     * 不断创建 byte 数组对象并添加到 List 中,模拟内存占用增长,可能触发 GC。
     */
    public class GCTest {
        public static void main(String[] args) {
            ArrayList<byte[]> list = new ArrayList<>();
    
            for (int i = 0; i < 1000; i++) {
                // 每次循环创建一个 100KB 大小的 byte 数组
                byte[] arr = new byte[1024 * 100];
                list.add(arr);
                try {
                    // 短暂休眠,模拟应用运行过程中的停顿
                    Thread.sleep(120);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("GCTest 执行完毕");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    启动 JVM 参数:限制堆大小为 60MB。

    -Xms60m -Xmx60m -XX:SurvivorRatio=8
    
    1

    执行 jstat -gc 命令:假设 GCTest 进程 ID 为 13968,每秒输出一次,共输出 10 次。

    jstat -gc 13968 1000 10
    # 或者 jstat -gc 13968 1s 10
    
    1
    2

    输出解读: image-20220131160826978 图:jstat -gc 输出示例

    表头 含义 (单位: KB) 解释
    S0C 当前 Survivor 0 区的容量 (Capacity)
    S1C 当前 Survivor 1 区的容量 (Capacity)
    S0U Survivor 0 区已使用的大小 (Utilization)
    S1U Survivor 1 区已使用的大小 (Utilization)
    EC 当前 Eden 区的容量 (Capacity)
    EU Eden 区已使用的大小 (Utilization) 观察 EU 的增长和突然减少,可以判断 Minor GC 的发生
    OC 当前 Old (老年代) 区的容量 (Capacity)
    OU Old (老年代) 区已使用的大小 (Utilization) 观察 OU 的增长,判断对象是否晋升到老年代,持续增长可能意味着内存泄漏
    MC Metaspace (元空间) 的容量 (Capacity) (JDK 8 及以后)
    MU Metaspace (元空间) 已使用的大小 (Utilization)
    CCSC 压缩类空间 (Compressed Class Space) 的容量 (Capacity) (如果启用指针压缩)
    CCSU 压缩类空间已使用的大小 (Utilization)
    YGC 从 JVM 启动至今,Young GC (Minor GC) 的次数
    YGCT 从 JVM 启动至今,Young GC (Minor GC) 的总耗时 (秒) 结合 YGC 次数,可以计算平均单次 Minor GC 时间
    FGC 从 JVM 启动至今,Full GC 的次数 FGC 次数频繁是严重的性能问题
    FGCT 从 JVM 启动至今,Full GC 的总耗时 (秒) 单次 Full GC 耗时长会导致应用长时间卡顿 (STW)
    GCT 从 JVM 启动至今,所有 GC (YGC + FGC) 的总耗时 (秒)
  • -gccapacity: 与 -gc 类似,但主要关注堆各个区域的最小容量 (Min)、最大容量 (Max) 和当前容量 (Current)。还包括 GC 次数统计。有助于了解堆内存的配置和动态调整情况。

    示例:

    jstat -gccapacity 13968
    
    1

    image-20220131160931520 图:jstat -gccapacity 输出示例 (NGCMN: 年轻代最小容量, NGCMX: 年轻代最大容量, NGC: 当前年轻代容量, OGCMN: 老年代最小容量, OGCMX: 老年代最大容量, OGC: 当前老年代容量, MCMN: 元空间最小容量, MCMX: 元空间最大容量, MC: 当前元空间容量, CCSMN/CCSMC/CCSC: 压缩类空间相关)

  • -gcutil: 与 -gc 类似,但输出的是各区域已使用空间占总容量的百分比。更直观地了解内存使用率。

    示例:

    jstat -gcutil 13968 1s 5
    
    1

    image-20220131161054138 图:jstat -gcutil 输出示例

    表头 含义 (百分比)
    S0 Survivor 0 区已使用空间百分比
    S1 Survivor 1 区已使用空间百分比
    E Eden 区已使用空间百分比
    O Old (老年代) 区已使用空间百分比
    M Metaspace (元空间) 已使用空间百分比
    CCS 压缩类空间已使用空间百分比
    YGC Young GC 次数
    YGCT Young GC 总耗时 (秒)
    FGC Full GC 次数
    FGCT Full GC 总耗时 (秒)
    GCT GC 总耗时 (秒)
  • -gccause: 功能与 -gcutil 基本相同,但额外输出最后一次 GC 或当前正在发生的 GC 的原因 (LGCC - Last GC Cause, GCC - Current GC Cause)。有助于分析 GC 触发的具体场景(例如:Allocation Failure, System.gc(), Metadata GC Threshold 等)。

    示例:

    jstat -gccause 13968
    
    1

    image-20220131161322060 图:jstat -gccause 输出示例 (注意多了 LGCC 和 GCC 列)

  • -gcnew: 专注于新生代的 GC 状况。包括 S0/S1/Eden 区的容量、使用量,以及对象晋升到老年代的年龄 (TT, MTT) 和期望的 Survivor 空间占用 (DSS)。

    示例:

    jstat -gcnew 13968
    
    1

    image-20220131161525208

  • -gcnewcapacity: 与 -gcnew 类似,但主要关注新生代各区域的最小、最大和当前容量。

    示例:

    jstat -gcnewcapacity 13968
    
    1

    image-20220131161618811

  • -gcold: 专注于老年代的 GC 状况。包括老年代和元空间的容量、使用量以及 GC 次数和耗时。

    示例:

    jstat -gcold 13968
    
    1

    image-20220131161659710

  • -gcoldcapacity: 与 -gcold 类似,但主要关注老年代的最小、最大和当前容量。

    示例:

    jstat -gcoldcapacity 13968
    
    1

    image-20220131161741659

  • -gcpermcapacity (JDK 7及之前): 显示永久代 (Permanent Generation) 的容量信息。在 JDK 8 及之后被元空间 (Metaspace) 取代,此选项可能不再适用或输出内容变化。

其他通用参数:

  • interval: 输出统计数据的时间间隔,单位毫秒或秒。

  • count: 指定总共查询的次数。

  • -t: 在输出的第一列增加一个时间戳 (Timestamp),表示从 JVM 启动到当前采样点的时间(秒)。

    • 应用:结合 GCT(总 GC 时间)列,可以计算 GC 时间占总运行时间的比例。例如,比较两次采样点的 Timestamp 差值(运行时间增量)和 GCT 差值(GC 时间增量)。如果 GC 时间占比过高(如经验值超过 20%),说明堆内存压力较大;如果超过 90%,则可能即将 OOM。

    示例 (-gcutil 结合 -t):

    jstat -gcutil -t 13968 1s 5
    
    1

    image-20220131161938786 图:jstat -gcutil -t 输出示例 (第一列为 Timestamp)

  • -h<lines>: 在连续输出时,每隔 lines 行数据就重新打印一次表头,方便阅读。

    示例 (-gcutil 结合 -t 和 -h3):每 3 行数据输出一次表头。

    jstat -gcutil -t -h3 13968 1s 10
    
    1

    image-20220131162141993 图:jstat -h 参数示例

利用 jstat 判断内存泄漏

jstat 可以辅助初步判断是否存在内存泄漏:

  1. 获取基线:在 Java 程序稳定运行一段时间后,执行 jstat -gc <vmid> <interval> <count>(例如 jstat -gc 13968 5s 12,观察一分钟)。记录下这组数据中 OU (老年代已使用空间) 列的最小值。
  2. 长期观察:每隔一段较长的时间(例如几小时或一天),重复步骤 1,获取多组 OU 的最小值。
  3. 趋势分析:如果这些 OU 的最小值呈现持续上涨的趋势,即使经过了多次 Full GC (观察 FGC 次数增加) 也无法降下来,那么很可能存在内存泄漏——即不再使用的对象仍然被引用,无法被 GC 回收,导致老年代内存持续增长。

# 4. jinfo:实时查看与调整 JVM 参数

jinfo (Configuration Info for Java) 命令用于查看目标 Java 进程正在使用的 JVM 配置参数,并且可以在运行时动态地修改部分参数并使其立即生效。

用途:

  • 确认 JVM 当前实际使用的参数值(包括默认值和用户指定的)。
  • 在不重启服务的情况下,临时调整某些可管理的(manageable)JVM 参数。

基本语法:

jinfo [option] <pid>
1

必须提供目标 Java 进程的 PID。

option 参数详解:

选项 选项说明
(无选项) 输出目标 JVM 当前加载的所有配置参数 (VM Flags) 和 Java 系统属性 (System.getProperties())
-flag <name> 输出指定名称的 JVM 参数 (<name>) 的当前值。
-flag [+|-]<name> 动态启用 (+) 或禁用 (-) 指定名称的 可管理 (manageable) JVM 参数。
-flag <name>=<value> 动态修改指定名称的 可管理 (manageable) JVM 参数的值。
-flags 仅输出目标 JVM 当前生效的所有配置参数 (VM Flags)。
-sysprops 仅输出目标 JVM 当前的 Java 系统属性 (System.getProperties())。

示例:

假设 GCTest 进程 ID 为 13968。

  • 查看所有系统属性:

    jinfo -sysprops 13968
    
    1

    输出可能包含 java.version, os.name, user.dir 等大量系统属性。

    # 部分输出示例
    jboss.modules.system.pkgs = com.intellij.rt
    java.vendor = Oracle Corporation
    sun.java.launcher = SUN_STANDARD
    sun.management.compiler = HotSpot 64-Bit Tiered Compilers
    catalina.useNaming = true
    os.name = Windows 10
    ...
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 查看所有 VM Flags:

    jinfo -flags 13968
    
    1

    输出会列出所有非默认的 JVM 参数以及命令行参数。

    # 输出示例
    Attaching to process ID 13968, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.231-b11
    Non-default VM flags: -XX:CICompilerCount=4 -XX:InitialHeapSize=62914560 -XX:MaxHeapSize=62914560 -XX:MaxNewSize=20971520 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=20971520 -XX:OldSize=41943040 -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
    Command line:  -Xms60m -Xmx60m -XX:SurvivorRatio=8 -javaagent:F:\Programming area\Ideal\IntelliJ IDEA 2020.3\lib\idea_rt.jar=55919:F:\Programming area\Ideal\IntelliJ IDEA 2020.3\bin -Dfile.encoding=UTF-8
    
    1
    2
    3
    4
    5
    6
    7
  • 查看特定 Flag 的值:

    # 查看是否启用了 ParallelGC (Parallel Scavenge + Parallel Old)
    jinfo -flag UseParallelGC 13968
    # 输出: -XX:+UseParallelGC (表示已启用)
    
    # 查看是否启用了 G1 GC
    jinfo -flag UseG1GC 13968
    # 输出: -XX:-UseG1GC (表示未启用)
    
    1
    2
    3
    4
    5
    6
    7
  • 动态修改可管理 Flag (以 PrintGCDetails 为例,它通常是 manageable 的):

    # 1. 启用 PrintGCDetails
    jinfo -flag +PrintGCDetails 13968
    
    # 2. 验证是否已启用
    jinfo -flag PrintGCDetails 13968
    # 输出: -XX:+PrintGCDetails
    
    # 3. 禁用 PrintGCDetails
    jinfo -flag -PrintGCDetails 13968
    
    # 4. 验证是否已禁用
    jinfo -flag PrintGCDetails 13968
    # 输出: -XX:-PrintGCDetails
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    注意:并非所有参数都支持运行时修改。只有被 JVM 标记为 manageable 的参数才可以。GC 策略相关的参数(如 UseG1GC)通常不是 manageable 的,不能在运行时更改。

扩展:查找 JVM 参数信息

除了 jinfo,还可以用 java 命令本身来查看 JVM 参数信息:

  • java -XX:+PrintFlagsInitial: 打印所有 JVM 参数的初始默认值。

    # 部分输出示例
    [Global flags]
         intx ActiveProcessorCount                      = -1                                  {product}
        uintx AdaptiveSizeDecrementScaleFactor          = 4                                   {product}
        uintx AdaptiveSizeMajorGCDecayTimeScale         = 10                                  {product}
        uintx AdaptiveSizePausePolicy                   = 0                                   {product}
    ...
    
    1
    2
    3
    4
    5
    6
    7
  • java -XX:+PrintFlagsFinal: 打印所有 JVM 参数在加载用户配置后的最终生效值。如果值与默认值不同,会用 := 标出。

    # 部分输出示例
    [Global flags]
         intx ActiveProcessorCount                      = -1                                  {product}
    ...
         intx CICompilerCount                          := 4                                   {product} # 被修改过的值
        uintx InitialHeapSize                          := 333447168                           {product} # 被修改过的值
        uintx MaxHeapSize                              := 1029701632                          {product} # 被修改过的值
        uintx MaxNewSize                               := 1774714880                          {product}
    
    1
    2
    3
    4
    5
    6
    7
    8
  • java -XX:+PrintCommandLineFlags: 打印出由用户显式设置或 JVM 根据环境自动调整的 -XX 参数及其值。这对于了解哪些参数影响了最终配置很有帮助。

    # 输出示例
    -XX:InitialHeapSize=332790016 -XX:MaxHeapSize=5324640256 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
    
    1
    2
  • 查找 manageable 参数:可以通过 PrintFlagsFinal 结合 grep (或 Windows findstr) 来查找哪些参数是可以在运行时修改的。

    # Linux/macOS
    java -XX:+PrintFlagsFinal -version | grep manageable
    
    # Windows
    java -XX:+PrintFlagsFinal -version | findstr manageable
    
    1
    2
    3
    4
    5

# 5. jmap:导出堆转储快照与分析内存使用

官方文档参考 (Java 11): https://docs.oracle.com/en/java/javase/11/tools/jmap.html

jmap (JVM Memory Map) 是一个非常重要的工具,主要有两个核心功能:

  1. 生成堆转储快照 (Heap Dump):将某一时刻 JVM 堆内存中的对象信息完整地保存到一个二进制文件中(通常以 .hprof 结尾)。这是分析内存泄漏、查找大对象、诊断 OOM 等内存问题的最权威依据。
  2. 查询内存相关信息:获取目标 Java 进程的堆内存使用情况、对象统计信息、类加载信息等。

基本语法:

jmap [option] <pid>                   # 本地进程
jmap [option] <executable> <core>       # 对核心转储文件操作
jmap [option] [server_id@]<remote-hostname-or-IP> # 远程进程 (需 JMX 或 jstatd 支持)
1
2
3

option 参数详解:

选项 作用 平台限制
-dump:<options> 生成堆转储快照 (heap dump)。常用子选项:
live: 只 dump 堆中的存活对象 (推荐,文件更小,分析更快)。
format=b: 指定输出为二进制格式。
file=<filename>: 指定 dump 文件名。
通用
-heap 输出 Java 堆的详细信息,包括使用的 GC 算法、堆配置参数 (Min/Max Heap Size, NewRatio 等)、各区域 (Eden, Survivor, Old) 的容量和使用情况。 通用
-histo[:live] 输出堆中对象的统计直方图。按类名、实例数量、总占用字节数排序。添加 :live 只统计存活对象。非常适合快速查找占用内存最多的对象类型。 通用
-finalizerinfo 显示在 F-Queue 中等待 Finalizer 线程执行 finalize() 方法的对象信息。 Linux/Solaris
-permstat (JDK 7及之前) 以 ClassLoader 为统计口径,输出永久代 (PermGen) 的内存状态信息。 Linux/Solaris
-F 强制模式。当目标 JVM 进程对正常的 -dump 请求没有响应时(例如进程挂起),可以尝试使用此选项强制生成 dump 文件。可能导致 dump 文件不完整或不一致。 Linux/Solaris
-J<flag> 向运行 jmap 的 JVM 传递参数,例如 -J-Xmx512m。 通用
-h | -help 显示 jmap 的帮助信息。 通用

核心用法一:导出堆转储快照 (Heap Dump)

  • 手动导出:

    # 导出所有对象 (包括已死亡但未回收的) 到文件 3.hprof
    jmap -dump:format=b,file=d:\3.hprof 13968
    
    # 仅导出存活对象 (推荐) 到文件 4.hprof
    jmap -dump:live,format=b,file=d:\4.hprof 13968
    
    1
    2
    3
    4
    5

    image-20220131172515141 图:使用 jmap -dump 命令

    执行后会在 D 盘生成 dump 文件。通常 live 模式生成的 dump 文件会比全量 dump 小,尤其是在 GC 刚发生后。

    image-20220131173748601 图:生成的 dump 文件

  • 自动导出 (OOM 时): 在 JVM 启动参数中配置,可以在发生 OutOfMemoryError 时自动生成 dump 文件,非常适合生产环境排查问题。

    • -XX:+HeapDumpOnOutOfMemoryError: 启用 OOM 时自动 dump。
    • -XX:HeapDumpPath=<path/to/filename.hprof>: 指定 dump 文件的生成路径和文件名。可以使用 %p 占位符表示进程 ID。

    示例代码 (GCTest.java - 可能导致 OOM):

    import java.util.ArrayList;
    
    /**
     * 持续分配内存,可能导致 OOM,用于演示自动 Heap Dump。
     */
    public class GCTest {
        public static void main(String[] args) {
            ArrayList<byte[]> list = new ArrayList<>();
            System.out.println("GCTest 开始分配内存...");
            try {
                for (int i = 0; i < 1000; i++) { // 循环次数可能需要调整以确保 OOM
                    byte[] arr = new byte[1024 * 100]; // 100KB
                    list.add(arr);
                    Thread.sleep(60); // 稍微减慢分配速度
                }
            } catch (OutOfMemoryError oom) {
                System.err.println("发生 OutOfMemoryError!");
                // oom.printStackTrace(); // 可以选择性打印堆栈
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("GCTest 执行结束。");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    启动 JVM 参数:限制堆大小并启用自动 Dump。

    -Xms60m -Xmx60m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\oom_dump_%p.hprof
    
    1

    运行程序,当发生 OOM 时,控制台会打印错误,并且在 D 盘会自动生成类似 oom_dump_xxxxx.hprof 的文件。

    image-20220131173702078 图:OOM 时自动生成的 dump 文件

核心用法二:显示堆内存相关信息

  • jmap -heap <pid>: 查看堆的整体配置和使用情况。
  • jmap -histo <pid>: 查看堆中对象的直方图统计。

示例:

# 查看进程 1904 的堆信息,并将结果重定向到 a.txt
jmap -heap 1904 > a.txt

# 查看进程 1904 的对象直方图,并将结果重定向到 b.txt
jmap -histo 1904 > b.txt
1
2
3
4
5

image-20220131211705430

a.txt (-heap 输出) 内容解读:

Attaching to process ID 1904, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.231-b11

// 使用的内存分配方式
using thread-local object allocation.
// 使用的 GC 收集器 (这里是 Parallel GC,包含 Parallel Scavenge + Parallel Old)
Parallel GC with 10 thread(s)

// ---- 堆配置信息 (Heap Configuration) ----
Heap Configuration:
   MinHeapFreeRatio         = 0             // 最小堆空闲比例
   MaxHeapFreeRatio         = 100           // 最大堆空闲比例
   MaxHeapSize              = 4255121408 (4058.0MB) // 最大堆内存 (-Xmx)
   NewSize                  = 88604672 (84.5MB)   // 新生代初始大小 (-Xmn 或通过 NewRatio 计算)
   MaxNewSize               = 1418199040 (1352.5MB) // 新生代最大大小
   OldSize                  = 177733632 (169.5MB)  // 老年代大小
   NewRatio                 = 2             // 新生代与老年代比例 (老年代/新生代)
   SurvivorRatio            = 8             // Eden 区与 Survivor 区比例 (Eden/Survivor)
   MetaspaceSize            = 21807104 (20.796875MB) // 元空间初始大小
   CompressedClassSpaceSize = 1073741824 (1024.0MB) // 压缩类空间大小
   MaxMetaspaceSize         = 17592186044415 MB   // 元空间最大大小 (非常大,接近无限)
   G1HeapRegionSize         = 0 (0.0MB)     // G1 GC 的 Region 大小 (未使用 G1 时为 0)

// ---- 堆使用情况 (Heap Usage) ----
Heap Usage:
PS Young Generation // Parallel Scavenge 管理的新生代
Eden Space: // Eden 区
   capacity = 66584576 (63.5MB)     // 当前容量
   used     = 31136256 (29.69384765625MB) // 已使用
   free     = 35448320 (33.80615234375MB) // 剩余空间
   46.761964812992126% used        // 使用率
From Space: // Survivor From 区
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
To Space: // Survivor To 区
   capacity = 11010048 (10.5MB)
   used     = 0 (0.0MB)
   free     = 11010048 (10.5MB)
   0.0% used
PS Old Generation // Parallel Old 管理的老年代
   capacity = 177733632 (169.5MB)
   used     = 0 (0.0MB)
   free     = 177733632 (169.5MB)
   0.0% used

// ---- 字符串常量池信息 ----
3163 interned Strings occupying 259640 bytes. // 字符串常量池中有 3163 个字符串,占用约 259KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

b.txt (-histo 输出) 内容解读:

 num     #instances         #bytes  class name          // 表头:序号、实例数量、总字节数、类名
----------------------------------------------
   1:          1610       44293784  [B                  // 排名第一的是 byte[] 数组,有 1610 个实例,总共占用约 44MB
   2:           659        2906760  [I                  // int[] 数组
   3:          7774         906192  [C                  // char[] 数组 (通常由 String 内部持有)
   4:          5933         142392  java.lang.String     // String 对象本身 (不含内部 char[])
   5:           691          79112  java.lang.Class      // Class 对象
   6:          1305          67680  [Ljava.lang.Object; // Object[] 数组
   7:           791          31640  java.util.TreeMap$Entry
   8:           628          25120  java.util.LinkedHashMap$Entry
   9:           456          22168  [Ljava.lang.String; // String[] 数组
  10:           364          11648  java.util.HashMap$Node
  ... (省略后续内容) ...
Total         24100       48649392                     // 汇总:总共有 24100 个对象实例,总共占用约 48MB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • [B 表示 byte[] 数组。
  • [I 表示 int[] 数组。
  • [C 表示 char[] 数组。
  • [L<classname>; 表示该类的对象数组,例如 [Ljava.lang.String; 就是 String[]。

-histo 的输出对于快速定位哪个类的实例最多或哪个类的实例总占用空间最大非常有用,是排查内存问题的常用手段。如果加上 :live,则只统计 GC 后仍然存活的对象。

jmap 的注意事项(重要)

  • Stop-The-World (STW): jmap(尤其是 -dump 和 -histo)需要暂停目标 JVM 的所有应用线程(进入安全点 SafePoint)来保证内存数据的一致性。在生产环境中对一个繁忙的应用执行 jmap 可能会导致较长时间的卡顿,需要谨慎使用。
  • 安全点偏差 (Safepoint Bias): 由于 jmap 获取的是安全点时刻的快照,那些生命周期恰好在两个安全点之间的对象可能不会被统计到(尤其是在使用 :live 选项时),这可能导致分析结果存在一定的偏差。
  • 等待安全点: 如果某个线程长时间无法到达安全点(例如执行 JNI 本地代码或处于长时间循环中),jmap 命令可能会一直阻塞等待。

相比之下,jstat 通常对应用影响较小,因为它读取的是 JVM 主动维护的性能计数器,不需要长时间暂停应用。

# 6. jhat:交互式堆转储快照分析工具 (已移除)

jhat (JVM Heap Analysis Tool) 是早期 JDK 提供的一个与 jmap 配套使用的工具,用于分析 jmap 生成的堆转储快照 (.hprof 文件)。

jhat 的主要特点是它会解析 dump 文件,并在本地启动一个微型的 HTTP/HTML 服务器(默认端口 7000)。用户可以通过浏览器访问该服务器,交互式地查看分析结果,例如查看类信息、对象实例、引用关系,甚至执行对象查询语言 (OQL)。

重要说明:jhat 命令在 JDK 9 及之后的版本中已经被移除。官方推荐使用更强大的内存分析工具,如 Eclipse Memory Analyzer (MAT) 或 VisualVM 来替代。jhat 在分析大型堆 dump 文件时性能较差,且功能相对有限。

基本语法 (仅适用于 JDK 8 及之前版本):

jhat [options] <dumpfile>
1
  • <dumpfile>: jmap 生成的 .hprof 文件路径。

常用 options 参数:

option 参数 作用
-stack false|true 关闭/打开对象分配调用栈跟踪 (分析需要 dump 文件包含栈信息)
-refs false|true 关闭/打开对象引用跟踪
-port <port-number> 设置 jhat HTTP 服务器的端口号,默认为 7000。
-exclude <file> 指定一个文件,列出在对象查询时需要排除的数据成员(属性)。
-baseline <file> 指定一个基准堆转储文件,用于对比分析两个 dump 文件。
-J<flag> 向运行 jhat 的 JVM 传递参数,例如 -J-Xmx1024m (处理大 dump 时可能需要调大内存)。

示例 (假设使用 JDK 8):

# 分析 D 盘的 3.hprof 文件
jhat d:\3.hprof
1
2

命令行会显示启动信息,并提示服务器正在监听 7000 端口:

image-20220131212831537 图:jhat 启动信息

然后,在浏览器中访问 http://localhost:7000/:

image-20220131212920407 图:jhat 的 Web 分析界面

界面提供了多种链接来查看堆信息:

  • Show heap histogram: 显示与 jmap -histo 类似的对象统计直方图。
  • Show finalizer summary: 查看等待 finalize 的对象。
  • Execute Object Query Language (OQL) query: 执行 OQL 查询。

OQL 查询示例:查找所有长度大于 100 的 String 对象。

select s from java.lang.String s where s.value.length > 100
1

# 7. jstack:捕获 JVM 线程快照 (Thread Dump)

官方文档参考 (Java 11): https://docs.oracle.com/en/java/javase/11/tools/jstack.html

jstack (JVM Stack Trace) 是用于生成目标 Java 进程在某一时刻的线程快照 (Thread Dump) 的命令。线程快照包含了该进程内每一条线程当前正在执行的方法堆栈信息的集合。

核心用途:

  • 定位线程长时间停顿的原因:例如:
    • 线程间死锁 (Deadlock):多个线程互相等待对方持有的锁。
    • 死循环 (Infinite Loop):线程在代码中无限循环,消耗 CPU。
    • 等待外部资源:线程等待网络 I/O、数据库响应、磁盘读写等,导致长时间阻塞或等待。
    • 等待锁 (Monitor Entry / Wait):线程等待获取某个对象的监视器锁,或调用了 Object.wait() / Condition.await() 等待通知。
  • 分析 CPU 占用过高:结合操作系统的 top / prstat 等命令找到高 CPU 占用的 Java 线程(得到其原生线程 ID),再用 jstack 找到对应的 Java 线程堆栈,分析其正在执行的代码。

线程状态关注点:

在分析 jstack 输出的线程 dump 时,需要特别关注以下几种线程状态:

  • Deadlock: 死锁状态,jstack 会明确指出死锁涉及的线程和锁对象,是最需要优先处理的问题。
  • WAITING (on object monitor) / TIMED_WAITING (on object monitor): 线程调用了 Object.wait() 或 LockSupport.park() 等待被唤醒。需要分析为何长时间未被唤醒。
  • WAITING (parking) / TIMED_WAITING (parking): 通常是调用了 LockSupport.park() 系列方法,常见于 JUC 包中的锁和同步器。
  • BLOCKED (on object monitor): 线程正在等待进入 synchronized 代码块/方法,因为锁被其他线程持有。大量线程处于 BLOCKED 状态通常意味着激烈的锁竞争。
  • RUNNABLE: 线程处于可运行状态,可能正在执行代码,也可能在等待操作系统分配 CPU 时间片。如果一个线程长时间处于 RUNNABLE 状态且 CPU 占用高,可能是在执行计算密集型任务或死循环。
  • TIMED_WAITING (sleeping): 线程调用了 Thread.sleep()。
  • NEW: 线程已创建但尚未启动。
  • TERMINATED: 线程已执行完毕。

基本语法:

jstack [option] <pid>
1

option 参数详解:

option 参数 作用
-F 强制模式。当正常的 jstack 请求没有响应时(例如进程挂起),强制输出线程堆栈。
-l 显示关于锁的附加信息。除了堆栈,还会打印出 locked <addr> (a <classname>) 和 waiting to lock <addr> (a <classname>) 等锁信息,对于分析锁竞争和死锁非常有帮助。强烈推荐使用!
-m 混合模式。如果线程调用了 JNI (Java Native Interface) 本地方法,同时打印 Java 堆栈和 C/C++ 堆栈信息(需要操作系统支持)。

代码示例:模拟死锁 (ThreadDeadLock.java)

import java.util.Map;
import java.util.Set;

/**
 * 演示线程死锁的示例代码。
 * 两个线程互相持有对方需要的锁,导致死锁。
 */
public class ThreadDeadLock {

    public static void main(String[] args) {

        final StringBuilder s1 = new StringBuilder(); // 锁对象1
        final StringBuilder s2 = new StringBuilder(); // 锁对象2

        // 线程 1
        new Thread("T1-持有s1等待s2") { // 给线程命名,方便 jstack 分析
            @Override
            public void run() {
                synchronized (s1) { // 获取 s1 的锁
                    s1.append("a");
                    s2.append("1");
                    System.out.println(Thread.currentThread().getName() + " 获取 s1,尝试获取 s2...");
                    try {
                        // 短暂休眠,增加死锁发生的概率
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 尝试获取 s2 的锁(此时 s2 可能被线程 2 持有)
                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        // 线程 2
        new Thread("T2-持有s2等待s1") { // 给线程命名
            @Override
            public void run() {
                synchronized (s2) { // 获取 s2 的锁
                    s1.append("c");
                    s2.append("3");
                    System.out.println(Thread.currentThread().getName() + " 获取 s2,尝试获取 s1...");
                    try {
                        // 短暂休眠
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 尝试获取 s1 的锁(此时 s1 可能被线程 1 持有)
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        // 主线程休眠一会儿,等待死锁发生
        try {
            Thread.sleep(1000);
            System.out.println("死锁应该已经发生,准备执行 jstack...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // (可选) 在程序内部打印所有线程堆栈信息,效果类似 jstack
        /*
        new Thread(() -> {
            Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces();
            Set<Map.Entry<Thread, StackTraceElement[]>> entries = all.entrySet();
            for(Map.Entry<Thread, StackTraceElement[]> en : entries){
                Thread t = en.getKey();
                StackTraceElement[] v = en.getValue();
                System.out.println("\n【Thread name is :" + t.getName() + " ID:" + t.getId() + " State:" + t.getState() + "】");
                for(StackTraceElement s : v){
                    System.out.println("\t at " + s.toString());
                }
            }
        }).start();
        */
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

执行 jstack 命令:

运行 ThreadDeadLock 程序,它会很快进入死锁状态。然后通过 jps 找到该进程的 PID (假设为 22164),执行 jstack (推荐带 -l 参数):

# 使用 jps 找到 PID
jps

# 执行 jstack,并附带 -l 参数显示锁信息
jstack -l 22164
1
2
3
4
5

jstack -l 输出解读 (关键部分):








 






 





























2022-01-31 21:51:08
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.231-b11 mixed mode):

// ... (其他线程信息,如 VM Thread, GC threads, Compiler threads, Finalizer, Reference Handler 等)

// --- 线程 2 (T2) ---
"T2-持有s2等待s1" #13 prio=5 os_prio=0 tid=0x000000001e041800 nid=0x9bc waiting for monitor entry [0x000000001fd1f000]
   java.lang.Thread.State: BLOCKED (on object monitor) // 状态:阻塞,等待获取锁
        at com.youngkbt.jstack.ThreadDeadLock$2.run(ThreadDeadLock.java:63) // 阻塞发生在第 63 行 (尝试获取 s1)
        - waiting to lock <0x000000076ba1e2f0> (a java.lang.StringBuilder) // 正在等待获取地址为 ...e2f0 的 StringBuilder (s1) 的锁
        - locked <0x000000076ba1e338> (a java.lang.StringBuilder) // 当前持有地址为 ...e338 的 StringBuilder (s2) 的锁

// --- 线程 1 (T1) ---
"T1-持有s1等待s2" #12 prio=5 os_prio=0 tid=0x000000001e03b800 nid=0x52f8 waiting for monitor entry [0x000000001fc1f000]
   java.lang.Thread.State: BLOCKED (on object monitor) // 状态:阻塞,等待获取锁
        at com.youngkbt.jstack.ThreadDeadLock$1.run(ThreadDeadLock.java:35) // 阻塞发生在第 35 行 (尝试获取 s2)
        - waiting to lock <0x000000076ba1e338> (a java.lang.StringBuilder) // 正在等待获取地址为 ...e338 的 StringBuilder (s2) 的锁
        - locked <0x000000076ba1e2f0> (a java.lang.StringBuilder) // 当前持有地址为 ...e2f0 的 StringBuilder (s1) 的锁

// ... (其他线程信息)

// --- 死锁检测部分 ---
Found one Java-level deadlock:
=============================
"T2-持有s2等待s1":
  waiting to lock monitor 0x000000001e044d58 (object 0x000000076ba1e2f0, a java.lang.StringBuilder), // T2 等待锁 ...e2f0 (s1)
  which is held by "T1-持有s1等待s2" // 该锁被 T1 持有
"T1-持有s1等待s2":
  waiting to lock monitor 0x000000001c7c2dc8 (object 0x000000076ba1e338, a java.lang.StringBuilder), // T1 等待锁 ...e338 (s2)
  which is held by "T2-持有s2等待s1" // 该锁被 T2 持有

Java stack information for the threads listed above:
===================================================
"T2-持有s2等待s1":
        at com.youngkbt.jstack.ThreadDeadLock$2.run(ThreadDeadLock.java:63)
        - waiting to lock <0x000000076ba1e2f0> (a java.lang.StringBuilder)
        - locked <0x000000076ba1e338> (a java.lang.StringBuilder)
"T1-持有s1等待s2":
        at com.youngkbt.jstack.ThreadDeadLock$1.run(ThreadDeadLock.java:35)
        - waiting to lock <0x000000076ba1e338> (a java.lang.StringBuilder)
        - locked <0x000000076ba1e2f0> (a java.lang.StringBuilder)

Found 1 deadlock. // 明确提示发现了 1 个死锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

解读:

  1. jstack 清晰地列出了 T1 和 T2 线程都处于 BLOCKED 状态。
  2. 通过 -l 参数提供的锁信息,我们可以看到 T1 持有 s1 (...e2f0) 的锁,并等待 s2 (...e338) 的锁;而 T2 持有 s2 (...e338) 的锁,并等待 s1 (...e2f0) 的锁。
  3. jstack 在输出的末尾直接检测并报告了 Java 级别的死锁,明确指出了涉及的线程和它们互相等待的锁对象。这极大地简化了死锁问题的定位。

其他示例代码 (用于练习 jstack):

  • 线程睡眠 (TreadSleepTest.java):

    /**
     * 演示线程长时间 sleep 的情况。
     * jstack 会显示 main 线程处于 TIMED_WAITING (sleeping) 状态。
     */
    public class TreadSleepTest {
        public static void main(String[] args) {
            System.out.println("hello - 1");
            try {
                // 让主线程睡眠 10 分钟
                Thread.sleep(1000 * 60 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hello - 2");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  • 线程同步 (ThreadSyncTest.java):

    /**
     * 演示多个线程竞争同一个锁 (synchronized)。
     * jstack 会显示一个线程持有锁处于 RUNNABLE 或 TIMED_WAITING (sleeping) 状态,
     * 而其他线程则处于 BLOCKED (on object monitor) 状态,等待获取锁。
     */
    public class ThreadSyncTest {
        public static void main(String[] args) {
            Number number = new Number();
            Thread t1 = new Thread(number, "线程1"); // 命名线程
            Thread t2 = new Thread(number, "线程2"); // 命名线程
    
            t1.start();
            t2.start();
        }
    }
    
    class Number implements Runnable {
        private int number = 1;
        private final Object lock = new Object(); // 或者直接 synchronized(this)
    
        @Override
        public void run() {
            while (true) {
                synchronized (lock) { // 同步块,保证 number 操作的原子性
                    if (number <= 100) {
                        try {
                            // 模拟耗时操作
                            Thread.sleep(10); // 减少 sleep 时间以增加竞争
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + ":" + number);
                        number++;
                    } else {
                        break; // 结束循环
                    }
                } // 锁在这里释放
                 try {
                     // 在锁外 sleep,减少持有锁的时间,但仍可能观察到 BLOCKED
                     Thread.sleep(5);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46

# 8. jcmd:多功能 JVM 诊断命令

官方文档参考 (Java 11): https://docs.oracle.com/en/java/javase/11/tools/jcmd.html

jcmd (Java Command) 是 JDK 1.7 及以后版本引入的一个多功能命令行工具。它的目标是整合其他多个命令的功能(除了 jstat 的实时统计流),提供一个统一的接口来与 JVM 进行交互和诊断。Oracle 官方也推荐使用 jcmd 来替代 jmap 的部分功能。

核心优势:

  • 功能整合:可以用 jcmd 执行许多原本需要 jps, jinfo, jmap, jstack 才能完成的操作。
  • 无需 JMX:与 jinfo, jmap 等不同,jcmd 发送命令给目标 JVM 进程时,不需要 JMX Agent 的支持,减少了配置的复杂性。
  • 动态加载:支持向运行中的 JVM 请求执行特定的诊断命令。

常用命令格式:

  1. 列出所有 Java 进程 (jcmd -l):功能类似于 jps。

    jcmd -l
    
    1

    image-20220131220313703 图:jcmd -l 输出示例

  2. 查看指定进程支持的命令 (jcmd <pid> help):列出目标 JVM 实例当前可用的所有 jcmd 命令。

    jcmd 10561 help
    
    1

    image-20220131220228871 图:jcmd help 输出示例

  3. 执行具体诊断命令 (jcmd <pid> <command> [arguments]):向目标进程发送并执行具体的诊断命令。

常用 jcmd 命令及其替代功能:

  • jcmd <pid> Thread.print: 替代 jstack <pid>,打印线程堆栈。可以加上 -l 参数获取锁信息 (jcmd <pid> Thread.print -l)。
  • jcmd <pid> GC.class_histogram: 替代 jmap -histo <pid>,打印堆对象直方图。
  • jcmd <pid> GC.heap_dump <filename>: 替代 jmap -dump:format=b,file=<filename> <pid>,生成堆转储快照。
  • jcmd <pid> GC.run: 手动触发一次 Full GC (谨慎在生产环境使用)。
  • jcmd <pid> GC.run_finalization: 手动触发 System.runFinalization()。
  • jcmd <pid> VM.uptime: 查看 JVM 的启动时长,类似 jstat -t 的第一列。
  • jcmd <pid> VM.system_properties: 替代 jinfo -sysprops <pid>,查看 Java 系统属性。
  • jcmd <pid> VM.flags [-all]: 替代 jinfo -flags <pid>,查看 JVM 参数。-all 显示所有参数(包括默认值)。
  • jcmd <pid> VM.command_line: 查看 JVM 启动的完整命令行参数。
  • jcmd <pid> VM.version: 查看 JVM 版本信息。

jcmd 示例:

  • 替代 jmap -histo:

    # 打印进程 10561 的对象直方图
    jcmd 10561 GC.class_histogram
    
    1
    2

    输出结果与 jmap -histo 类似。

  • 替代 jmap -dump:

    # 将进程 10561 的堆 dump 到 D 盘的 m.hprof 文件
    jcmd 10561 GC.heap_dump d:\m.hprof
    
    1
    2

    执行后会在 D 盘生成 m.hprof 文件。

# 9. jstatd:开启远程 JVM 监控

jstatd (Java Statistics Monitoring Daemon) 是一个 RMI (Remote Method Invocation) 服务端程序。它的主要作用是允许远程监控工具(如 jps, jstat, VisualVM 等)连接到本机,并获取本机上运行的 Java 应用程序的性能数据。

简单来说,jstatd 扮演了一个代理服务器的角色,负责收集本机 JVM 的信息,并通过 RMI 协议将其暴露给远程连接的客户端。

image-20220131221034831 图:jstatd 工作原理示意图

启动 jstatd:

启动 jstatd 通常需要指定安全策略文件,以控制哪些远程主机可以连接以及允许执行哪些操作。一个简单的(但不安全的)策略文件 jstatd.all.policy 可能如下:

grant codebase "file:${java.home}/../lib/tools.jar" {
   permission java.security.AllPermission;
};
1
2
3

启动命令示例:

# -J-Djava.security.policy 指定策略文件
# -p 指定 RMI 端口 (可选,默认 1099)
jstatd -J-Djava.security.policy=jstatd.all.policy
1
2
3

使用远程工具连接:

启动 jstatd 后,就可以在另一台机器上使用 jps 或 jstat 等工具,通过 hostid 参数连接到运行 jstatd 的机器了。

# 查看远程主机 a.b.c.d 上的 Java 进程
jps a.b.c.d

# 监控远程主机 a.b.c.d 上 PID 为 1234 的进程的 GC 情况
jstat -gc 1234@a.b.c.d 1s
1
2
3
4
5

安全警告:jstatd 暴露了本地 JVM 的大量信息,直接在生产环境或不受信任的网络中运行 jstatd(尤其是使用 AllPermission 策略)存在严重的安全风险。建议优先考虑使用 SSH 隧道或其他更安全的监控方案(如 JMX + 认证/SSL,或 APM 系统)。如果必须使用 jstatd,务必配置严格的安全策略。

编辑此页 (opens new window)
上次更新: 2025/04/05, 20:16:54
JVM - 调优概述
JVM - 监控及诊断工具GUI

← JVM - 调优概述 JVM - 监控及诊断工具GUI→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式