内存管理
1. 总结
java应用的内存主要分为三块
- jvm管理的堆内存
- jvm管理的非堆内存
- 非jvm管理的内存
1.1. JVM管理的堆内存(Heap Memory)
存储对象实例,是 Java 内存管理的核心区域,由所有线程共享。
- 新生代(Young Generation):存放新创建的对象,包括 Eden 区和两个 Survivor 区(S0、S1),对象在此经历多次 GC 后若存活会晋升到老年代。
- 老年代(Old Generation):存储生命周期较长的对象,如长期持有的缓存对象。
1.2. JVM管理的非堆内存(Non-Heap Memory)
存储与JVM运行时环境相关的数据,不直接存放对象实例。
- 元空间(Metaspace):逻辑上仍属堆外内存,但由JVM管理,存放类结构、方法字节码、静态变量等。
- JVM 栈(JVM Stack):每个线程独有,存储栈帧(局部变量、方法参数、操作数栈等),栈深度过大会导致
StackOverflowError。 - 本地方法栈(Native Method Stack):用于调用 Native 方法(如 JVM 底层 C/C++ 代码)的栈空间。
- 程序计数器(Program Counter Register):记录当前线程执行的字节码位置,是最小的内存区域。
1.3. 非JVM管理的内存(Off-Heap Memory)
由操作系统直接管理,JVM通过本地接口访问,不受JVM堆内存限制。
- 直接内存(Direct Memory):通过
java.nio.ByteBuffer.allocateDirect()创建,用于NIO操作(如文件读写、网络通信),避免堆内存与本地内存的拷贝开销。 - JNI本地内存:通过JNI调用C/C++代码分配的内存,需手动释放(如使用malloc,free等),否则可能导致内存泄漏。
- 堆外缓存:如Redis客户端、Netty框架的内存池,直接使用系统内存提高性能。
[!warning]
非 JVM 管理的内存不受 GC 控制,若使用不当(如大量分配未释放),可能导致系统 OOM(Out of Memory)
1.4. 不同内存的观测方法

[!note]
通过Unsafe方法直接管理的内存也可以通过jcmd观测到
2. 实验
2.1. 测试应用
测试应用可以通过以下命令获取
git clone git@github.com:shinerio/java_memory_occupy_analyzer.git
可以通过以下命令启动
java -XX:NativeMemoryTracking=detail -Xms256m -Xmx256m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=10M -jar demo-0.0.1-SNAPSHOT.jar
2.2. heap memory
2.2.1. 测试命令
curl localhost:8080/heap/128
curl localhost:8080/heap/release
2.2.2. OOM异常
java.lang.OutOfMemoryError: Java heap space
2.2.3. arthas观测
堆内存正常使用和释放
2.2.4. jcmd观测
jcmd `ps -ef|grep demo|grep -v grep|awk '{print $2}'` VM.native_memory scale=MB
关键信息如下
Total: reserved=894MB, committed=190MB
malloc: 36MB <span class="tag">#122322</span>
mmap: reserved=858MB, committed=154MB
- Java Heap (reserved=256MB, committed=66MB)
(mmap: reserved=256MB, committed=66MB, at peak)
# curl localhost:8080/heap/128
Total: reserved=879MB, committed=299MB
malloc: 21MB <span class="tag">#130291</span>
mmap: reserved=858MB, committed=279MB
- Java Heap (reserved=256MB, committed=186MB)
(mmap: reserved=256MB, committed=186MB, at peak)
# curl localhost:8080/heap/release
Total: reserved=878MB, committed=174MB
malloc: 20MB <span class="tag">#128567</span>
mmap: reserved=858MB, committed=154MB
- Java Heap (reserved=256MB, committed=64MB)
(mmap: reserved=256MB, committed=64MB, peak=237MB)
2.2.5. rss观测
-Xms参数用于指定 JVM 启动时逻辑上分配的堆内存大小,例如-Xms256m表示初始堆大小为 256MB。- 操作系统不会立即为这些内存分配物理页面(即 RAM 空间),而是在 JVM 需要实际使用内存时(例如对象创建)才逐步分配。
JVM 采用 “延迟分配” 策略: - 初始时,堆内存仅在逻辑上被预留(通过
mmap系统调用分配地址空间),但物理内存(RSS)不会立即增长。 - 当对象被创建并填充数据时,操作系统才会为这些内存区域分配实际的物理页面,此时 RSS 才会逐渐增加。
[!note]
一旦堆内存被使用后,即使后面JVM通过GC释放了堆内存,超过xms设定部分的内存也不会还给系统,体现在RSS值不会小于xms。
2.2.5.1. xms < xmx
-Xms64m -Xmx256m

- 执行
curl localhost:8080/heap/128分配堆内存 - 执行
curl localhost:8080/heap/release释放堆内存
GC日志
[2025-07-02T23:04:07.843+0800][89.112s][info][gc ] GC(9) Pause Full (System.gc()) 145M->12M(64M) 26.362ms
2.2.5.2. xms = xmx
-Xms256m -Xmx256m
- 执行
curl localhost:8080/heap/128分配堆内存 - 执行
curl localhost:8080/heap/release释放堆内存
GC日志
[2025-07-02T23:01:41.604+0800][147.067s][info][gc ] GC(7) Pause Full (System.gc()) 143M->11M(256M) 25.176ms
2.3. unsafe
- 直接使用unsafe命令分配的内存不受
-XX:MaxDirectMemorySize控制。 - unsafe底层使用malloc进行内存分配,通常是虚拟内存,操作系统仅分配虚拟地址空间,而不立即分配物理内存(RAM)。
- 只有当虚拟内存被读写时才会触发缺页中断(Page Fault)时,操作系统才会分配物理页并将其加载到RAM中,此时才会计入RSS。
2.3.1. OOM
连续执行curl localhost:8080/unsafe/512尝试分配内存,当超过系统物理内存上限后,进程会被直接kill
2025-07-02T23:08:27.140+08:00 INFO 2181 --- [demo] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2025-07-02T23:08:27.140+08:00 INFO 2181 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2025-07-02T23:08:27.141+08:00 INFO 2181 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
Killed
查看系统日志,可以看到进程直接被kill了
Jul 2 23:09:16 shinerio-huoshan kernel: oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0-1,global_oom,task_memcg=/user.slice/user-0.slice/session-1.scope,task=java,pid=2181,uid=0
Jul 2 23:09:16 shinerio-huoshan kernel: Out of memory: Killed process 2181 (java) total-vm:3353044kB, anon-rss:1445296kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:3164kB oom_score_adj:0
Jul 2 23:09:16 shinerio-huoshan systemd[1]: session-1.scope: A process of this unit has been killed by the OOM killer.
2.3.2. 测试命令
curl localhost:8080/unsafe/128
curl localhost:8080/unsafe/release
2.3.3. arthas观测
unsafe命令直接分配和释放的内存无法通过arthas观察
2.3.4. jcmd观测
分配前(无other)
- Java Heap (reserved=256MB, committed=256MB)
(mmap: reserved=256MB, committed=256MB, at peak)
分配后
- Java Heap (reserved=256MB, committed=256MB)
(mmap: reserved=256MB, committed=256MB, at peak)
- Other (reserved=128MB, committed=128MB)
(malloc=128MB <span class="tag">#5</span>) (at peak)
释放后
- Java Heap (reserved=256MB, committed=256MB)
(mmap: reserved=256MB, committed=256MB, at peak)
- Other (reserved=0MB, committed=0MB)
(malloc=0MB <span class="tag">#7</span>) (peak=128MB <span class="tag">#6</span>)
2.3.5. rss观测
直接使用unsafe命令分配和释放的内存可以通过jcmd观测
2.3.6. pmap观测
输出内存显示为anon
2.4. ByteBuffer.allocateDirect
通过ByteBuffer.allocateDirect分配和释放的内存会立即体现在RSS的变化上,其内部通过UNSAFE分配内存后,立即调用了UNSAFE.setMemory(base, size, (byte) 0);方法,触发了缺页中断,实际分配了物理内存。
2.4.1. 命令
# 分配128M直接内存
curl localhost:8080/direct_byte_buffer/128
# 释放
curl localhost:8080/direct_byte_buffer/release
2.4.2. OOM
直接内存的大小默认与-xmx设定相当,可通过以下命令显示指定
-XX:MaxDirectMemorySize=128m
超过上限后会oom
java.lang.OutOfMemoryError: Cannot reserve 134217728 bytes of direct buffer memory (allocated: 134252544, limit: 268435456)
2.4.3. arthas观测

2.4.4. jcmd观测
分配前
- Java Heap (reserved=256MB, committed=64MB)
(mmap: reserved=256MB, committed=64MB, peak=66MB)
- Compiler (reserved=0MB, committed=0MB)
(arena=0MB <span class="tag">#4</span>) (peak=24MB <span class="tag">#11</span>)
- Internal (reserved=2MB, committed=2MB)
(malloc=2MB <span class="tag">#6615</span>) (at peak)
分配后
- Java Heap (reserved=256MB, committed=64MB)
(mmap: reserved=256MB, committed=64MB, peak=66MB)
- Internal (reserved=2MB, committed=2MB)
(malloc=2MB <span class="tag">#6627</span>) (peak=2MB <span class="tag">#6615</span>)
- Other (reserved=132MB, committed=132MB)
(malloc=132MB <span class="tag">#24</span>) (at peak)
释放后
- Java Heap (reserved=256MB, committed=64MB)
(mmap: reserved=256MB, committed=64MB, peak=66MB)
- Internal (reserved=3MB, committed=3MB)
(malloc=3MB <span class="tag">#6812</span>) (peak=3MB <span class="tag">#6804</span>)
- Other (reserved=4MB, committed=4MB)
(malloc=4MB <span class="tag">#26</span>) (peak=132MB <span class="tag">#26</span>)
2.4.5. rss观测

2.5. netty(PooledByteBufAllocator)
2.5.1. 命令
curl localhost:8080/netty/128
curl localhost:8080/netty/release
2.5.2. oom
netty使用的也是直接内存,也受-XX:MaxDirectMemorySize参数控制,超过后会oom。-[Dio.netty.maxDirectMemory](Dio.netty.maxDirectMemory)Dio.netty.maxDirectMemoryDio.netty.maxDirectMemory并不会限制用例中PooledByteBufAllocator的内存分配。
java.lang.OutOfMemoryError: Cannot reserve 536870912 bytes of direct buffer memory (allocated: 4237314, limit: 268435456)
2.5.3. arthas观测

2.5.4. jcmd观测

2.5.5. rss观测

2.6. JNI
2.6.1. 启动
java -XX:NativeMemoryTracking=detail -Xms256m -Xmx256m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=10M -Djava.library.path=$PROJECT_ROOT/src/main/native -jar demo-0.0.1-SNAPSHOT.jar
2.6.2. 测试命令
curl localhost:8080/jni/128
curl localhost:8080/jni/release
2.6.3. oom
执行curl localhost:8080/jni/1280分配超大内存
# 应用被系统自动kill
2025-07-03T22:18:00.323+08:00 INFO 23454 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
Killed
# 系统日志显示oom并自动kill进程
Jul 3 22:18:12 shinerio-huoshan kernel: oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=tuned.service,mems_allowed=0-1,global_oom,task_memcg=/user.slice/user-0.slice/session-7.scope,task=java,pid=23454,uid=0
Jul 3 22:18:12 shinerio-huoshan kernel: Out of memory: Killed process 23454 (java) total-vm:3615188kB, anon-rss:1458780kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:3180kB oom_score_adj:0
Jul 3 22:18:12 shinerio-huoshan systemd[1]: session-7.scope: A process of this unit has been killed by the OOM killer.
2.6.4. arthas观测
jarthas无法观察到通过jni直接调用malloc分配的内存,其绕过了jvm内部的分配器。
2.6.5. jcmd观测
arthas无法观察到通过jni直接调用malloc分配的内存
2.6.6. rss观测
通过jni直接调用malloc分配和释放的内存会体现在系统rss指标上
2.6.7. pmap观测
pmap -x `ps -ef|grep demo|grep -v grep|awk '{print $2}'` | sort -nrk3
- Linux将进程内存虚拟为伪文件/proc/$pid/mem,通过它即可查看进程内存中的数据。
- tail用于偏移到指定内存段的起始地址,即pmap的第一列,head用于读取指定大小,即pmap的第二列。
- strings用于找出内存中的字符串数据,less用于查看strings输出的字符串。如果内存中不是字符串,也可以不加strings原样输出
tail -c +$((0x00007face0000000+1)) /proc/`ps -ef|grep demo|grep -v grep|awk '{print $2}'`/mem|head -c $((11616*1024))|strings|less -S
通过jni分配的内存显示为anno
2.7. 综合实验
启动命令
java -XX:NativeMemoryTracking=detail -Xms256m -Xmx256m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=10M -Djava.library.path=$PROJECT_ROOT/src/main/native -jar demo-0.0.1-SNAPSHOT.jar
内存占用测试命令
curl localhost:8080/heap/128
curl localhost:8080/heap/64
curl localhost:8080/heap/32
curl localhost:8080/heap/16
curl localhost:8080/heap/8
curl localhost:8080/heap/4
curl localhost:8080/heap/2
curl localhost:8080/heap/1
curl localhost:8080/heap/1
curl localhost:8080/netty/128
curl localhost:8080/unsafe/128
curl localhost:8080/jni/128
2.7.1. jcmd详细统计报表
| 分类 | Reserved | Committed | 说明 |
|---|---|---|---|
| Java Heap | 256 | 256 | Java 堆内存,由JVM控制,通常通过 mmap 分配,这里堆内存几乎占满 |
| Class | 209 | 5 | 包括类元数据(Metadata)和类空间(Class space),用于加载类结构信息。 |
| ├─ Metadata | 64 | 28 | 类的结构定义,如字段、方法、注解信息等。 |
| └─ Class space | 208 | 4 | 用于存储类的静态信息等;Java 8+中取代永久代的一部分。 |
| Thread | 35 | 3 | 每个线程的本地栈空间,默认每线程约 1MB。 |
| Code | 243 | 12 | JIT编译后的代码缓存区,如 CodeCache。 |
| GC | 42 | 42 | 垃圾回收器的工作内存,如标记、扫描、复制等临时结构。 |
| Compiler | 0 | 0 | JIT 编译器本身使用的内存(如优化数据结构),当前无占用。 |
| Other | 256 | 256 | 非堆内内存,如DirectByteBuffer和Unsafe分配的本地内存。 |
| Symbol | 11 | 11 | 常量池中的符号表、方法名、字段名等字符串。 |
| NMT Tracking | 2 | 2 | Native Memory Tracking 自身运行时记录用的空间。 |
| Shared class space | 16 | 13 | 类数据共享(CDS)机制使用的共享空间。 |
| Arena Chunk | 0 | 0 | 本地内存 arena 分配器使用的临时分配区,当前为空。 |
| Metaspace | 64 | 28 | JVM 8+ 用于存储类定义元数据,支持动态扩展。 |
| 合计 | 1135 | 629 | malloc 总共占用 277MB,mmap 保留 858MB,提交 352MB(虚拟保留与实际提交) |

2.7.2. 总内存分析
RSS占用为797472 KB ≈ 779MB
JVM管理内存629 + JNI分配内存128 = 757MB,剩余22MB可能来自于Linux 分配的线程栈、page tables、管理结构等。
2.8. Bits类
Bits.reserveMemory和Bits.unreserveMemory会更新JVM内部的直接内存计数器,Arthas可以通过读取这些计数器来显示直接内存使用情况。
使用Unsafe方法直接分配和释放的内存,没有经过Bits管理,因此arthas无法观测到。
Bits.reserveMemory(size, cap);
Bits.unreserveMemory(size, cap);
2.9. DirectByteBuffer
非jvm管理的内存典型代表为DirectByteBuffer,jvm gc只能回收DirectByteBuffer对象本身,而无法管理其内部通过Unsafe申请的内存。当DirectByteBuffer对象被GC回收时,JVM会通过Cleaner机制调用本地方法。
具体来说,Cleaner类继承自PhantomReference,实现了clean方法。JVM启动时会创建一个名为Reference Handler的守护线程,其优先级为MAX_PRIORITY(10),该线程不断从ReferenceQueue中取出引用对象,并调用其clean方法。
public class Cleaner extends PhantomReference<Object>
[!note]
JVM垃圾回收没有直接回收DBB对象通过Unsafe方法分配的内存,而是通过其Cleaner机制实现了间接释放。内存的分配和释放都是由系统malloc()和free()函数实现的。
2.10. metaspace和class space
Metaspace区域位于堆外,最大内存大小取决于系统内存,而不是堆大小,可以通过指定 MaxMetaspaceSize参数来限制最大内存。
虽然每个Java类都关联了一个java.lang.Class的实例,而且它是一个贮存在堆中的 Java 对象。但是类的class metadata不是一个Java对象,它不在堆中,而是在 Metaspace 中。
有两个核心配置参数:
-XX:MaxMetaspaceSize:Metaspace 总空间的最大允许使用内存,默认是不限制。-XX:CompressedClassSpaceSize:Metaspace中的Compressed Class Space的最大允许内存,默认值是1G,这部分会在JVM启动的时候向操作系统申请1G的虚拟地址映射,但不是真的就用了操作系统的1G内存。
2.10.1. 分配
当一个类被加载时,它的类加载器会负责在Metaspace中分配空间用于存放这个类的元数据。如下图,可以看到在Id这个类加载器第一次加载类X 和 Y 的时候,在 Metaspace 中为它们开辟空间存放元信息。
2.10.2. 回收
分配给一个类的空间,是归属于这个类的类加载器的,只有当这个类加载器卸载的时候,这个空间才会被释放。所以,只有当这个类加载器加载的所有类都没有存活的对象,并且没有到达这些类和类加载器的引用时,相应的Metaspace空间才会被GC释放。

2.10.3. 系统内存回收
释放Metaspace的空间,并不意味着将这部分空间还给系统内存,这部分空间通常会被JVM保留下来。
这部分被保留的空间有多大,取决于Metaspace的碎片化程度。另外,Metaspace中有一部分区域Compressed Class Space是一定不会还给操作系统的。