Home Java性能分析第四节 - Linux 下 JVM监控.
Post
Cancel

Java性能分析第四节 - Linux 下 JVM监控.

监控 与 性能分析

监控

监控通常是指一种在生产,质量评估或者开发环境中带有预防或主动性的活动。当应用负责人收到性能问题却没有足以定位问题的线索,首先会查看性能监控,随后性能 分析。

性能分析

性能分析是一种侵入方式收集运行性能数据的活动,他会影响应用的吞吐量或响应时间。性能分析是监控报警后的回应,关注并监控更加集中。

JVM 监控

一、Linux 下 JVM 内存监控

Linux 下 常用 JVM 内存命令可以查看 Java性能分析第三节 - Linux 下 内存监控 这里主要介绍以下两种方式实现 JVM 监控

二、代码实现打印 JVM 监控

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
  /**
     * 格式化字节
     */
    private static String formatBytes(long bytes) {
        if (bytes < 0) return "N/A";
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format("%.2f KB", bytes / 1024.0);
        if (bytes < 1024 * 1024 * 1024) return String.format("%.2f MB", bytes / (1024.0 * 1024));
        return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
    }

    /**
     * 打印内存信息
     */
    public static void printMemoryInfo() {
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();

        // 堆内存
        MemoryUsage heapMemory = memoryMXBean.getHeapMemoryUsage();
        System.out.println("=== 堆内存信息 ===");
        System.out.println("初始大小: " + formatBytes(heapMemory.getInit()));
        System.out.println("已使用: " + formatBytes(heapMemory.getUsed()));
        System.out.println("已提交: " + formatBytes(heapMemory.getCommitted()));
        System.out.println("最大值: " + formatBytes(heapMemory.getMax()));
        System.out.println("使用率: " + String.format("%.2f%%",
                (double) heapMemory.getUsed() / heapMemory.getMax() * 100));

        // 非堆内存
        MemoryUsage nonHeapMemory = memoryMXBean.getNonHeapMemoryUsage();
        System.out.println("\n=== 非堆内存信息 ===");
        System.out.println("已使用: " + formatBytes(nonHeapMemory.getUsed()));
        System.out.println("已提交: " + formatBytes(nonHeapMemory.getCommitted()));

        // 各个内存池详细信息
        System.out.println("\n=== 内存池详细信息 ===");
        List<MemoryPoolMXBean> memoryPools = ManagementFactory.getMemoryPoolMXBeans();
        for (MemoryPoolMXBean pool : memoryPools) {
            MemoryUsage usage = pool.getUsage();
            System.out.println("\n内存池: " + pool.getName());
            System.out.println("  类型: " + pool.getType());
            System.out.println("  已使用: " + formatBytes(usage.getUsed()));
            System.out.println("  最大值: " + formatBytes(usage.getMax()));
            if (usage.getMax() > 0) {
                System.out.println("  使用率: " + String.format("%.2f%%",
                        (double) usage.getUsed() / usage.getMax() * 100));
            }
        }
    }

    /**
     * 打印 GC 信息
     */
    public static void printGCInfo() {
        System.out.println("\n=== GC 信息 ===");
        List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();

        for (GarbageCollectorMXBean gc : gcBeans) {
            System.out.println("\nGC 名称: " + gc.getName());
            System.out.println("  回收次数: " + gc.getCollectionCount());
            System.out.println("  回收耗时: " + gc.getCollectionTime() + " ms");
            System.out.println("  内存池: " + String.join(", ", gc.getMemoryPoolNames()));

            // 平均每次 GC 时间
            if (gc.getCollectionCount() > 0) {
                System.out.println("  平均每次耗时: " +
                        String.format("%.2f ms", (double) gc.getCollectionTime() / gc.getCollectionCount()));
            }
        }
    }

三、通过 JMX 远程监控

启用 JMX 远程监控,需要在 JVM 启动参数中添加以下配置:

1
2
3
4
5
java -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=9999 \
     -Dcom.sun.management.jmxremote.authenticate=false \
     -Dcom.sun.management.jmxremote.ssl=false \
     -jar your-app.jar

代码实现

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
/**
     * 连接远程 JVM
     */
    public static MBeanServerConnection connectToRemoteJVM(String host, int port)
            throws Exception {
        String url = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi";
        JMXServiceURL serviceURL = new JMXServiceURL(url);
        JMXConnector connector = JMXConnectorFactory.connect(serviceURL);
        return connector.getMBeanServerConnection();
    }

    /**
     * 从远程获取内存信息
     */
    public static void getRemoteMemoryInfo(MBeanServerConnection mbsc) throws Exception {
        ObjectName memoryBean = ObjectName.getInstance("java.lang:type=Memory");

        // 获取堆内存
        CompositeData heapMemory = (CompositeData) mbsc.getAttribute(memoryBean, "HeapMemoryUsage");
        long used = (Long) heapMemory.get("used");
        long max = (Long) heapMemory.get("max");

        System.out.println("远程 JVM 堆内存:");
        System.out.println("  已使用: " + formatBytes(used));
        System.out.println("  最大值: " + formatBytes(max));
        System.out.println("  使用率: " + String.format("%.2f%%", (double) used / max * 100));
    }

    /**
     * 从远程获取 GC 信息
     */
    public static void getRemoteGCInfo(MBeanServerConnection mbsc) throws Exception {
        ObjectName gcQuery = ObjectName.getInstance("java.lang:type=GarbageCollector,*");
        Set<ObjectName> gcNames = mbsc.queryNames(gcQuery, null);

        System.out.println("\n远程 JVM GC 信息:");
        for (ObjectName gcName : gcNames) {
            String name = (String) mbsc.getAttribute(gcName, "Name");
            Long count = (Long) mbsc.getAttribute(gcName, "CollectionCount");
            Long time = (Long) mbsc.getAttribute(gcName, "CollectionTime");

            System.out.printf("  %s: 次数=%d, 耗时=%dms%n", name, count, time);
        }
    }

四、-XX:+PrintGCDetails

java -XX:+PrintGCDetails -jar your-application.jar 这里注意正式环境慎用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[44.881s][info   ][gc,start    ] GC(3) Pause Young (Prepare Mixed) (G1 Evacuation Pause)
[44.881s][info   ][gc,task     ] GC(3) Using 9 workers of 9 for evacuation
[44.886s][info   ][gc,phases   ] GC(3)   Pre Evacuate Collection Set: 0.2ms
[44.886s][info   ][gc,phases   ] GC(3)   Merge Heap Roots: 0.1ms
[44.886s][info   ][gc,phases   ] GC(3)   Evacuate Collection Set: 3.9ms
[44.886s][info   ][gc,phases   ] GC(3)   Post Evacuate Collection Set: 0.6ms
[44.886s][info   ][gc,phases   ] GC(3)   Other: 0.1ms
[44.886s][info   ][gc,heap     ] GC(3) Eden regions: 510->0(549)
[44.886s][info   ][gc,heap     ] GC(3) Survivor regions: 5->10(65)
[44.886s][info   ][gc,heap     ] GC(3) Old regions: 8->8
[44.886s][info   ][gc,heap     ] GC(3) Humongous regions: 0->0
[44.886s][info   ][gc,metaspace] GC(3) Metaspace: 33276K(33920K)->33276K(33920K) NonClass: 28938K(29312K)->28938K(29312K) Class: 4338K(4608K)->4338K(4608K)
[44.886s][info   ][gc          ] GC(3) Pause Young (Prepare Mixed) (G1 Evacuation Pause) 521M->16M(1024M) 5.087ms
[44.886s][info   ][gc,cpu      ] GC(3) User=0.02s Sys=0.01s Real=0.01s

基本信息

字段 数值 含义解释 数值解读
触发原因 G1 Evacuation Pause G1疏散暂停 正常的对象复制/疏散过程

并行工作线程

字段 数值 含义解释 数值解读
Using X workers of Y 9 / 9 使用了9个线程,总共9个可用 🟢 线程利用率100%,并行度最大化

GC各阶段耗时详解

阶段 耗时 含义解释 数值解读
Pre Evacuate Collection Set 0.2ms 疏散前准备:选择要回收的区域 选择工作很快,开销小
Merge Heap Roots 0.1ms 合并堆根引用:整合GC根对象 根对象数量少,处理快速
Evacuate Collection Set 3.9ms 疏散回收集:复制存活对象到新区域 🔴 主要耗时(占76%),存活对象较多
Post Evacuate Collection Set 0.6ms 疏散后处理:更新引用、清理工作 后续处理开销合理
Other 0.1ms 其他杂项时间 其他开销可忽略

堆内存各区域变化

区域 变化 (前→后) 容量 含义解释 数值解读
Eden regions 510 → 0 (549) 新对象分配区,GC后清空 🟢 回收了510个区域,回收率100%
Survivor regions 5 → 10 (65) 存活对象暂存区 🟡 存活对象翻倍,说明有较多对象晋升
Old regions 8 → 8 - 老年代区域 🟢 老年代暂未增长,压力不大
Humongous regions 0 → 0 - 大对象区(> 50%区域大小的对象) 🟢 无大对象,内存碎片风险低

区域数量说明:

  • 每个region默认大小通常为 1-32MB(取决于堆大小)
  • 括号内数字(549)、(65)为该区域类型的最大容量

Metaspace元空间(类元数据)

区域 变化 容量 含义解释 数值解读
Metaspace总量 33276K → 33276K (33920K) 类元数据总空间 🟢 无变化,已加载类稳定(使用率98%)
NonClass 28938K → 28938K (29312K) 非类元数据(常量池等) 使用率99%,接近满
Class 4338K → 4338K (4608K) 类元数据 使用率94%,正常范围

⚠️ 注意:Metaspace使用率很高(98%),如果持续增长可能导致Full GC


总体GC效果摘要

指标 数值 含义解释 数值解读
堆内存变化 521M → 16M GC前后堆使用量 🟢 回收了505M(97%),效果极佳
堆总容量 (1024M) 堆最大可用内存 当前使用率仅1.6%,空间充足
总暂停时长 5.087ms 应用停顿的实际时间 🟢 < 10ms,对用户无感知

CPU资源使用

CPU指标 数值 含义解释 数值解读
User Time 0.02s 用户态CPU时间(GC线程执行时间) 9个线程共消耗0.02s CPU
System Time 0.01s 内核态CPU时间(系统调用) 系统调用开销小
Real Time 0.01s 实际挂钟时间(墙钟时间) 实际暂停0.01s = 10ms

CPU利用率计算:

1
CPU利用率 = (User + System) / Real = (0.02 + 0.01) / 0.01 = 300%

解读:多线程并行效果好,9个线程让CPU利用率达到300%(3个核心满负荷)

五、OutOfMemoryError: Metaspace

Metaspace 内存溢出,一般是有循环生成代理类,或者频繁加载大量第三方库导致的。

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
public String testMetapace() {
  System.out.println("开始 Metaspace 溢出测试...");
  System.out.println("当前 MaxMetaspaceSize: " +
    Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");

  List<Object> proxyList = new ArrayList<>();
  int count = 0;
  try {
    while (true) {
      // 创建新的 ClassLoader,每次都创建新的类
      ClassLoader classLoader = Demo1Application.class.getClassLoader();

      // 创建动态代理
      Object proxy = createDynamicProxy(classLoader);
      proxyList.add(proxy);

      count++;
      if (count % 1000 == 0) {
        System.out.println("已创建代理类数量: " + count);
        printMemoryInfo();
      }
    }
  } catch (Exception e) {
    System.err.println("\n=== 发生 OutOfMemoryError ===");
    System.err.println("错误信息: " + e.getMessage());
    System.err.println("总共创建了 " + count + " 个代理类");
    printMemoryInfo();
    e.printStackTrace();
  }
  return "Metaspace 溢出测试完成";
}

/**
 * 创建动态代理对象
 */
private static Object createDynamicProxy(ClassLoader classLoader) {
  return Proxy.newProxyInstance(
    classLoader,
    new Class<?>[]{TestInterface.class},
    new TestInvocationHandler()
  );
}

weixin.png

公众号名称:怪味Coding
微信扫码关注或搜索公众号名称
This post is licensed under CC BY 4.0 by the author.