NetBeans Profiler 的编程式用法

1、概览

对应用程序进行分析可以深入了解其运行时的行为。Java 生态系统中有多种流行的分析器(Profiler),如用于通用分析的 NetBeans ProfilerJProfilerVisualVM

本文将带你了解如何以编程方式使用 NetBeans profiler API。

2、NetBeans Profiler

NetBeans IDE 提供免费的分析器来分析 Java 应用。它通过 IDE 中直观的嵌入式用户界面,提供了评估 CPU 性能和内存使用情况的功能。

然而,NetBeans Profiler 还提供了可用于编程式的分析 API。这可用于 Heap Dump 的自动化分析,而不需要依赖于 UI 界面。

Heap Dump(堆转储)是一段时间内应用的内存快照。它是深入了解内存使用情况的良好指标,因为它包括内存中的实时对象、对象的类和字段以及对象之间的引用。

3、示例项目

要使用 NetBeans Profiler API,首先在 pom.xml 中添加 依赖

<dependency>
    <groupId>org.netbeans.modules</groupId>
    <artifactId>org-netbeans-lib-profiler</artifactId>
    <version>RELEASE220</version>
</dependency>

该依赖提供了 JavaClassesInstances 等各种工具类,以帮助我们分析类、创建的实例数量和使用的内存。

接着,创建一个简单的项目并分析它的 Heap Dump

class SolarSystem {

    private static final Logger LOGGER = Logger.getLogger(SolarSystem.class.getName());

    private int id;

    private String name;

    private List<String> planet = new ArrayList<>();

    // 构造函数

    public void logSolarSystem() {
        LOGGER.info(name);
        LOGGER.info(String.valueOf(id));
        LOGGER.info(planet.toString());
    }
}

在上面的代码中,我们定义了一个名为 SolarSystem太阳系)的类,并在控制台中输出了太阳系的 nameidplanet

在应用运行时,我们可以使用 jmap 获取 Heap Dump,以便进一步分析。此外,我们还可以通过编程的方式来获取 Heap Dump

static void dumpHeap(String filePath, boolean live) throws IOException {
    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(
      server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);
    mxBean.dumpHeap(filePath, live);
}

在上面的代码中,我们创建了 MBeabServerHotSpotDiagnosticMXBean 对象来获取 Heap Dump

接下来,创建一个名为 SolApp 的类,并添加一个方法来实例化 SolarSystem

class SolApp {

    static void solarSystem() throws IOException {
        List<String> planet = new ArrayList<>();
        planet.add("Mercury");
        planet.add("Mars");
        planet.add("Earth");
        planet.add("Venus");
        SolarSystem solarSystem = new SolarSystem(1, "Sol System", planet);
        solarSystem.logSolarSystem();

        HeapDump.dumpHeap("solarSystem.hprof", true);
    }
}

如上,我们使用一些 planet(行星)实例化 SolarSystem 类,并以编程式进行 Heap Dump 以进行性能分析。执行后,会在项目根目录中生成 solarSystem.hprof 文件。

接着,创建一个 Heap 对象来加载 Heap Dump

Heap heap = HeapFactory.createHeap(new File("solarSystem.hprof"));

在上面的代码中,我们准备了用于分析的 dump 文件。现在,调用 Heap 对象上的各种方法对其进行分析。

有了 Heap Dump,我们就可以进行多项分析,例如了解类的内存使用情况、检测潜在的数据泄漏,以及根据分析结果优化代码性能。

4、堆概述

先来简要了解一下堆:

static void heapDumpSummary() {
    HeapSummary summary = heap.getSummary();
    LOGGER.info("Total instances: " + summary.getTotalLiveInstances());
    LOGGER.info("Total bytes: " + summary.getTotalLiveBytes());
    LOGGER.info("Time: " + summary.getTime());
    LOGGER.info("GC Roots: " + heap.getGCRoots().size());
    LOGGER.info("Total classes: " + heap.getAllClasses().size());
}

在上面的代码中,我们创建了一个 HeapSummary 对象,并调用其中的各种方法来检查 dump 文件。

日志输出如下:

INFO com.baeldung.netbeanprofiler.SolApp -- Total instances: 79893
INFO com.baeldung.netbeanprofiler.SolApp -- Total bytes: 6235526
INFO com.baeldung.netbeanprofiler.SolApp -- Time: 1724568603079
INFO com.baeldung.netbeanprofiler.SolApp -- GC Roots: 2612
INFO com.baeldung.netbeanprofiler.SolApp -- Total classes: 3207

结果显示,在收集 Heap Dump 时,有 79893 个活动对象。这些活动实例占用了大约 6 MiB 的空间,共有 3207 个类的实例占用了 79893 个活动对象。

Dump 时,GC root 数为 2612

5、类直方图

概览 Heap Dump 后,让我们来检查应用程序中使用的类和实例数量。

首先,创建一个对象来获取示例应用中的所有类:

List<JavaClass> unmodifiableClasses = heap.getAllClasses();

接下来,创建一个 List 对象来存储用于排序的类:

List<JavaClass> classes = new ArrayList<>(unmodifiableClasses);
classes.sort((c1, c2) -> Long.compare(c2.getInstancesCount(), c1.getInstancesCount()));

然后,迭代 JavaClass 对象:

for (int i = 0; i < Math.min(5, classes.size()); i++) {
    JavaClass javaClass = classes.get(i);
    LOGGER.info("  " + javaClass.getName());
    LOGGER.info("    Instances: " + javaClass.getInstancesCount());
    LOGGER.info("    Total size: " + javaClass.getAllInstancesSize() + " bytes");
}

在上面的代码中,我们调用了 javaClassgetName()getInstancesCount()getAllInstancesSize() 方法,分别获取类名、创建的实例总数和实例的总大小。

以下是控制台输出结果:

INFO com.baeldung.netbeanprofiler.SolApp --   byte[]
INFO com.baeldung.netbeanprofiler.SolApp --     Instances: 18996
INFO com.baeldung.netbeanprofiler.SolApp --     Total size: 2714375 bytes
INFO com.baeldung.netbeanprofiler.SolApp --   java.lang.String
INFO com.baeldung.netbeanprofiler.SolApp --     Instances: 18014
INFO com.baeldung.netbeanprofiler.SolApp --     Total size: 540420 bytes
INFO com.baeldung.netbeanprofiler.SolApp --   java.util.concurrent.ConcurrentHashMap$Node
INFO com.baeldung.netbeanprofiler.SolApp --     Instances: 5522
INFO com.baeldung.netbeanprofiler.SolApp --     Total size: 242968 bytes

以上结果显示 byte[]String 类的实例数最多,分别为 1899618014 个。这是意料之中的,因为我们的 SolarSystem 类在内部依赖于 String 对象。

6、分析 SolarSystem 对象

现在,让我们来看看 SolarSystem 类实例的大小和数量。

6.1、总大小和实例数

首先,计算 SolarSystem 类的大小和实例总数:

static void solarSystemSummary() {
    JavaClass solarSystemClass = heap.getJavaClassByName("com.baeldung.netbeanprofiler.galaxy.SolarSystem");
    List<Instance> instances = solarSystemClass.getInstances();
    long totalSize = 0;
    long instancesNumber = solarSystemClass.getInstancesCount();
    for (Instance instance : instances) {
        totalSize += instance.getSize();
    }
    LOGGER.info("Total SolarSystem instances: " + instancesNumber);
    LOGGER.info("Total memory used by SolarSystem instances: " + totalSize);
}

如上,创建一个 JavaClass 对象,并在 heap 实例上调用 getJavaClassByName() 方法。接着,获取该类的实例,并在控制台中输出实例的数量和大小:

INFO com.baeldung.netbeanprofiler.SolApp - Total SolarSystem instances: 1
INFO com.baeldung.netbeanprofiler.SolApp - Total memory used by SolarSystem instances: 36

从控制台输出来看,SolarSystem 实例创建一次使用了 36 个字节。这与示例代码中创建的实例数量一致,表明 SolarSystem 对象已按预期实例化。

6.2、字段详情

此外,我们还可以分析指定类的字段详情:

static void logFieldDetails() {
    JavaClass solarSystemClass = heap.getJavaClassByName("com.baeldung.netbeanprofiler.galaxy.SolarSystem");

    List<Field> fields = solarSystemClass.getFields();
    for (Field field : fields) {
        LOGGER.info("Field: " + field.getName());
        LOGGER.info("Type: " + field.getType().getName());
    }
}

如上,我们选择 SolarSystem 类并获取其所有实例。然后创建 Field 对象集合并遍历该集合。最后,将字段的名称和类型输出到控制台。

7、分析 GC Root

GC Root 清楚地显示了垃圾回收器的行为。任何直接或间接被 GC Root 引用的对象都不会被垃圾回收。这表明对象仍然存活,还不能被清理。

调用 heap 对象上的 getGCRoots() 方法来收集所有 GC Root

Collection<GCRoot> gcRoots = heap.getGCRoots();

如上,我们调用 heap 实例上的 getGCRoots() 方法来获取 GC Root 的集合。

接着,创建一个变量来存储不同 GC Root 的计数:

int threadObj = 0, jniGlobal = 0, jniLocal = 0, javaFrame = 0, other = 0;

然后,迭代 GC Root,并计算 Thread 对象、Java 本地接口 (JNI) 全局和本地对象以及 Java Frame(栈帧)的数量:

for (GCRoot gcRoot : gcRoots) {
    Instance instance = gcRoot.getInstance();
    String kind = gcRoot.getKind();

    switch (kind) {
        case THREAD_OBJECT:
            threadObj++;
            break;
        case JNI_GLOBAL:
            jniGlobal++;
            break;
        case JNI_LOCAL:
            jniLocal++;
            break;
        case JAVA_FRAME:
            javaFrame++;
            break;
        default:
            other++;
    }
}

如上,迭代 GC Root,并计算 Thread 对象、Java Frame、JNI 全局引用、JNI 本地引用等的数量。

Thread(线程)对象是在进行 Heap Dump 时处于活动状态的线程。它所引用的对象被视为实时对象,不会被垃圾回收。Java Frame 表示 JVM Stack Frame 中的对象引用。它保存了局部变量和方法调用的详细信息。

此外,JNI 还允许 Java 代码与 C 或 C++ 等本地代码交互。本地引用在创建引用的本地方法范围内有效,而全局引用则在单个本地方法调用范围之外保留对 Java 对象的引用。

最后,在控制台输出详细信息:

LOGGER.info("\nGC Root Summary:");
LOGGER.info("  Thread Objects: " + threadObj);
LOGGER.info("  JNI Global References: " + jniGlobal);
LOGGER.info("  JNI Local References: " + jniLocal);
LOGGER.info("  Java Frames: " + javaFrame);
LOGGER.info("  Other: " + other);

结果如下:

INFO com.baeldung.netbeanprofiler.SolApp --   Thread Objects: 8
INFO com.baeldung.netbeanprofiler.SolApp --   JNI Global References: 122
INFO com.baeldung.netbeanprofiler.SolApp --   JNI Local References: 1
INFO com.baeldung.netbeanprofiler.SolApp --   Java Frames: 481
INFO com.baeldung.netbeanprofiler.SolApp --   Other: 2000

从上面的结果来看,有 8 个 Thread(线程)对象仍然存活,Java Frame 计数为 481 个。

8、总结

本文介绍了如何通过 NetBeans Profiler API 以编程式获取 Heap Dump 并对其进行进一步分析,以及如何获取堆摘要并对指定的类进行更深入的分析。


Ref:https://www.baeldung.com/java-netbeans-use-profiler-programmatically