运维工程师技能之JVM

文章目录

  • 前言
  • [一、前置基础:JVM 核心认知(懂概念,能对应线上问题)](#一、前置基础:JVM 核心认知(懂概念,能对应线上问题))
    • [JVM 运行时数据区(核心关注异常高发区域)](#JVM 运行时数据区(核心关注异常高发区域))
        • [一、 堆(Heap)是什么?](#一、 堆(Heap)是什么?)
        • [二、 堆(Heap)的具体作用](#二、 堆(Heap)的具体作用)
        • [三、 为什么堆最容易出问题?](#三、 为什么堆最容易出问题?)
      • 元空间(metaspace,替代永久代,对应图中的方法区)
        • [一、 元空间是什么?](#一、 元空间是什么?)
        • [二、 元空间的具体作用](#二、 元空间的具体作用)
        • [三、 为什么元空间容易出问题?](#三、 为什么元空间容易出问题?)
      • 虚拟机栈
        • [一、 虚拟机栈是什么?](#一、 虚拟机栈是什么?)
        • [二、 虚拟机栈的具体作用](#二、 虚拟机栈的具体作用)
        • [三、 为什么虚拟机栈容易出问题?](#三、 为什么虚拟机栈容易出问题?)
    • [GC 基础(懂类型、分代、核心流程)](#GC 基础(懂类型、分代、核心流程))
      • [一、 GC 分代模型:核心是 "按对象存活时间划分内存,提升回收效率"](#一、 GC 分代模型:核心是 “按对象存活时间划分内存,提升回收效率”)
      • [二、 常见垃圾收集器:核心是 "选对场景,无需深究实现"](#二、 常见垃圾收集器:核心是 “选对场景,无需深究实现”)
      • [三、 核心认知(运维实操关键)](#三、 核心认知(运维实操关键))
  • [二、 核心技能 1:JDK 自带命令行工具(运维必备,实操核心)](#二、 核心技能 1:JDK 自带命令行工具(运维必备,实操核心))
    • [一、 JDK 核心运维命令详解](#一、 JDK 核心运维命令详解)
      • [1. `jps`:Java 进程快速查询工具](#1. jps:Java 进程快速查询工具)
      • [2. `jstat`:Java 运行状态监控工具](#2. jstat:Java 运行状态监控工具)
      • [3. `jstack`:Java 线程堆栈分析工具](#3. jstack:Java 线程堆栈分析工具)
      • [4. `jmap`:Java 堆内存分析工具](#4. jmap:Java 堆内存分析工具)
      • [5. `jcmd`:Java 多功能综合命令](#5. jcmd:Java 多功能综合命令)
    • [二、 运维实操实验(掌握 Java 应用问题排查全流程)](#二、 运维实操实验(掌握 Java 应用问题排查全流程))
      • 实验目标
      • 实验环境准备
      • 实验步骤(有两个场景)
        • [场景 1:线程死锁排查(核心命令:jps + jstack + jcmd)](#场景 1:线程死锁排查(核心命令:jps + jstack + jcmd))
        • [场景 2:内存泄漏前兆排查(核心命令:jps + jstat + jmap)](#场景 2:内存泄漏前兆排查(核心命令:jps + jstat + jmap))

前言

作为运维工程师,JVM 学习无需深入底层开发实现,核心围绕 "监控异常、定位问题、应急处置、配置优化、日常运维" 展开,重点解决线上 Java 应用的可用性和性能问题


一、前置基础:JVM 核心认知(懂概念,能对应线上问题)

JVM 运行时数据区(核心关注异常高发区域)

  • 重点掌握:堆(Heap)、元空间(Metaspace,替代永久代,对应图中的方法区)、虚拟机栈
  • 核心对应:
    • 堆:OOM 异常(内存溢出 / 泄漏)的核心发生区域,线上最高频问题
    • 元空间:动态类加载导致的 OOM 异常(如 Spring Boot 热部署、反射框架)
    • 虚拟机栈:StackOverflowError(无限递归)、栈内存不足(业务调用链过长)
  • 非重点:程序计数器、本地方法栈(几乎无运维相关异常)

一、 堆(Heap)是什么?
  1. JVM 运行时最大、全局共享的内存区域(所有 Java 线程共用);
  2. 启动时由 -Xms 初始化,上限由 -Xmx 控制,逻辑上分为年轻代(Eden+S0+S1)和老年代;
  3. 专门存储 Java 对象实例(如 new 创建的对象、数组),由 GC 自动回收无用对象,无需手动管理。
二、 堆(Heap)的具体作用
  1. 核心容器:承载 Java 应用运行时的所有对象实例(业务对象、框架对象等),是应用运行的内存基石;
  2. 生命周期管理:通过分代模型 + GC,自动完成对象的存活判断、转移(年轻代→老年代)和无用对象回收;
  3. 保障可用性:堆内存充足与否直接决定应用能否正常运行,内存不足会直接导致应用闪退。
三、 为什么堆最容易出问题?
  1. 承载压力最大:内存占比最高(占 JVM 总内存 80% 以上),又是全局共享资源,对象创建压力全部集中在此;
  2. 对象管理复杂:
  • 高并发场景下对象创建频繁,若创建速度超过 GC 回收速度,会快速耗尽内存;
  • 内存泄漏(无用对象被长期引用)导致 "僵尸对象" 堆积,逐渐耗尽堆内存;
  • 大对象直接进入老年代,易快速占满老年代触发 Full GC 甚至 OOM;
  1. GC 机制存在天然瓶颈:
  • 年轻代过小导致 Minor GC 频繁,占用大量 CPU;
  • 老年代不足触发 Full GC(STW 停顿),导致应用卡顿;
  • 非 G1/ZGC 收集器易产生内存碎片,导致 "内存充足但无法分配大对象" 的 OOM;
  1. 配置失误高发:-Xms/-Xmx 配置不合理、收集器选型错误等,会放大堆内存问题,加速异常发生。

元空间(metaspace,替代永久代,对应图中的方法区)

一、 元空间是什么?
  1. JVM 中用于替代永久代的内存区域,本质是本地内存(直接占用服务器物理内存,非 JVM 堆内存);
  2. 由 XX:MetaspaceSize(初始阈值)和 XX:MaxMetaspaceSize(最大上限)控制,默认无最大上限(可能耗尽系统内存);
  3. 线程私有(非全局共享),专门存储类的元数据信息,无需手动管理,由 GC 自动回收无用类元数据。
二、 元空间的具体作用
  1. 存储类元数据:核心承载 Java 类的结构信息(类名、方法定义、字段信息、常量池、注解等);
  2. 支撑类加载机制:Java 程序运行时(类加载阶段),将类的.class 文件解析后,元数据存入元空间,保障类的正常加载和使用;
  3. 避免永久代缺陷:突破永久代内存上限限制,减少因类元数据堆积导致的早期 OOM 问题。
三、 为什么元空间容易出问题?
  1. 配置缺失高发:默认无最大内存上限,若未配置 XX:MaxMetaspaceSize,元空间会持续占用服务器物理内存,最终耗尽系统内存导致应用 / 服务器宕机;
  2. 类加载异常场景多:
  • 动态类加载频繁(如 Spring 热部署、反射框架、ASM 字节码生成、微服务频繁重启),导致类元数据急剧飙升,快速占满元空间;
  • 类加载后未正常卸载(如类加载器内存泄漏),无用类元数据无法回收,形成 "僵尸元数据" 堆积;
  1. 初始阈值配置不合理:XX:MetaspaceSize 过小,导致元空间频繁扩容,每次扩容都会触发 Full GC,造成应用卡顿;
  2. 监控缺失:运维常聚焦堆内存监控,忽略元空间监控,异常发生前无预警,出现问题时已导致应用闪退。

虚拟机栈

一、 虚拟机栈是什么?
  1. 线程私有内存区域(每个 Java 线程对应一个独立虚拟机栈,互不干扰);
  2. 由 -Xss 参数控制单个栈的内存大小(默认 1M),无全局上限;
  3. 以 "栈帧" 为基本单位,方法调用时创建栈帧、方法执行完销毁栈帧。
二、 虚拟机栈的具体作用
  1. 支撑方法调用:承载 Java 方法的执行流程,方法调用对应栈帧入栈,方法返回对应栈帧出栈;
  2. 存储局部数据:每个栈帧中存储方法的局部变量、方法执行状态(如指令指针、操作数栈),保障方法正常执行;
  3. 隔离线程数据:线程私有特性避免了方法执行过程中的数据干扰,保障多线程运行安全。
三、 为什么虚拟机栈容易出问题?
  1. 方法调用深度超限:无限递归、超长业务调用链(如多层微服务嵌套调用),导致栈帧不断入栈,耗尽栈内存,触发 StackOverflowError;
  2. 栈内存配置过小:-Xss 参数配置过低(如小于 1M),对于方法调用层级深、局部变量多的场景,易触发栈内存不足异常;
  3. 阻塞场景堆积:线程长期处于阻塞状态(如锁等待、IO 阻塞),对应的虚拟机栈长期占用内存,若线程数量过多,会间接耗尽服务器物理内存;
  4. 排查难度较高:栈异常多与业务代码逻辑(递归、调用链)强相关,运维无法直接通过配置优化解决,需结合线程快照(jstack)定位问题。

GC 基础(懂类型、分代、核心流程)

GC:garbage collection,垃圾回收,是JVM自动内存管理的重要机制之一

一、 GC 分代模型:核心是 "按对象存活时间划分内存,提升回收效率"

JVM 堆内存并非整体管理,而是按对象存活时间逻辑划分为年轻代和老年代,对应两种核心 GC 类型,这是 GC 操作的基础:

  1. 内存划分
  • 年轻代:分为 Eden 区(占 80%)、Survivor 0(S0,10%)、Survivor 1(S1,10%),专门存储新创建的对象(生命周期短,大部分对象创建后很快无用);
  • 老年代(Tenured):存储从年轻代 "存活下来" 的对象(生命周期长,如缓存对象、全局对象)。
  1. 两种核心 GC 的区别(运维重点关注)
类型 回收区域 特点(运维视角) 异常信号
Minor GC 仅年轻代 执行快、停顿短(毫秒级)、频率高 每秒多次执行 → 年轻代过小
Full GC 整堆(年轻代 + 老年代 + 元空间) 执行慢、停顿长(秒级)、频率低 1 小时超过 3 次 → 内存瓶颈
  1. 执行快:指回收时只需要扫描一小块内存,且存货对象少、处理逻辑简单
  2. 停顿短:指触发的STW(停止所有 Java 线程,GC 执行时会暂停所有业务线程)时间极短,单位是毫秒级
  3. 频率高:GC 发生的次数多
  1. 核心流程(辅助理解):新对象入 Eden 区 → Eden 满触发 Minor GC → 存活对象移至 S0/S1 → 对象多次 Minor GC 后存活 → 晋升到老年代 → 老年代满触发 Full GC。

举例:

  1. 一个新创建的对象会优先放到 堆内存的新生代区
  2. 当新生代区的内存满了后 则会触发一次 Minor GC,来尝试回收那些能被回收/删除的对象(如被创建的临时对象(仅方法内局部变量,无长期引用,会被Minor GC回收)等)
  3. 当新生代区中对象多次无法被Minor GC回收则会被放入 老年代
  4. 同样,当 堆内存中的老年代区 满了后,则会触发 Full GC,尝试对这些对象进行回收,因为 Full GC 触发会对程序性能造成影响,其触发的次数需要尽可能地少
  5. 最后,当新生代与老年代的内存都满了后,就会触发堆内存溢出错误

下面这个简单的代码 GcGrokExperiment.java 可以帮助理解。运行该代码,然后用 jstat 命令(后半部分会提到常用命令的用法)去查看JVM的状态。

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * GC机制观察实验:
 * 1. 持续创建对象,部分存活(晋升老年代),部分临时(新生代Minor GC回收)
 * 2. 程序无限稳定运行,不快速OOM,支持jstat长期监控
 * 3. 清晰呈现:Eden区耗尽→Minor GC→存活对象晋升老年代→老年代耗尽→Full GC
 */
public class GcGrokExperiment {
    // 静态集合:持有对象引用,使其存活并最终晋升老年代(老年代内存占用的核心来源)
    private static List<byte[]> oldGenAliveObjects = new ArrayList<>();

    // 控制参数:避免内存快速耗尽,平衡GC触发频率(便于jstat观察)
    private static final int TEMP_OBJ_SIZE_KB = 50;    // 临时对象大小:50KB(新生代回收)
    private static final int ALIVE_OBJ_SIZE_KB = 100;  // 存活对象大小:100KB(晋升老年代)
    private static final int ALIVE_OBJ_INTERVAL = 20;  // 每创建20个临时对象,创建1个存活对象
    private static final int CLEAR_ALIVE_RATIO = 100;  // 每创建100个存活对象,移除20个(延缓OOM,长期运行)
    private static long aliveObjCount = 0;             // 存活对象计数器

    public static void main(String[] args) throws InterruptedException {
        System.out.println("GC机制观察实验启动!");
        System.out.println("可使用jstat命令监控:jstat -gc <PID> 1000(每秒刷新一次)");
        System.out.println("程序将持续运行,按Ctrl+C终止");

        // 无限循环:持续创建对象,稳定触发GC流程
        int tempObjCounter = 0;
        while (true) {
            // 1. 创建临时对象(仅方法内局部变量,无长期引用,会被Minor GC回收)
            // 频繁创建临时对象,快速耗尽Eden区,触发Minor GC
            byte[] tempObject = new byte[1024 * TEMP_OBJ_SIZE_KB];
            tempObjCounter++;

            // 2. 定期创建存活对象(存入静态集合,长期持有,最终晋升老年代)
            if (tempObjCounter % ALIVE_OBJ_INTERVAL == 0) {
                byte[] aliveObject = new byte[1024 * ALIVE_OBJ_SIZE_KB];
                oldGenAliveObjects.add(aliveObject);
                aliveObjCount++;
                System.out.printf("创建存活对象%d,当前老年代存活对象总数:%d%n", aliveObjCount, oldGenAliveObjects.size());

                // 3. 定期移除部分存活对象引用(释放老年代内存,避免快速OOM,实现程序长期运行)
                if (aliveObjCount % CLEAR_ALIVE_RATIO == 0 && oldGenAliveObjects.size() > 30) {
                    // 移除前20个存活对象引用,让其可被Full GC回收
                    oldGenAliveObjects.subList(0, 20).clear();
                    System.out.printf("移除20个存活对象,当前老年代存活对象总数:%d(触发Full GC后回收)%n", oldGenAliveObjects.size());
                }
            }

            // 控制对象创建速度:每10ms创建一个对象,避免内存瞬间耗尽,便于jstat观察
            Thread.sleep(10);
            // 重置临时对象计数器,避免数值过大
            if (tempObjCounter >= Integer.MAX_VALUE - 1000) {
                tempObjCounter = 0;
            }
        }
    }
}

二、 常见垃圾收集器:核心是 "选对场景,无需深究实现"

  1. Parallel GC(并行收集器)
  • 地位:JDK 默认收集器,无需额外配置即可使用;
  • 核心优势:吞吐量优先(GC 耗时占比低,能处理更多业务请求);
  • 适用场景:普通业务系统(如内部管理系统、低并发服务),对应用卡顿不敏感的场景。
  1. G1 GC(区域化收集器)
  • 地位:高可用场景首选,目前微服务、中间件(Kafka/Elasticsearch/Spring Boot)的主流选型;
  • 核心优势:低停顿优先(可通过参数控制最大停顿时间),兼顾吞吐量;
  • 适用场景:线上核心业务、微服务、中间件,对应用响应速度要求高的场景。
  1. ZGC/Shenandoah GC(低延迟收集器)
  • 核心优势:极致低停顿(毫秒级以内),支持超大堆内存(百 G 级,如 128G/256G 堆内存);
  • 适用场景:超大内存应用、对延迟要求极高的核心业务(如金融交易、实时风控)

三、 核心认知(运维实操关键)

"GC 不是越多越好",这是线上排查性能问题的核心原则:

  1. GC 本身会消耗服务器 CPU 资源,且 Full GC 执行时会触发 STW(停止所有 Java 线程);
  2. 线上性能瓶颈的核心诱因:
  • Full GC 频繁(1 小时超 3 次):导致应用频繁卡顿,用户请求超时;
  • GC 停顿时间过长(Full GC 超 2 秒):应用直接出现明显卡顿,甚至被监控系统判定为服务不可用;
  • Minor GC 过于频繁(每秒超 1 次):占用大量 CPU 资源,导致应用处理业务请求的能力下降。
  1. 运维关注点:无需优化 GC 底层逻辑,只需通过 jstat 监控 GC 频率和停顿时间,发现异常后通过调整 JVM 配置(堆内存、收集器类型)缓解问题,优先保障应用可用性。

二、 核心技能 1:JDK 自带命令行工具(运维必备,实操核心)

作为运维工程师,jps / jstat / jstack / jmap / jcmd 是排查 Java 应用问题(内存泄漏、线程死锁、GC 异常等)的核心工具,下面先逐一详解命令,再设计可落地的实操实验,帮助你快速掌握使用方法。

一、 JDK 核心运维命令详解

1. jps:Java 进程快速查询工具

  • 核心作用 :快速查找本地运行的 Java 进程 PID 和主类 / Jar 包信息(比ps -ef | grep java更简洁、精准,无冗余输出)。

  • 常用参数

    参数 含义 示例
    无参数 仅显示 Java 进程 PID 和主类名 jps
    -l 显示 PID + 主类全路径 / Jar 包完整路径(运维首选) jps -l
    -v 显示 PID + 主类名 + JVM 启动参数 jps -v
  • 运维场景 :排查 Java 应用时,第一步用jps -l获取目标进程 PID,为后续jstat / jstack等命令提供参数。

2. jstat:Java 运行状态监控工具

  • 核心作用:实时监控 Java 进程的 GC 状态、内存分区使用情况、类加载统计等,是排查 GC 频繁、内存溢出前兆的核心工具。

  • 常用参数

    命令格式 含义 运维场景
    jstat -gc <PID> <间隔毫秒> <采样次数> 监控堆内存(Eden/S0/S1 / 老年代)使用量 + GC 次数 / 耗时 排查内存增长异常、GC 频繁问题
    jstat -gcutil <PID> <间隔毫秒> <采样次数> 以百分比显示堆内存使用 + GC 统计(运维首选,更直观) 快速判断老年代是否满额、Minor GC/Full GC 频率
    jstat -class <PID> 监控类加载 / 卸载数量、总大小 排查类加载泄漏(如频繁热加载导致元空间溢出)
  • 核心特性:无侵入式监控,不影响 Java 应用运行,支持持续采样,可输出日志用于后续分析。

3. jstack:Java 线程堆栈分析工具

  • 核心作用 :导出 Java 进程的线程堆栈信息,用于排查线程死锁、线程阻塞(BLOCKED/WAITING)、CPU 100%、线程泄漏等问题。

  • 常用参数

    命令格式 含义 运维场景
    jstack <PID> 导出完整线程堆栈 常规线程问题排查
    jstack -F <PID> 强制导出堆栈(当 Java 进程卡死、无响应时使用) 进程挂起时的应急排查
    jstack -l <PID> 导出堆栈 + 锁详细信息(运维首选,排查死锁必备) 精准定位死锁持有者、锁竞争场景
  • 核心特性 :能识别线程状态(BLOCKED/WAITING/RUNNABLE)、线程名称、所属线程池、锁信息,jstack会自动检测死锁并给出明确提示。

4. jmap:Java 堆内存分析工具

  • 核心作用:导出 Java 进程的堆内存快照(hprof 文件)、查看堆内存分布、大对象统计,是排查 ** 内存泄漏、堆内存溢出(OOM)** 的核心工具。

  • 常用参数

    命令格式 含义 运维场景
    jmap -histo <PID> 按内存占用排序显示堆中对象(类名、数量、大小) 快速定位大对象(如超大 List/Map 导致内存溢出)
    jmap -dump:format=b,file=<文件名.hprof> <PID> 导出堆快照(二进制格式),用于 MAT/JProfiler 分析 深度排查内存泄漏(运维核心用法)
  • 注意事项 :导出堆快照时(-dump参数),Java 进程会短暂停顿(STW),建议在低峰期执行;快照文件可能较大,确保磁盘有足够空间。

5. jcmd:Java 多功能综合命令

  • 核心作用 :一站式替代jps/jstack/jmap的部分功能,支持 GC 手动触发、堆快照导出、线程堆栈导出、JVM 参数查询等,是 JDK 9 + 推荐的运维工具。

  • 常用参数

    命令格式 含义 对应旧命令
    jcmd 列出所有 Java 进程(等效jps jps
    jcmd <PID> VM.flags 查看 JVM 启动参数(等效jps -v jps -v
    jcmd <PID> Thread.print 导出线程堆栈(等效jstack -l <PID> jstack -l <PID>
    jcmd <PID> GC.heap_dump <文件名.hprof> 导出堆快照(等效jmap -dump jmap -dump:format=b,file=xxx.hprof <PID>
    jcmd <PID> GC.run 手动触发 Full GC(仅用于测试,生产环境禁止随意执行) 无对应旧命令
  • 运维场景 :生产环境中可优先使用jcmd,减少命令记忆成本,功能更全面、兼容性更好。

二、 运维实操实验(掌握 Java 应用问题排查全流程)

实验目标

  1. 掌握 5 个核心命令的基本用法和联动使用流程
  2. 模拟「线程死锁」和「内存泄漏前兆」场景,用对应命令排查定位
  3. 形成 Java 应用运维排查的标准化流程(适用于生产环境)

实验环境准备

  1. 系统环境:Ubuntu 20.04
  2. JDK 版本:JDK 11
  3. 实验代码 :编写两个 Java 程序,分别模拟线程死锁和内存泄漏前兆
    • 程序 1:DeadLockDemo.java(模拟线程死锁)
    • 程序 2:MemoryLeakDemo.java(模拟内存泄漏,静态集合持续存放对象)
  4. 工具准备 :确保 JDK 的bin目录加入系统环境变量(echo $PATH验证,能直接执行jps等命令)

实验步骤(有两个场景)

场景 1:线程死锁排查(核心命令:jps + jstack + jcmd)

步骤 1:编译并运行死锁程序

  1. 创建DeadLockDemo.java
java 复制代码
/**
 * 模拟线程死锁场景(运维排查常用测试用例)
 */
public class DeadLockDemo {
    // 定义两个锁对象
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();

    public static void main(String[] args) {
        // 线程1:先获取LOCK_A,再尝试获取LOCK_B
        new Thread(() -> {
            synchronized (LOCK_A) {
                System.out.println(Thread.currentThread().getName() + " 获取到LOCK_A,等待LOCK_B");
                try {
                    Thread.sleep(1000); // 确保线程2先获取LOCK_B
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (LOCK_B) {
                    System.out.println(Thread.currentThread().getName() + " 获取到LOCK_B");
                }
            }
        }, "Thread-DeadLock-1").start();

        // 线程2:先获取LOCK_B,再尝试获取LOCK_A
        new Thread(() -> {
            synchronized (LOCK_B) {
                System.out.println(Thread.currentThread().getName() + " 获取到LOCK_B,等待LOCK_A");
                try {
                    Thread.sleep(1000); // 确保线程1先获取LOCK_A
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (LOCK_A) {
                    System.out.println(Thread.currentThread().getName() + " 获取到LOCK_A");
                }
            }
        }, "Thread-DeadLock-2").start();

        // 让程序持续运行,便于排查
        while (true) {
            try {
                Thread.sleep(3600000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 编译运行:
bash 复制代码
# 编译
javac DeadLockDemo.java
# 后台运行(避免控制台阻塞,运维常用方式)
nohup java DeadLockDemo > deadlock.log 2>&1 &

步骤 2:用 jps 获取进程 PID

bash 复制代码
# 运维首选命令,精准获取PID和主类
jps -l
# 输出示例:76000 DeadLockDemo(76000即为PID,记录该值)

步骤 3:用 jstack 排查死锁

bash 复制代码
# 导出线程堆栈+锁信息,并重定向到文件(便于分析)
jstack -l 76000 > deadlock_thread.log
# 查看日志,筛选死锁信息
grep -A 30 "Deadlock" deadlock_thread.log

预期结果

  • jstack会明确提示「Found one Java-level deadlock」
  • 显示死锁线程(Thread-DeadLock-1、Thread-DeadLock-2)
  • 显示每个线程持有的锁和等待的锁,精准定位死锁原因

步骤 4:用 jcmd 重复排查(替代 jstack)

bash 复制代码
# 导出线程堆栈,等效jstack -l
jcmd 76000 Thread.print > deadlock_thread_jcmd.log
# 查看死锁信息
grep -A 30 "Deadlock" deadlock_thread_jcmd.log

运维总结 :生产环境中,jcmd <PID> Thread.print 可替代jstack -l <PID>,功能一致。

场景 2:内存泄漏前兆排查(核心命令:jps + jstat + jmap)

步骤 1:编译并运行内存泄漏程序

  1. 创建MemoryLeakDemo.java
java 复制代码
/**
 * 模拟内存泄漏前兆(静态List持续添加对象,不释放)
 */
import java.util.ArrayList;
import java.util.List;

public class MemoryLeakDemo {
    // 静态集合(生命周期与JVM一致,不会被GC回收,导致内存持续增长)
    private static List<byte[]> leakList = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        System.out.println("内存泄漏模拟程序启动...");
        // 循环添加大对象(每个对象100KB),模拟内存持续增长
        int count = 0;
        while (true) {
            leakList.add(new byte[1024 * 100]); // 100KB
            count++;
            if (count % 100 == 0) {
                System.out.println("已添加" + count + "个对象,当前集合大小:" + leakList.size() * 100 + "KB");
                Thread.sleep(500); // 控制增长速度,便于监控
            }
        }
    }
}
  1. 编译运行:
bash 复制代码
# 编译
javac MemoryLeakDemo.java
# 后台运行,指定堆内存大小(便于快速观察内存增长)
nohup java -Xms50m -Xmx50m MemoryLeakDemo > memory_leak.log 2>&1 &

步骤 2:用 jps 获取进程 PID

bash 复制代码
jps -l
# 输出示例:76100 MemoryLeakDemo(76100即为PID,记录该值)

步骤 3:用 jstat 监控 GC 和内存增长

bash 复制代码
# 以百分比监控GC状态,每秒采样1次,共采样100次(运维常用监控命令)
jstat -gcutil 76100 1000 100
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT   
0.00 100.00   0.00  47.29  10.20   7.48      2    0.011     0    0.000     0    0.000    0.011
0.00 100.00   0.00  47.29  10.20   7.48      2    0.011     0    0.000     0    0.000    0.011
0.00 100.00  28.57  47.29  10.20   7.48      2    0.011     0    0.000     0    0.000    0.011
0.00 100.00  42.86  47.29  10.20   7.48      2    0.011     0    0.000     0    0.000    0.011
0.00 100.00  57.14  47.29  10.20   7.48      2    0.011     0    0.000     0    0.000    0.011

关键指标观察(对应内存泄漏前兆)

  • E(Eden 区百分比):快速增长→归零(Minor GC),反复循环
  • O(老年代百分比):持续缓慢增长(最终接近 100%)
  • YGC(Minor GC 次数):快速递增(Eden 区频繁耗尽)
  • FGC(Full GC 次数):随着老年代增长,逐步递增(Full GC 无法释放内存,因为静态集合持有对象引用)

步骤 4:用 jmap 查看堆内存分布

bash 复制代码
# 导出堆快照(用于深度分析,运维核心操作)
jmap -dump:format=b,file=memory_leak.hprof 76100

后续分析 :可将memory_leak.hprof下载到本地,用 MAT(Eclipse Memory Analyzer Tool)/ visualvm 工具打开,能精准定位到leakList静态集合导致的内存泄漏。

相关推荐
春天的菠菜2 小时前
【jenkins】使用匿名访问访问jenkins的项目
运维·jenkins
可口码农2 小时前
Kwrt软路由从“路由模式”改为“交换机模式”,再改为旁路由模式接管 DHCP 的透明网关模式。
运维·服务器
奶油话梅糖2 小时前
解决Windows SSH无法连接老旧网络设备(路由器交换机)
运维·windows·ssh
郝学胜-神的一滴2 小时前
Linux 多线程编程:深入理解 `pthread_join` 函数
linux·开发语言·jvm·数据结构·c++·程序人生·算法
广州服务器托管2 小时前
[2025.12.25] Win10.LTSC2021极速响应养老版19045.3208轻精简全功能【可更新】PIIS出品 老电脑福利 老旧电脑流畅运行
运维·人工智能·计算机网络·云计算·电脑·可信计算技术
峰顶听歌的鲸鱼2 小时前
20.MySql数据库
运维·数据库·笔记·mysql·云计算·学习方法
G_H_S_3_2 小时前
【网络运维】SQL 语言:MySQL数据库基础与管理
运维·网络·数据库·mysql
清平乐的技术专栏2 小时前
电脑参数自检-BIOS
运维·服务器·电脑
翼龙云_cloud2 小时前
亚马逊云渠道商:用 AWS Lightsail 30 分钟搭建专业作品集网站
运维·服务器·云计算·aws