\
本地内存跟踪NMT详解 1. Overview 为什么java程序消耗的内存,远超-Xms、-Xmx的限制?因为各种原因,或是为了进行某些优化,JVM会额外分配内存。这些额外的分配,会导致java程序占用的内存,超出-Xmx的限制。 本文档列举了通常情况下,JVM会分配哪几部分内存,以及各部分调整大小的方法。然后,了解如何使用Native Memory Tracking工具进行监控。 2. Native Allocations 通常,heap是java程序占用内存中的最大的一部分,但也有特例。除了heap,JVM会分配很大一块内存,用于存储metadata、application code、the code generated by JIT、internal data structures等等。如下章节,对各部分进行详述。 2.1. Metaspace JVM使用专用的non-heap区域,存储已加载类的元数据。java 8版本以前,此区域称为PermGen or Permanent Generation。此区域存储的是类的元数据,而不是类的实例,实例是存储在heap内存中的。 对heap内存的限制,是无法影响Metaspace的。如果要调整Metaspace,要使用如下标志: -XX:MetaspaceSize,最小值 -XX:MaxMetaspaceSize,最大值 Java 8版本以前,使用-XX:PermSize、-XX:MaxPermSize,含义是一样的。 2.2. Threads 另一个消耗内存较大的部分是stack。创建线程时,同时创建stack。stack存储局部变量和中间结果,在方法调用中扮演重要角色。 线程stack的默认大小与平台相关,但是,对于现在大多的64位操作系统来说,大约是1MB。此值可以通过-Xss调整。 创建的线程越多,此部分占用的内存越多。 另外需要注意的是,JVM本身也需要一些线程,用于执行内部操作,如:GC、JIT编译。 2.3. Code Cache 为了在不同平台运行JVM字节码,需要将其转换成机器指令。程序运行时,JIT编译器负责这个编译工作。当JVM将字节码编译为汇编指令时,它将这些指令存储在一个特殊non-heap区域:Code Cache。Code Cache可以像JVM的其他数据区域一样被管理。控制此区域的大小,使用如下两个指令: -XX:InitialCodeCacheSize,初始值 -XX:ReservedCodeCacheSize,最大值 2.4. Garbage Collection JVM附带了一些GC算法,每个算法都适用于不同的用例。所有这些GC算法都有一个共同的特征:他们需要一些堆外数据结构来执行任务。这些内部数据结构,会消耗一些内存。 2.5. Symbols 让我们从Strings开始,这是最常用的数据类型之一。因为其使用频率很高,Strings通常会占用较大一部分heap内存。如果大量的Strings包含相同的内容,那么会造成heap内存的浪费。 为了节省内存,对于每一个String可以仅存一个副本,然后其他的指向该副本。这个过程称为字符串驻留(String Interning)。JVM仅可以驻留编译时的字符串常量(Compile Time String Constants),我们可以对strings手动调用intern()方法以实现驻留。 JVM将驻留的strings存储在一个专用的固定大小的hashtable中,称为String Table,也称为String Pool。可以通过如下标志调节其大小:-XX:StringTableSize。 除了String Table,还有一个内存区域称为运行时常量池(Runtime Constant Pool),JVM使用这个池来存储一些必须在运行时解析的常量,如编译时数值常量或方法和字段引用。 2.6. Native Byte Buffers JVM通常是大量内存占用的可疑对象,但有时开发人员也可以直接分配内存。最常见的方式是: malloc call by JNI; NIO's direct ByteBuffers; 2.7. Additional Tuning Flags 本章节使用了一些JVM调节标志。使用如下命令,可以找到几乎所有的、关于特定概念的调节标志。 java -XX:+PrintFlagsFinal -version | grep <concept> 1. PrintFlagsFinal会打印出JVM中所有的-XX标志。例如,找出所有关于Metaspace的标志: java -XX:+PrintFlagsFinal -version | grep Metaspace // truncated uintx MaxMetaspaceSize = 18446744073709547520 {product} uintx MetaspaceSize = 21807104 {pd product} // truncated 1. 2. 3. 4. 5. 3. Native Memory Tracking (NMT) 我们知道了JVM中消耗内存的几个源头,现在就来看看如何监视它们。首先,启用NMT,在启动命令中加入如下标志即可: -XX:NativeMemoryTracking=off|sumary|detail 1. NMT默认是关闭的。 假设,我们想要跟踪一个典型的SpringBoot应用程序: java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar 1. 3.1. Instant Snapshots 开启NMT后,可以使用如下命令,随时获取本地内存占用信息。其中表示java进程的id。 jcmd <pid> VM.native_memory 1. 下面详解NMT命令的输出内容。 3.2. Total Allocations NMT显示总的预留内存、已提交内存: Native Memory Tracking: Total: reserved=1731124KB, committed=448152KB 1. 2. 预留内存表示我们的应用程序可能使用的内存总量。已提交内存表示应用程序当前使用的内存。 尽管仅为应用程序分配了300MB内存,但总的预留内存近1.7GB。类似的,已提交内存近440M。这两个数据都比300MB多了很多。 除了整体的内存占用信息外,NMT还报告了各个源头占用内存的情况。下面章节详述。 3.3. Heap heap内存占用情况显示如下: Java Heap (reserved=307200KB, committed=307200KB) (mmap: reserved=307200KB, committed=307200KB) 1. 2. 预留内存、已提交内存均为300MB,符合我们对heap内存的设置。 3.4. Metaspace 已加载类的元数据内存占用信息如下: Class (reserved=1091407KB, committed=45815KB) (classes #6566) (malloc=10063KB #8519) (mmap: reserved=1081344KB, committed=35752KB) 1. 2. 3. 4. 加载了6566个class,预留内存差不多1G,提交内存45M。 3.5. Thread 线程内存分配情况如下: Thread (reserved=37018KB, committed=37018KB) (thread #37) (stack: reserved=36864KB, committed=36864KB) (malloc=112KB #190) (arena=42KB #72) 1. 2. 3. 4. 5. 37个线程,stack内存共计36M,差不多每个线程的stack占用1M。JVM在创建线程时,同时分配stack内存,所以预留内存和提交内存是一样的。 3.6. Code Cache JIT生成并缓存的汇编指令的内存占用情况: Code (reserved=251549KB, committed=14169KB) (malloc=1949KB #3424) (mmap: reserved=249600KB, committed=12220KB) 1. 2. 3. 当前,大概13M的内存占用,可能会增加到大约245M(预留内存)。 3.7. GC G1 GC内存占用情况如下: GC (reserved=61771KB, committed=61771KB) (malloc=17603KB #4501) (mmap: reserved=44168KB, committed=44168KB) 1. 2. 3. 预留内存大概60M。 Serial GC是一个简单的多的方法,当使用此方法时,配置方法如下: java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar 1. 内存占用情况如下,仅仅用了1M: GC (reserved=1034KB, committed=1034KB) (malloc=26KB #158) (mmap: reserved=1008KB, committed=1008KB) 1. 2. 3. 当然,我们不能仅根据内存消耗来决定选择什么GC算法,因为Serial GC的“stop-the-world”特性,可能会导致性能下降。 3.8. Symbol symbol内存占用情况如下,如string table和constant pool: Symbol (reserved=10148KB, committed=10148KB) (malloc=7295KB #66194) (arena=2853KB #1) 1. 2. 3. 大概占用10M。 3.9. NMT over Time NMT使得我们可以跟踪内存占用情况。首先,要记录应用程序当前的内存占用情况,做为基线。命令如下: $ jcmd <pid> VM.native_memory baseline Baseline succeeded 1. 2. 然后,过一段时间,可以将当前内存占用与基线做比较: $ 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 1. 2. 3. 4. 5. 6. 7. 8. 9. 预留内存、提交内存分别增长了3M、6M。内存分配中的其他波动也可以很容易地发现。 3.10. Detailed NMT NMT可以提供关于整个内存空间占用情况的非常详细的信息。要显示详细信息,要使用如下标志: -XX:NativeMemoryTracking=detail 1. 4. Conclusion 我们列举了JVM中内存占用的不同类别。然后,我们学习了如何监控一个正在运行的应用程序的内存占用情况。有了这些,我们可以更有效地调整运行时环境的大小。 关于JIT 对于Java语言: 一、你可以说它是编译型的:因为所有的Java代码都是要编译的,.java不经过编译就什么用都没有。 二、你可以说它是解释型的:因为java代码编译后不能直接运行,它是解释运行在JVM上的,所以它是解释运行的,那也就算是解释的了。 三、但是,现在的JVM为了效率,都有一些JIT优化。它又会把.class的二进制代码编译为本地的代码(汇编)直接运行,所以,又是编译的。 像C、C++ 他们经过一次编译之后直接可以编译成操作系统了解的类型,可以直接执行的,所以他们是编译型的语言。没有经过第二次的处理。 而Java不一样,他首先由编译器编译成.class类型的文件,这个是java自己类型的文件 然后再通过虚拟机(JVM)从.class文件中读一行解释执行一行,所以他是解释型的语言,而由于java对于多种不同的操作系统有不同的JVM,所以,Java实现了真正意义上的跨平台! JIT:Just In Time Compiler,一般翻译为即时编译器,这是是针对解释型语言而言的,而且并非虚拟机必须,是一种优化手段,Java的商用虚拟机HotSpot就有这种技术手段,Java虚拟机标准对JIT的存在没有作出任何规范,所以这是虚拟机实现的自定义优化技术。 HotSpot虚拟机的执行引擎在执行Java代码是可以采用【解释执行】和【编译执行】两种方式的,如果采用的是编译执行方式,那么就会使用到JIT,而解释执行就不会使用到JIT,所以,早期说Java是解释型语言,是没有任何问题的,而在拥有JIT的Java虚拟机环境下,说Java是解释型语言严格意义上已经不正确了。 HotSpot中的编译器是javac,他的工作是将源代码编译成字节码,这部分工作是完全独立的,完全不需要运行时参与,所以Java程序的编译是半独立的实现。有了字节码,就有解释器来进行解释执行,这是早期虚拟机的工作流程,后来,虚拟机会将执行频率高的方法或语句块通过JIT编译成本地机器码,提高了代码执行的效率。 --结束--
标签:NMT,reserved,占用,XX,详解,内存,JVM,committed From: https://www.cnblogs.com/iancloud/p/17616682.html