原创

【JAVA】深入解析JDK飞行记录器:新时代下的Java本地内存追踪(NMT)

前言

JDK 飞行记录器(JFR)是我在Java平台上最喜欢的工具之一。这个内置在JVM中的低开销事件记录引擎,用于观测Java应用程序的运行特性,并且能帮助识别任何潜在的性能问题。随着新版本的推出,JFR 不断变得更加强大,其中最近的一个新增功能是对本地内存追踪(NMT)的支持。

NMT

本地内存追踪(NMT)本身并不是JVM的一个新功能:它为您提供了对应用程序内存消耗的详细洞察,远远超出了众所周知的Java堆空间。NMT告诉您JVM用于类元数据、线程堆栈、JIT编译器、垃圾回收、内存映射文件等的内存使用量,以及许多其他方面。要了解有关NMT的更多信息,我强热推阅读Brice Dutheil的出色文章《Off-Heap memory reconnaissance》。

以前,您必须使用jcmd命令行工具以特定的方式捕获运行中JVM的值,直到最近,Java 20以来,添加了两种新的JFR事件类型,您可以使用JFR连续记录NMT数据。

这使得持续收集数据并以系统化的方式进行分析变得更加容易。还可以通过JFR事件流将NMT数据的实时流推送给远程客户端,与仪表板和监控解决方案集成。

例子

那么让我们看看通过JFR报告NMT数据的方式。这里有一个简单的示例程序,它使用传统的缓冲区分配了一些堆外内存,然后利用Java 22中的JEP 454特性Foreign Memory API分配(一次性分配4GB的感觉真好,这是之前无法做到的):

import java.nio.ByteBuffer;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;

import static java.time.LocalDateTime.now;

public void main() throws Exception {
  System.out.println(STR."\{ now() } Started");
  Thread.sleep(5000);

  ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
  System.out.println(STR."\{ now() } Allocated (Direct)");
  Thread.sleep(5000);

  try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(4L * 1024L * 1024L * 1024L);
    System.out.println(STR."\{ now() } Allocated (FMI)");
    Thread.sleep(5000);
  }

  buffer = null;
  System.out.println(STR."\{ now() } Deallocated");
  Thread.sleep(5000);
}

JFR默认每秒记录一次NMT事件,因此我在程序中添加了一些sleep()调用,以确保程序运行足够长的时间,使不同的内存分配在时间上有所分散。只是为了好玩,我还使用了JEP 463支持的顶级main方法和JEP 459的字符串模板来编写日志消息。

让我们运行这个示例程序,看看JFR如何跟踪这些堆外内存分配。

有点出乎意料的是,在JFR中,NMT是通过gc设置进行控制的,必须将其设置为"normal"、"detailed"、"high"或"all"以记录NMT数据。

当然,默认的profile JFR配置就能支持,,它们随SDK一起提供,除此之外,必须使用-XX:NativeMemoryTracking JVM选项启用NMT本身:

java --enable-preview --source 22 \
  -XX:StartFlightRecording=name=Profiling,filename=nmt-recording.jfr,settings=profile \
  -XX:NativeMemoryTracking=detail main.java
[0.316s][info][jfr,startup] Started recording 1. No limit specified, using maxsize=250MB as default.
[0.316s][info][jfr,startup]
[0.316s][info][jfr,startup] Use jcmd 47194 JFR.dump name=Profiling to copy recording data to file.
2023-12-17T18:31:00.475598 Started
2023-12-17T18:31:05.609319 Allocated (Direct)
2023-12-17T18:31:11.167484 Allocated (FMI)
2023-12-17T18:31:16.253059 Deallocated

让我们在JDK Mission Control中打开录制文件,看看我们能发现什么。截至8.3版本,JMC没有专门的视图来显示NMT数据,但NMT事件会显示在通用的事件浏览器视图中。有两种事件类型,第一种是"Total Native Memory Usage"(总本地内存使用):

file

这两个堆外内存分配(1 GB的直接字节缓冲区和4 GB的Foreign Memory API)如预期般显示为程序保留和提交内存的增加。我们还看到了新的Foreign Memory API的一个优势:只要Arena对象关闭,内存就会被释放,而JVM会在丢弃引用后仍然保留字节缓冲区的内存。对于这块内存何时释放,我们无法精确控制,它将通过基于[虚引用的清理器](https://stackoverflow.com/questions/36077641/java-when-does-direct-buffer-released在GC删除相关缓冲区对象后的某个时刻完成。

第二个新的事件类型是"Native Memory Usage Per Type",它提供了更详细的视图(当将-XX:NativeMemoryTracking设置为detail而不是summary时)。堆外内存分配显示在"Other"类别下:

file

根据文档,NMT会导致5%到10%的性能开销(实际开销大小很大程度上取决于具体的工作负载),因此在生产环境中可能不是永久启用的首选选项。幸运的是,Java 21添加了另一种JFR事件类型,"Resident Set Size"(RSS),允许您持续跟踪应用程序的总体内存消耗:

file

当然,您也可以使用其他工具(比如ps)检索RSS,即由进程分配的物理内存,但通过JFR记录它使得对其随时间的发展进行简化分析,同时还可以将其与其他相关的JFR事件进行关联,例如类的(卸载)加载或垃圾回收。

使用JFR事件流,您还可以向远程监控客户端提供值的实时信息流,从而可以通过仪表板进行可视化跟踪。但您还可以对这些值的时间序列应用某种模式匹配,在应用程序预热阶段之后,如果它继续增长,触发警报。

引用

Tracking Java Native Memory With JDK Flight Recorder

本文来自:【JAVA】深入解析JDK飞行记录器:新时代下的Java本地内存追踪(NMT)-小码农,转载请保留本条链接,感谢!

温馨提示:
本文最后更新于 2023年12月18日,已超过 181 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我
正文到此结束
本文目录