Skip to content

一、JVM 中常用的工具有

  • jps: 虚拟机进程状况工具.
  • jstack: 线程堆栈跟踪工具.
  • jstat: 统计监测工具.
  • jmap: 堆内存使用状况工具.
  • JConsole: JMX的可视化管理工具.
  • VisualVM: 多合一故障管理工具.
  • Arthas: Java应用程序诊断和调试工具.

二、【调优命令】排查问题

java
package com.calvin.jvm.structure.heap.question.example;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 案例: 对数据库中的信用卡进行贷款评估。
 * - 该程序执行一定时间后可能频繁FullGC,然后OOM。
 * - 启动参数: -XX:+PrintGCDetails -Xms200M -Xmx200M
 *
 * @author calvin
 * @date 2024/03/06
 */
public class HeapMemoryLeakExampleByScheduledThreadPool {

  /**
   * 定时线程池
   */
  public static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
    50,
    new ThreadPoolExecutor.DiscardOldestPolicy()
  );

  /**
   * 卡信息
   * <p>
   * - 模拟一张行用卡,里面有个空的m()
   *
   * @author calvin
   * @date 2024/03/06
   */
  public static class CardInfo {

    /**
     * 价格
     */
    BigDecimal price = BigDecimal.ZERO;

    /**
     * 名字
     */
    String name = "张三";

    /**
     * 年龄
     */
    long age = 30;

    /**
     * 出生日期
     */
    Date birthDate = new Date();

    /**
     * 米
     */
    public void m() {

    }
  }


  /**
   * main方法
   *
   * @param args 参数
   * @throws Exception 异常
   */
  public static void main(String[] args) throws Exception {
    executor.setMaximumPoolSize(50);

    // 死循环 每次去100条数
    for (; ; ) {
      modFit();
      Thread.sleep(100);
    }
  }

  /**
   * 模型匹配
   */
  public static void modFit() {
    // 取100条数据
    List<CardInfo> list = getAllCard();

    list.forEach(cardInfo -> {
      // to do something
      executor.scheduleWithFixedDelay(() -> {
        cardInfo.m();
      }, 2, 3, TimeUnit.SECONDS);
    });
  }

  /**
   * 每次从数据库中获得100个行用卡信息
   */
  public static List<CardInfo> getAllCard() {
    List<CardInfo> list = new ArrayList<>();

    for (int i = 0; i < 100; i++) {
      list.add(new CardInfo());
    }
    return list;
  }

}

1. top 命令

  • 使用 top 命令, 先查看CPU、内存等其他信息飙高的进程
    • -H 显示线程信息
    • -p 指定pid
shell
# 查看【进程】使用率
$ top 

# 输出结果: 
PID   COMMAND      %CPU  TIME     #TH   #WQ  #PORT MEM    PURG   CMPRS  PGRP PPID STATE    BOOSTS          %CPU_ME %CPU_OTHRS UID  FAULTS   COW   MSGSENT   MSGRECV   SYSBSD    SYSMACH   CSW       PAGEIN IDLEW  POWER INSTRS      CYCLES      USER
4603  java         560.9 01:57:34 76/8  1    194   255M   0B     66M    967  967  running  *0[1]           0.00000 0.00000    501  56316+   129   5344+     2593+     13682699+ 11354483+ 16756058+ 21     1657   560.9 22195113919 15375896232 calvin

# 查看该进程下【线程】使用率 (mac 无法使用该命令)
$ top -Hp 4603 

# 输出结果: 已下子线程
PID   COMMAND      %CPU TIME     #TH   #WQ  #PORT MEM    PURG   CMPRS  PGRP PPID STATE    BOOSTS          %CPU_ME %CPU_OTHRS UID  FAULTS   COW   MSGSENT   MSGRECV
4603  java          560  02:05:39 76/8  1   194    255M  0B    65M   967  967  running  *0[1]      0.00000 0.00000    501 58909   129  5706     2770     14201699 12100458 17686388 21      1658   0.0   0      0      calvin                N/A    N/A   N/A   N/A   N/A   N/A  
4602  java          0.0  00:01.71 23    1   88     127M  0B    126M  967  967  sleeping *0[1]      0.00000 0.00000    501 10423   142  158      62       90269    875      36848    2       84     0.0   0      0      calvin                N/A    N/A   N/A   N/```

2. jps 命令

  • 使用 jps 命令,查看java程序的应用进程。
shell
$ jps

# 输出结果
5698 Jps
967 
4602 Launcher
4603 HeapMemoryLeakExampleByScheduledThreadPool

3. jmap 命令

  • 打印对于每个Java类,将打印对象数量、内存大小(以字节为单位)和完全限定类名。
  • 查看前20行堆对象数量、内存大小(以字节为单位)和完全限定类名。
shell
# 生产环境不建议使用,当前已经发生了GC, 会影响到用户线程
$ jmap -histo 4603 | head -n 20 


# 输出结果
 num     #instances         #bytes  class name
----------------------------------------------
   1:        415378       38174664  [Ljava.lang.Object;
   2:        414501       33160016  [S
   3:        207546       29859648  [I
   4:        209917       15114024  java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask
   5:        209917        6717344  com.calvin.jvm.structure.heap.question.example.HeapMemoryLeakExampleByScheduledThreadPool$CardInfo
   6:        207540        6641280  java.util.concurrent.ConcurrentHashMap$Node
   7:        207250        6632000  com.intellij.rt.debugger.agent.CaptureStorage$WeakKey
   8:        207250        6632000  java.lang.Throwable
   9:        209917        5038008  java.util.Date
  10:        209917        5038008  java.util.concurrent.Executors$RunnableAdapter
  11:        209917        3358672  com.calvin.jvm.structure.heap.question.example.HeapMemoryLeakExampleByCardInfo$$Lambda$2/250421012
  12:        207250        3316000  com.intellij.rt.debugger.agent.CaptureStorage$ExceptionCapturedStack
  13:            10        2099360  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  14:             1        1065096  [Ljava.util.concurrent.RunnableScheduledFuture;
  15:          2218         192040  [C
  16:           842          96696  java.lang.Class
  17:          2204          52896  java.lang.String

4. jstat 命令

  • 统计gc收集情况
shell
$ jstat -gc 4603

# 输出:  FGC 出现 17757 次 
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
22528.0 22528.0  0.0    0.0   23040.0  23040.0   136704.0   136523.9  5248.0 4870.3 640.0  497.4   12    0.182 17757 1617.494 1617.676

# `S0C`:年轻代中第一个survivor(幸存区)的容量 (字节)
# `S1C`:年轻代中第二个survivor(幸存区)的容量 (字节)
# `S0U`:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
# `S1U`:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
# `EC`:年轻代中Eden(伊甸园)的容量 (字节)
# `EU`:年轻代中Eden(伊甸园)目前已使用空间 (字节)
# `OC`:Old代的容量 (字节)
# `OU`:Old代目前已使用空间 (字节)
# `MC`:metaspace(元空间)的容量 (字节)
# `MU`:metaspace(元空间)目前已使用空间 (字节)
# `CCSC`:当前压缩类空间的容量 (字节)
# `CCSU`:当前压缩类空间目前已使用空间 (字节)
# `YGC`:从应用程序启动到采样时年轻代中gc次数
# `YGCT`:从应用程序启动到采样时年轻代中gc所用时间(s)
# `FGC`:从应用程序启动到采样时old代(全gc)gc次数
# `FGCT`:从应用程序启动到采样时old代(全gc)gc所用时间(s)
# `GCT`:从应用程序启动到采样时gc用的总时间(s)
  • jstat 命令
  • -class: 用于查看类加载情况的统计
  • -compiler: 用于查看HotSpot中即时编译器编译情况的统计
  • -gc: 用于查看JVM中堆的垃圾收集情况的统计
  • -gccapacity: 用于查看新生代、老生代及持久代的存储容量情况
  • -gcmetacapacity: 显示metaspace的大小
  • -gcnew: 用于查看新生代垃圾收集的情况
  • -gcnewcapacity: 用于查看新生代存储容量的情况
  • -gcold: 用于查看老生代及持久代垃圾收集的情况
  • -gcoldcapacity: 用于查看老生代的容量
  • -gcutil: 显示垃圾收集信息
  • -gccause: 显示垃圾回收的相关信息(通-gcutil),同时显示最后一次仅当前正在发生的垃圾收集的原因
  • -printcompilation: 输出JIT编译的方法信息

5. 导出产生堆内存存储文件

  • 导出产生堆内存存储文件 (不建议直接导出)
shell
$ jmap -dump:format=b,file=20240306.hprof 4603

# 输出结果:

Dumping heap to /Users/calvin/学习/01-后端开发/Java/02-进阶知识/Jvm 虚拟机/20240306.hprof ...
Heap dump file created

6. VisualVM

  • 导入到 VisualVM, 进行分析结果

7. 通过启动参数【配置内存异常后,添加堆转储文件】

  • 启动参数【配置内存异常后,添加堆转储文件】,所以一般都是提前规划预防。
shell
java -Xms20m -Xmx20m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError HeapMemoryLeakExampleByScheduledThreadPool.java

8. 分析结果

  • 从 VisualVM 反馈问题,可以看出异常线程不断堆积,状态都是等待,说明了当前引发了内存泄漏问题
java
/**
 * 模型匹配(内存泄漏的代码)
 */
public static void modFit() {
  // 取100条数据
  List<CardInfo> list = getAllCard();

  // 任务中引用了外部的 cardInfo 对象。由于 cardInfo 是在 getAllCard() 方法中创建的对象
  list.forEach(cardInfo -> {
    // to do something
    executor.scheduleWithFixedDelay(() -> {
    // ScheduledThreadPoolExecutor 会持有对这些任务的引用,导致这些 cardInfo 对象无法被释放,从而导致内存泄漏。
    cardInfo.m();
    }, 2, 3, TimeUnit.SECONDS);
  });

}

注意事项

线上环境,内存特别大,jmap 执行期间会对进程产生很大影响,甚至卡顿(电商不适合)

  1. 设定了参数HeapDump,OOM的时候会自动产生堆转储文件(不是很专业,因为多有监控,内存增长就会报警)
  2. 很多服务器备份(高可用),停掉这台服务器对其他服务器不影响.
  3. 在线定位(一般小点儿公司用不到).
  4. 在测试环境中压测(产生类似内存增长问题,在堆还不是很大的时候进行转储).
  5. 不要使用上述第4、5种操作,因为线上环境会导致程序性能更加低下,甚至影响卡顿,部分产生堆存储文件100多G, 会影响到用户线程。

官方文档: https://arthas.aliyun.com/

Arthas

1. 启动 Arthas

  • 启动 Arthas, 通过序号选择有问题程序。
shell
$ java -jar arthas-boot.jar

[INFO] JAVA_HOME: /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre
[INFO] arthas-boot version: 3.7.2
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 6787 
  [2]: 19204 org.jetbrains.jps.cmdline.Launcher
  [3]: 19220 com.calvin.jvm.structure.heap.question.example.HeapMemoryLeakExampleByScheduledThreadPool
3
[INFO] arthas home: /Users/calvin/.arthas/lib/3.7.2/arthas
[INFO] Try to attach process 19220
Picked up JAVA_TOOL_OPTIONS: 
[INFO] Attach process 19220 success.
[INFO] arthas-client connect 127.0.0.1 3658
  ,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.                           
 /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'                          
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.                          
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |                         
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'                          

wiki       https://arthas.aliyun.com/doc                                        
tutorials  https://arthas.aliyun.com/doc/arthas-tutorials.html                  
version    3.7.2                                                                
main_class com.calvin.jvm.structure.heap.question.example.HeapMemoryLeakExample 
           ByScheduledThreadPool                                                
pid        19220                                                                
time       2024-03-07 14:04:15

2. dashboard 命令

  • 观察当前系统的实时数据面板,主要查看 CPUSTATEMemory中 used使用状况,以及GC回收次数。
shell
$ dashboard
  • 第一: 从 ParGC 可以看出线上环境已经发生了 GC。
  • 第二: 从 pool-1-thread-序号线程池中的线程在不断堆积, 线程状态都是等待,并且没有执行成功。
  • 第三: 从展示内存信息 GC 新生代 17次老年代 2352次,以及内存使用率非常高, 可以判定程序已经接近STW状态。

3. heapdump 命令

  • dump java heap, 类似 jmap 命令的 heap dump 功能。
  • 导出堆存储文件
shell
$ heapdump 

# 输出
Dumping heap to /var/folders/_f/40h94x8j6xb5x00hz_xmmf8w0000gn/T/heapdump2024-03-07-15-438412767050560146693.hprof ...
Heap dump file created

4. VisualVM 分析

5. 问题产生

  • 从 VisualVM 反馈问题,可以看出异常线程不断堆积,状态都是等待,说明了当前引发了内存泄漏问题
java

/**
 * 模型匹配(内存泄漏的代码)
 */
public static void modFit() {
  // 取100条数据
  List<CardInfo> list = getAllCard();

  // 任务中引用了外部的 cardInfo 对象。由于 cardInfo 是在 getAllCard() 方法中创建的对象
  list.forEach(cardInfo -> {
    // to do something
    executor.scheduleWithFixedDelay(() -> {
        // ScheduledThreadPoolExecutor 会持有对这些任务的引用,导致这些 cardInfo 对象无法被释放,从而导致内存泄漏。
        cardInfo.m();
        }, 2, 3, TimeUnit.SECONDS);
  });

}
  • // 为了解决这个问题,可以在 executor.scheduleWithFixedDelay() 方法中使用局部变量来引用 cardInfo 对象,而不是直接引用外部对象。这样可以确保任务执行完后, cardInfo 对象可以被垃圾回收释放内存。
java
 list.forEach(cardInfo -> {
            // 使用局部变量引用外部对象
            CardInfo localCardInfo = cardInfo; 
            executor.scheduleWithFixedDelay(() -> {
                localCardInfo.m();
            }, 2, 3, TimeUnit.SECONDS);
});