有没有想过为何Java应用程序经过众所周知的-Xms和-Xmx调优标志消耗的内存比指定数量多得多?出于各类缘由和可能的优化,JVM能够分配额外的本机内存。这些额外的分配最终会使消耗的内存超出-Xmx限制。java
在本教程中,咱们将列举JVM中的一些常见内存分配源,以及它们的大小调整标志,而后学习如何使用本机内存跟踪监视它们。算法
堆一般是Java应用程序中最大的内存使用者,但还有其余人。除了堆以外,JVM还从本机内存中分配出一个至关大的块来维护类的元数据,应用程序代码,JIT生成的代码,内部数据结构等。在下面的部分中,咱们将探讨其中的一些分配。缓存
为了维护有关已加载类的一些元数据,JVM使用名为Metaspace的专用非堆区域。在Java 8以前,被称为PermGen或Permanent Generation。 Metaspace或PermGen包含有关已加载类的元数据,而不是它们的实例,它们保存在堆中。数据结构
这里重要的是堆大小配置不会影响元空间大小,由于Metaspace是一个堆外数据区。为了限制Metaspace大小,咱们使用其余调优标志:app
JVM中最耗费内存的数据区之一是堆栈,与每一个线程同时建立。堆栈存储局部变量和部分结果,在方法调用中起着重要做用。jvm
默认的线程堆栈大小取决于平台,但在大多数现代64位操做系统中,它大约为1 MB。此大小可经过-Xss调整标志进行配置。性能
与其余数据区域相比,当对线程数没有限制时,分配给堆栈的总内存其实是无限制的。值得一提的是,JVM自己须要一些线程来执行其内部操做,如GC或即时编译。学习
为了在不一样平台上运行JVM字节码,须要将其转换为机器指令。执行程序时,JIT编译器负责此编译。优化
当JVM将字节码编译为汇编指令时,它会将这些指令存储在称为代码缓存的特殊非堆数据区中。能够像管理JVM中的其余数据区同样管理代码缓存。 -XX:InitialCodeCacheSize
和-XX:ReservedCodeCacheSize
调整标志肯定代码缓存的初始值和可能最大值。ui
JVM附带了一些GC算法,每一个算法适用于不一样的用例。全部这些GC算法都有一个共同的特色:他们须要使用一些堆外数据结构来执行他们的任务。这些内部数据结构消耗更多本机内存。
让咱们从 Strings 开始,这是应用程序和库代码中最经常使用的数据类型之一。因为它们无处不在,它们一般占据堆的很大一部分。若是大量的这些字符串包含相同的内容,那么堆的很大一部分将被浪费。
为了节省一些堆空间,咱们能够存储每一个 String 的一个版本,并让其余版本引用存储的版本。此过程称为 String Interning 。因为JVM只能内部编译时间字符串常量,咱们能够手动调用字符串的intern方法来获取内部编译字符串。
JVM将实际存储的字符串存储在本机特殊固定大小并称为字符串表的哈希表中,也称为字符串池。咱们能够经过-XX:StringTableSize
调整标志配置表大小(即桶的数量)。
除了字符串表以外,还有另外一个称为运行时常量池的本机数据区域。 JVM使用此池来存储常量,如编译时数字文字或必须在运行时解析的方法和字段引用。
JVM一般有大量分配本机内存的嫌疑,但有时开发人员也能够直接分配本机内存。最多见的方法是被JNI调用的malloc和NIO中可直接调用的ByteBuffers。
在本节中,咱们针对不一样的优化方案使用了少许JVM调优标志。使用如下提示,咱们几乎能够找到与特定概念相关的全部调优标志:
$ java -XX:+PrintFlagsFinal -version | grep <concept>
PrintFlagsFinal打印JVM中的全部-XX选项。例如,要查找全部与Metaspace相关的标志:
$ java -XX:+PrintFlagsFinal -version | grep Metaspace // truncated uintx MaxMetaspaceSize = 18446744073709547520 {product} uintx MetaspaceSize = 21807104 {pd product} // truncated
如今咱们已经了解了JVM中本机内存分配的常见来源,如今是时候找出如何监视它们了。首先,咱们应该使用另外一个JVM调优标志启用本机内存跟踪:-XX:NativeMemoryTracking = off | sumary | detail
。默认状况下,NMT处于关闭状态,但咱们可使其查看其观察的摘要或详细视图。
假设咱们想要跟踪典型Spring Boot应用程序的本机分配:
$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar
在这里,咱们在分配300 MB堆空间的同时启用NMT,G1做为咱们的GC算法。
启用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输出。
NMT报告所有保留和提交的内存以下:
Native Memory Tracking: Total: reserved=1731124KB, committed=448152KB
保留内存表示咱们的应用程序可能使用的内存总量。相反,提交的内存表示咱们的应用程序如今使用的内存量。
尽管分配了300MB的堆,咱们的应用程序的总预留内存几乎是1.7 GB,远远超过它。相似地,提交的内存大约为440 MB,这再次远远超过300 MB。
在总体了解以后,NMT报告每一个分配源的内存分配。因此,让咱们深刻探讨每一个来源。
NMT按咱们的预期报告堆分配:
Java Heap (reserved=307200KB, committed=307200KB) (mmap: reserved=307200KB, committed=307200KB)
300 MB的保留和已提交内存,与咱们的堆大小设置相匹配。
这是NMT关于加载类的元数据的报告:
Class (reserved=1091407KB, committed=45815KB) (classes #6566) (malloc=10063KB #8519) (mmap: reserved=1081344KB, committed=35752KB)
几乎保留了1 GB,45 MB保留加载6566个类。
这是关于线程分配的NMT报告:
Thread (reserved=37018KB, committed=37018KB) (thread #37) (stack: reserved=36864KB, committed=36864KB) (malloc=112KB #190) (arena=42KB #72)
总共有36 MB的内存被分配给37个线程的堆栈 - 每一个堆栈大约1 MB。 JVM在建立时将内存分配给线程,所以保留和提交的分配是相等的。
让咱们看看NMT对JIT生成和缓存的汇编指令的报告:
Code (reserved=251549KB, committed=14169KB) (malloc=1949KB #3424) (mmap: reserved=249600KB, committed=12220KB)
目前,正在缓存大约13 MB的代码,这个数量可能会达到245 MB。
如下是有关G1 GC内存使用状况的NMT报告:
GC (reserved=61771KB, committed=61771KB) (malloc=17603KB #4501) (mmap: reserved=44168KB, committed=44168KB)
咱们能够看到,保留和已提交都接近60 MB,致力于帮助G1。
让咱们来看看更简单的GC的内存使用状况,好比Serial GC:
$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar
Serial GC 几乎使用不到1 MB:
GC (reserved=1034KB, committed=1034KB) (malloc=26KB #158) (mmap: reserved=1008KB, committed=1008KB)
显然,咱们不能仅仅由于其内存使用而选择GC算法,由于串行GC的暂停回收本质可能会致使性能降低。可是,还有几个GC可供选择,它们各自平衡内存和性能。
如下是有关符号分配的NMT报告,例如字符串表和常量池:
Symbol (reserved=10148KB, committed=10148KB) (malloc=7295KB #66194) (arena=2853KB #1)
将近10 MB分配给符号。
NMT容许咱们跟踪内存分配如何随时间变化。首先,咱们应该将应用程序的当前状态标记为基线:
$ jcmd <pid> VM.native_memory baseline Baseline succeeded
而后,过了一下子,咱们能够将当前的内存使用状况与该基线(baseline)进行比较:
$ jcmd <pid> VM.native_memory summary.diff
NMT使用+和 - 符号将告诉咱们在此期间内存使用状况如何变化:
Total: reserved=1771487KB +3373KB, committed=491491KB +6873KB - Java Heap (reserved=307200KB, committed=307200KB) (mmap: reserved=307200KB, committed=307200KB) - Class (reserved=1084300KB +2103KB, committed=39356KB +2871KB) // Truncated
保留和提交的总内存分别增长了3 MB和6 MB。能够很容易地发现内存分配的其余波动。
NMT能够提供很是详细的有关整个存储空间映射的信息。要启用此详细报告,咱们应使用 -XX:NativeMemoryTracking =detail
信息调整标志。
在本文中,咱们列举了JVM中本机内存分配的不一样使用者。而后,咱们学习了如何检查正在运行的应用程序以监视其本机分配。借助以上这些,咱们能够更有效地调整应用程序以及运行时环境的大小。
原文:https://www.baeldung.com/native-memory-tracking-in-jvm
做者:Ali Dehghani
译者:Emma