NetBeans Profiler 的编程式用法

1、概览

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

本文将带你了解如何以编程方式使用 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>

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

接着,创建一个简单的项目并分析它的 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(太阳系)的类,并在控制台中输出了太阳系的 name、id 和 planet。

在应用运行时,我们可以使用 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);
}

在上面的代码中,我们创建了 MBeabServer 和 HotSpotDiagnosticMXBean 对象来获取 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");
}

在上面的代码中,我们调用了 javaClass 的 getName()、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 类的实例数最多,分别为 18996 和 18014 个。这是意料之中的,因为我们的 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