JVM中的本地内存追踪NMT(Native Memory Tracking)

JVM中的本地内存追踪NMT(Native Memory Tracking)

1 概述

每当想起为什么Java应用程序会消耗比设置-Xms和-Xmx多的内存的时候,心中总是会有个问号。因为一些理由和优化,JVM可能会额外分配本地内存。这些额外分配的内存最终会消耗超过Xmx的限制。

在本文中,我们将列举一些在JVM中常见的本地内存资源,以及设置它们大小的标志,然后我们学习怎么运用Native Memory Tracking去监视它们。

2. 本地分配

堆通常是在Java应用程序中消耗内存最大的部分,但是也有例外。**除了堆,JVM也会从本地内存中分配相当大的一块以维护其元数据(metadata),应用代码,JIT生成的代码,以及内部数据结构等等。**在接下来的章节,我们将探索这些内容。

2.1. 元空间(Metaspace)

为了维护加载类的元数据,JVM使用了一个专门的非堆区域,该区域称为元空间。在Java8之前,此区域等效于永久代。元空间或者永久代包含了加载类的元数据而非其实例,实例都保存在堆中。

这里值得强调的是:堆大小的配置不会影响元空间。因为元数据是非堆区域。为了限制元空间的大小,我们使用如下的标志:

  • -XX:MetaspaceSize和*-XX:MaxMetaspaceSize*以设置最小和最大的元空间大小。
  • 在Java8之前,-XX:PermSize和*-XX:MaxPermSize*是设置最小和最大的永久代大小。

2.2. 线程

最重要的内存数据区域之一是JVM的栈,每一条线程都有一个独立维护的栈。栈保存本地变量和部分的结果,扮演方法调用的重要角色。

默认的线程栈的大小是平台无关的,但是在大多数64位操作系统中,它的大小大约是1MB。该大小是通过*-Xss*标志设置。

相比于其他的数据区域,**只要线程的数量没限制,栈的内存分配通常是不受约束的。**同样值得说明的是JVM自身需要一些线程去执行一些内部操作如GC和即时编译。

2.3. 代码缓存

为了在不同的平台上运行JVM字节码,它需要将其转换为机器码。JIT编译器负责其编译操作。

**当JVM编译字节码到汇编指令时,它保存这些指令在一个特殊的非堆区域,该区域称为CodeCache。*JVM可以像管理其他区域一样管理CodeCache。-XX:InitialCodeCacheSize-XX:ReservedCodeCacheSize*标记决定了CodeCache的初始值和可能的最大值。

2.4. 垃圾回收

JVM有很多GC算法以针对不同的情形。所有的这些GC都有一个共同的特点:那就是它们需要一些非堆的结构来运行它们的任务。这些内部结构会消耗更多的本地内存。

2.5. 符号(Symbols)

让我们从String开始,这是在应用和库代码中使用最多的一种数据类型。因为它们的特殊性,它们通常会占据堆的很大一部分。如果很多的字符串共享同样的内容,则堆内存中的很大一部分都浪费了。

为了解决堆空间,我们保存每一个String的唯一版本然后让其它的变量引用这个版本。该过程称为*String Interning***。因为JVM只能intern编译时间字符串常量,我们能够手动调用intern()方法在我们想去intern的字符串上。

JVM stores interned strings in a special native fixed-sized hashtable called the String Table, also known as the *String Pool*. We can configure the table size (i.e. the number of buckets) via the -XX:StringTableSize tuning flag.

**JVM有一个本地固定大小的哈希表(该表称为String Table)来保存内部字符串,这个表也称为String Pool。**我们能够通过-XX:StringTableSize标志配置表的大小(例如桶的数量)。

除了String table,还有另外一个称为运行时常量池的内部区域。JVM使用该区域保存常量,例如编译时的数字字面量或者必须在运行时保存的方法和字段引用。

2.6. 本地字节缓存

JVM通常是自己做本地分配,到有时开发者想直接分配本地内存。通常使用的方法是通过JNI调用malloc或者NIO的直接ByteBuffers.

2.7. 其他标志

在本节,我们为不同的优化场景使用了一些JVM标志。用如下的命令可以找到所有相关的概念:

$ java -XX:+PrintFlagsFinal -version | grep <concept>

The PrintFlagsFinal prints all the –XX options in JVM. For example, to find all Metaspace related flags:

$ java -XX:+PrintFlagsFinal -version | grep Metaspace
      // truncated
      uintx MaxMetaspaceSize                          = 18446744073709547520                    {product}
      uintx MetaspaceSize                             = 21807104                                {pd product}
      // truncated

3. Native Memory Tracking (NMT)

现在我们知道在JVM中本地内存分配的一些常见的源,现在是找出如何监视它们的时候了。首先,我们应该通过使用其他的JVM标志是使得可以本地内存追踪:-XX:NativeMemoryTracking=off|summary|detail。默认地,NMT是关闭的,但是我们能够开启它。

假设我们想要跟踪一个典型的SpringBoot应用的本地分配:

$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar

Here, we’re enabling the NMT while allocating 300 MB of heap space, with G1 as our GC algorithm.

在这里,当分配300MB的堆大小的时候,我们正在启动NMT,同时,我们使用G1作为我们的GC算法。

3.1. 即时快照

当NMT被启动,我们能够在任何时候通过使用jcmd命令获得本地内存信息:

$ jcmd <pid> VM.native_memory

为了找到一个JVM应用程序的PID,我们使用jps命令:

$ jps -l                    
7858 app.jar // This is our app
7899 sun.tools.jps.Jps

现在如果我们使用jcmd获得pid,则VM.native_memory让JVM打印出有关本地分配的信息:

$ jcmd 7858 VM.native_memory

让我们分析一段一段的分析出NMT的输出。

3.2. 全部分配

NMT打印全部的保留和使用的内存如下:

Native Memory Tracking:
Total: reserved=1731124KB, committed=448152KB

保留内存显示了我们应用程序能够使用的全部内存。相对地,使用内存是当前正在使用的内存。

尽管分配了300MB的堆内存,但是我们应用程序全部的保留内存大约1.7GB,远远大于堆内存。类似,使用内存大约400MB,也大于300MB。

After the total section, NMT reports memory allocations per allocation source. So, let’s explore each source in depth.

在本节后,NMT打印了每个源的内存分配。让我们更深一步了解一下。

3.3. 堆

NMT打印我们的堆内存,确实像我们前面设置的一样:

Java Heap (reserved=307200KB, committed=307200KB)
          (mmap: reserved=307200KB, committed=307200KB)

300MB的保留和使用内存,符合我们设置的情况。

3.4. 元数据区(Metaspace)

下面是加载的类的类元数据的NMT信息:

Class (reserved=1091407KB, committed=45815KB)
      (classes #6566)
      (malloc=10063KB #8519) 
      (mmap: reserved=1081344KB, committed=35752KB)

大约1GB的保留和45MB的使用空间区加载6566个类。

3.5. 线程

下面是线程的内存分配信息:

Thread (reserved=37018KB, committed=37018KB)
       (thread #37)
       (stack: reserved=36864KB, committed=36864KB)
       (malloc=112KB #190) 
       (arena=42KB #72)

总共,36MB的内存被分配到了37个线程——大约每个栈有1MB的内存。JVM分配内存到线程是在JVM创建之初,所以保留内存和使用内存是相等的。

3.6. Code Cache

让我们看看JIT产生的汇编指令所占的空间:

Code (reserved=251549KB, committed=14169KB)
     (malloc=1949KB #3424) 
     (mmap: reserved=249600KB, committed=12220KB)

当前,大约13MB的空间被会缓存了,并且能使用的空间大约在245MB。

3.7. GC

下面是NMT报告的G1GC的内存使用情况:

GC (reserved=61771KB, committed=61771KB)
   (malloc=17603KB #4501) 
   (mmap: reserved=44168KB, committed=44168KB)

我们能看到,大约60MB的空间是G1可以保留和使用的。

让我们看看更简单的一种GC,SerialGC:

$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar

SerialGC仅仅使用1MB:

GC (reserved=1034KB, committed=1034KB)
   (malloc=26KB #158) 
   (mmap: reserved=1008KB, committed=1008KB)

明显地,我们不应该挑选一个GC算法仅仅是因为内存使用,作为SerialGC的STW(stop-the-world)属性可能导致性能下降。然而,有其他几种GC可供选择,并且它们在不同情况下能够平衡内存和性能。

3.8. 符号(Symbol)

下面是NMT打印有关符号的分配,如同String table和常量池:

Symbol (reserved=10148KB, committed=10148KB)
       (malloc=7295KB #66194) 
       (arena=2853KB #1)

大约10MB被分配给符号使用。

3.9. NMT 监控时间段

**NMT允许我们追踪在一段时间内的内存改变情况。**首先我们应该标记当前我们应用程序的状态作为基线:

$ jcmd <pid> VM.native_memory baseline
Baseline succeeded

然后,过一段时间,我们就能够比较出当前内存与基线之间的差别:

$ jcmd <pid> VM.native_memory summary.diff

现在,使用+和-标记,就能够告诉我们在这段时间内内存的使用情况:

4. 结论

本文,我们列举了在JVM中本地内存分配的使用方法。然后,我们学习了怎么监控一个运行中的应用程序。有了这些内容,我们就能更加有效地调整我们的应用并且规划我们的运行时环境。

  • 7
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
native memory tracking (本地内存跟踪) 是一种用于识别和定位Java应用程序本地内存使用情况的工具。它可以帮助我们分析和优化应用程序的内存使用,特别是与本机代码相关的部分。 使用native memory tracking,可以按照以下步骤进行: 1. 启用native memory tracking:在JVM启动时,需要添加"-XX:NativeMemoryTracking=summary"标志来启用本地内存跟踪。此标志告诉JVM在应用程序运行时跟踪本地内存的分配和释放。 2. 运行应用程序:使用启用了本地内存跟踪的JVM运行应用程序,可以是命令行应用程序、Web应用程序或其他类型的Java应用程序。 3. 分析native memory tracking数据:当应用程序运行结束后,可以使用JVM提供的工具来分析本地内存跟踪数据。其包括以下工具: - jcmd命令:运行jcmd命令并指定应用程序的进程ID,然后使用"VM.native_memory summary"子命令来查看本地内存跟踪的摘要信息。 - jmap命令:运行jmap命令并指定应用程序的进程ID,然后使用"-F"选项和"hprof"子命令来生成一个本地内存跟踪的快照。 - jconsole或Java Mission Control:可以使用这些工具对本地内存跟踪数据进行可视化分析。 4. 分析native memory tracking数据:通过分析本地内存跟踪数据,可以了解Java应用程序本地内存的使用情况,包括本地内存的总量、分配和释放的数量,以及与本机代码相关的详细信息。可以识别潜在的内存泄漏、优化内存使用和调优本机代码等问题。 需要注意的是,native memory trackingJVM的性能有一定的影响,因此在生产环境使用时需要谨慎考虑,并根据具体情况选择合适的数据分析方法和工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值