最近在对云主机的内存占用优化中,又有了新的认识,网上对JAVA的native memory 的认知五花八门,对direct memory 的获取的有偏差,今天我们来好好理一理,以便我们对JAVA 内存有个更好的认知。
理论部分:
首先,JAVA应用的内存到底包含了哪些部分?下图可以完整的表达,
从上图我们可以认识到,JAVA 应用包含Heap Memory,Non-heap Memory,以及Direct Memory.
1.Heap Memory(堆内存)
这块主要是有 Eden Space(伊甸园), Survivor Space (幸存者区), Old Gen (老年代)组成。
这块数据是可以监控到的,这个后面再提。
2.Non-heap Memory(堆外内存)
包含了Metaspace(元数据区或者叫元空间),Code cache(代码缓存,JIT产生的汇编指令所占的空间), Compressed Class Space 是 Metaspace 的一部分,默认大小为 1G,这三块属于可观察部分,这个也有相对应的API可以获取到。
我们看下metaSpace的JDK默认值,
[root@VM-16-13-centos ~]# java -XX:+PrintFlagsFinal -version | grep MetaspaceSize size_t InitialBootClassLoaderMetaspaceSize = 4194304 {product} {default} size_t MaxMetaspaceSize = 18446744073709547520 {product} {default} size_t MetaspaceSize = 21807104 {pd product} {default}
其中MaxMetaspace可以认为是无上限的值。
代码缓存如果 -XX:ReservedCodeCacheSize >= 240m,则代码分成三个区域:
其中 Code cache包含以下三个区域:
- CodeHeap ‘non-nmethods’ JVM 自己用的代码
- CodeHeap ‘non-profiled nmethods’, 完全优化的机器码
- CodeHeap ‘profiled nmethods’ 部分优化的机器码
如果 -XX:ReservedCodeCacheSize < 240m,则它们存在一起 不进行区分
另一部分,Thread Stack(线程堆栈),GC(执行GC逻辑所需的空间),internal,Symbol(有关符号的分配,如同String table和常量池),Other (Direct Memory),这部分暂时还没找到用API获取的方式,但也是能通过其他方式获取到。
3.Direct Memory(直接内存)
直接内存是和java.nio.DirectByteBuffer类相关联的,常被用在第三方库,比如nio和gzip。
这里有个疑问,经过观察,上面堆外内存的Other 和Direct Memory 是一样的,那么,上面那张图,那为何要独立分出这一部分来?
实验部分:
接下去我们通过实验来相互印证一些数据和想法。
准备环境:
JDK:
openjdk version "11.0.16.1" 2022-08-16 OpenJDK Runtime Environment TencentKonaJDK (build 11.0.16.1+2) OpenJDK 64-Bit Server VM TencentKonaJDK (build 11.0.16.1+2, mixed mode)
启动参数:
java -server -Xms256M -Xmx256M -XX:+UseG1GC -XX:MaxMetaspaceSize=1024m -XX:NativeMemoryTracking=detail -jar game.jar
在这里,我们分配256MB的堆大小,同时,使用G1作为我们的GC算法,并且启动了本地内存追踪XX:NativeMemoryTracking=detail ,
1.Platform MXBeans API 监控快照
max heap memory[G1 Eden Space,G1 Survivor Space,G1 Old Gen] 256 MB,used 169 MB [91, 3, 75], max non-heap[CodeHeap 'non-nmethods',CodeHeap 'non-profiled nmethods',CodeHeap 'profiled nmethods',Compressed Class Space,Metaspace] memory 2280 MB,used 165 MB [1, 7, 26, 14, 116], committed 172 MB, max direct memory 256 MB,used 4 MB
通过每分钟的内存监控打印,我们可以看到,
堆的使用空间为G1 Eden Space,G1 Survivor Space,G1 Old Gen 相加,总量的调用API为:
ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
非堆空间(可监控的) 为CodeHeap ‘non-nmethods’,CodeHeap ‘non-profiled nmethods’,CodeHeap ‘profiled nmethods’,Compressed Class Space,Metaspace相加,总量的调用API 为:ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage();
2.MetaSpace 快照:
jcmd 18228 GC.heap_info 18228: garbage-first heap total 262144K, used 205311K [0x00000000f0000000, 0x0000000100000000) region size 1024K, 126 young (129024K), 3 survivors (3072K) Metaspace used 118903K, capacity 121481K, committed 121632K, reserved 1146880K class space used 14438K, capacity 15451K, committed 15564K, reserved 1040384K
这里的 class Space 14438K,Metaspace 118903K 和API 的 14M,116M 基本能对应上。
3.Native Memory 快照:
jcmd 18228 VM.native_memory summary scale=MB 18228: Native Memory Tracking: Total: reserved=1795MB, committed=520MB - Java Heap (reserved=256MB, committed=256MB) (mmap: reserved=256MB, committed=256MB) - Class (reserved=1123MB, committed=122MB) (classes #22297) ( instance classes #20922, array classes #1375) (malloc=3MB #54279) (mmap: reserved=1120MB, committed=119MB) ( Metadata: ) ( reserved=104MB, committed=104MB) ( used=102MB) ( free=2MB) ( waste=0MB =0.00%) ( Class space:) ( reserved=1016MB, committed=15MB) ( used=14MB) ( free=1MB) ( waste=0MB =0.00%) - Thread (reserved=80MB, committed=8MB) (thread #79) (stack: reserved=79MB, committed=8MB) - Code (reserved=245MB, committed=42MB) (malloc=3MB #14293) (mmap: reserved=242MB, committed=39MB) - GC (reserved=48MB, committed=48MB) (malloc=7MB #20188) (mmap: reserved=42MB, committed=42MB) - Internal (reserved=1MB, committed=1MB) (malloc=1MB #2072) - Other (reserved=4MB, committed=4MB) (malloc=4MB #22) - Symbol (reserved=26MB, committed=26MB) (malloc=22MB #633541) (arena=4MB #1) - Native Memory Tracking (reserved=12MB, committed=12MB) (tracking overhead=11MB)
让我们一段一段的分析NMT的输出。
先插入一个常识:
reserved :操作系统已经为该进程“保留”的。所谓的保留,更加接近一种记账的概念,就是操作系统承诺最多可以给你多少资源,到时候并不一定能申请的到。 used:你当前正在使用的有多少资源。 committed:进程已经申请进的内存,可能在被使用,有可能闲置着。used<=committed
3.1 分配的内存
Native Memory Tracking: Total: reserved=1795MB, committed=520MB
reserved显示了我们应用程序能够预定的最大内存。相对地,committed内存是当前已经申请进JAVA进程的内存。
尽管分配了256 MB的堆内存,但是我们应用程序全部预定内存大约1.7 GB,远远大于堆内存。类似,使用内存大约520 MB,也大于256 MB。
3.2 堆
NMT打印我们的堆内存,确实像我们前面设置的一样:
Java Heap (reserved=256MB, committed=256MB) (mmap: reserved=256MB, committed=256MB)
256MB的保留和使用内存,符合我们设置的情况。
3.3 元数据区(Metaspace)
下面是加载的类的类元数据的信息:
Class (reserved=1123MB, committed=122MB) (classes #22297) ( instance classes #20922, array classes #1375) (malloc=3MB #54279) (mmap: reserved=1120MB, committed=119MB) ( Metadata: ) ( reserved=104MB, committed=104MB) ( used=102MB) ( free=2MB) ( waste=0MB =0.00%) ( Class space:) ( reserved=1016MB, committed=15MB) ( used=14MB) ( free=1MB) ( waste=0MB =0.00%)
大约1123MB的预定内存和122 MB的使用空间区加载22297个类。这里的used MetaData 102MB 和used Class space 14MB 没有和API获得的完全对应上 [Metaspace,Compressed Class Space] memory 2280 MB,used 165 MB [116,14] .这是为什么?
3.4 线程空间(Thread)
下面是线程的内存分配信息:
Thread (reserved=80MB, committed=8MB) (thread #79) (stack: reserved=79MB, committed=8MB)
总共,8MB的内存被分配到了79个线程——大约每个栈有0.1MB的内存。
3.5 代码缓存(Code Cache)
让我们看看JIT产生的汇编指令所占的空间:
Code (reserved=245MB, committed=42MB) (malloc=3MB #14293) (mmap: reserved=242MB, committed=39MB)
这个也在API 中反映, [CodeHeap ‘non-nmethods’,CodeHeap ‘non-profiled nmethods’,CodeHeap ‘profiled nmethods’] memory [1, 7, 26] 共 34MB,
当前,大约42MB的空间被会缓存了,并且能使用的空间大约在245MB。
3.6 GC
下面是NMT报告的G1GC的内存使用情况:
GC (reserved=48MB, committed=48MB) (malloc=7MB #20188) (mmap: reserved=42MB, committed=42MB)
我们能看到,大约48MB的空间是G1可以保留和使用的。
3.7 其他(Other)
Other (reserved=4MB, committed=4MB) (malloc=4MB #22)
回顾下我们API监控的日志“max direct memory 256 MB,used 4 MB”
从Other部分可以看到其值跟Direct Memory使用的值一致,改变ByteBuffer.allocateDirect的值再重新查看,可以发现Other部分跟着改变;因而初步断定Other部分应该是可以反映direct memory的使用大小
3.8 符号(Symbol)
下面是NMT打印有关符号的分配,如同String table和常量池:
Symbol (reserved=26MB, committed=26MB) (malloc=22MB #633541) (arena=4MB #1)
大约26MB被分配给Symbol使用。
4.NMT 监控时间段
NMT允许我们追踪在一段时间内的内存改变情况。首先我们应该标记当前我们应用程序的状态作为基线:
$ jcmd VM.native_memory baseline
Baseline succeeded
1
2
然后,过一段时间,我们就能够比较出当前内存与基线之间的差别:
$ jcmd VM.native_memory summary.diff
1
现在,使用+和-标记,就能够告诉我们在这段时间内内存的使用情况:
总结
我们通过API ,MetaData , Native memory 快照,让我们对JAVA内存结构有了更深度的认识,以便以后在分析问题时,能有更客观的数据分析的基础。
参考文献: