堆外内存是jvm堆外由os分配、不受gc管理的内存,易导致oom和泄漏;directbytebuffer有三重风险:默认上限同-xmx、cleaner回收不可控、易忘手动clean;unsafe.allocatememory更危险,需严格配对free且防重复释放。

堆外内存就是JVM堆之外、由操作系统直接分配的内存,不受GC管理,用错会OOM,不手动释放必泄漏。
DirectByteBuffer 是最常用也最容易出事的堆外内存入口
它表面简单:ByteBuffer.allocateDirect(1024) 一行就搞定,但背后藏着三重风险:一是默认最大堆外内存等于 -Xmx(比如你设了 -Xmx2g,-XX:MaxDirectMemorySize 就也是 2G);二是它的 Cleaner 回收时机不可控——对象被 GC 掉后,可能很久才真正释放堆外内存;三是很多人忘了调用 clean(),尤其在缓存场景反复创建 DirectByteBuffer 时,内存只涨不跌。
实操建议:
- 必须显式设置
-XX:MaxDirectMemorySize=512m(按需调整),别依赖默认值 - 高频短生命周期使用(如 Netty 每次请求分配 buffer),优先用
PooledByteBufAllocator而非裸allocateDirect - 若必须手动控制,释放写法要完整:
((sun.nio.ch.DirectBuffer) buf).cleaner().clean();
注意判空,且仅对isDirect() == true的 buffer 有效
Unsafe.allocateMemory 是“裸金属”操作,性能高但危险系数拉满
它绕过所有 Java 层封装,直连 mmap 或 malloc,适合底层框架(如 Chronicle Map、Spark shuffle 内存池),但绝不是业务代码该碰的东西。典型错误是只 allocateMemory 不 freeMemory,或重复 free 同一地址——后者会触发 SIGSEGV,JVM 直接崩溃,连 try-catch 都捕获不到。
立即学习“Java免费学习笔记(深入)”;
实操建议:
- 获取
Unsafe必须用反射,且 JDK 9+ 默认禁止,需加启动参数:--add-opens java.base/jdk.internal.misc=ALL-UNNAMED - 每次
allocateMemory后务必记录地址和 size,释放前校验地址有效性(可用unsafe.addressSize()辅助判断) - 多线程访问同一块堆外内存时,
putInt/getInt等操作不保证原子性,需自行加锁或用Unsafe.compareAndSwapInt
监控堆外内存不能只看 JVM 工具,得盯住操作系统层面
JVisualVM 插件 Buffer Pools 只能看 DirectByteBuffer 分配量,对 Unsafe 或 JNI 分配的内存完全无感;jstat -gc 更是彻底失明。真实泄漏往往表现为:Java 进程 RSS 内存持续上涨,但堆内存(used)平稳,top 里 RES 值远超 -Xmx + MaxDirectMemorySize 总和。
实操建议:
- Linux 下用
pmap -x <pid></pid>查看进程各内存段实际占用,重点关注[anon]区域增长 - 通过 JMX 获取精确 Direct 内存用量:
sun.misc.SharedSecrets.getJavaNioAccess().getDirectBufferPool().getMemoryUsed() - 上线前压测时,用
jemalloc替换系统 malloc,并开启MALLOC_CONF=prof:true,prof_prefix:jeprof.out,可定位Unsafe泄漏源头
堆外内存不是银弹,它是把双刃剑:用好了能扛住百万级连接、处理 TB 级日志;用歪了,一个没 free 的 Unsafe 地址,就能让服务半夜 OOM;而最隐蔽的坑,是以为 DirectByteBuffer “自动清理”就万事大吉——其实它只承诺“最终会清”,不承诺“什么时候清”。








