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