Java堆外内存(直接内存):平常少关注,懂了很高大上
Java 的内存管理向来是一个热门话题。今日来聊一聊平时较少被关注的那部分内存,即堆外内存,也称作直接内存。

不懂不影响生活,懂了就很高级,有木有!!!
看下图:
1、堆外内存是个啥
堆外内存被称作直接内存。因为这部分内存属于机器的物理内存,所以很直接。
直接内存并非虚拟机运行时数据区的组成部分,也不属于 Java 虚拟机规范所定义的内存区域。
使用 native 函数库来直接分配堆外内存,接着通过一个存于 Java 堆中的 DirectByteBuffer 对象,将其作为这块内存的引用,以此来进行操作。
在一些场景中能显著提高性能,原因是避免了在 Java 堆与 Native 堆之间来回复制数据。
举个通俗一点的例子:
假如操作系统就是你所在小区,
小区的居民就是不同的jvm或者其他的进程(程序)。
某天你有装修的想法,装修材料放置在自家,也就是属于自己的空间内,你可以随意摆弄。放置的位置由你自己管理就行。将其对应到虚拟机上,就如同你常规所理解的内存,其中包含 heap(堆)、栈等。
装修材料放在家里不太方便,并且还需要运进来。因此,你想知道我是否可以将其放在公共区域,比如在楼下找一块空地来放置装修材料。这部分就是你需要申请使用的堆外内存,也就是机器的物理内存,需要使用 native 来申请空间。
2.堆外内存的优缺点
优点:
减少垃圾回收的工作,从理论上来说能够减小 GC 暂停时间。这是因为堆外内存的释放并不受虚拟机的管理,虚拟机只是释放句柄,而真正的内存是由操作系统来进行释放的。
省去了不必要的内存复制操作,从而实现了 zero copy 。这样一来,数据就无需在 native memory 和 jvm memory 之间来回进行复制了。
缺点️:
堆外内存不好控制,一旦发生内存泄漏就不容易排查。所以要谨慎使用。
堆外内存相对而言,不太适合用来存储极为复杂的对象。通常是用于放大块的内存,并且其格式需要自行定义。
3、堆外内存的控制参数
可以通过设置来控制堆外内存的大小,具体设置为 XX:MaxDirectMemorySize=10M。如果超过了此内存,就会报错。
堆外内存能够借助 java.nio 的 ByteBuffer 进行创建,只需调用 allocateDirect 方法进行申请便可。
这是合法的途径
4、几个常用的类4.1 unsafe
Unsafe类操作堆外内存
Sun.misc.Unsafe 具备一组方法,这些方法可用于堆外内存的分配操作,还可用于对堆外内存进行重新分配,同时也能用于释放堆外内存。
分配一块内存空间,通过 public native long allocateMemory(long size) 这个操作来实现。
重新分配一块内存,将数据从 address 所指向的缓存中拷贝至新的内存块。
公共本地方法用于释放内存,该方法接收一个长整型的地址作为参数,通过调用此方法可以释放对应的内存空间。
public class UnsafeTest {
公共静态的 void 主函数(字符串数组参数)抛出异常。
//最简单的使用方式是基于反射获取Unsafe实例
在 Unsafe 类中获取名为“theUnsafe”的声明字段,并将其赋值给字段 f 。
f.setAccessible(true);
Unsafe 赋值给 unsafe,unsafe 等于 (Unsafe)f.get(null);先获取 f 的值为 null,再将其强制转换为 Unsafe 类型并赋值给 unsafe 。
long address 是通过 unsafe.allocateMemory(1024)来分配内存的。
unsafe.freeMemory(address);
}
}
复制代码
注意:若直接使用 unsafe ,则会报错,原因是 Unsafe 不允许被使用,其使用会引发安全问题。
4.2 DirectByteBuffer
创建一个直接缓冲区,分配 1024 个字节的空间;使用 ByteBuffer 的 allocateDirect 方法来实现;该方法用于分配直接内存缓冲区;分配的缓冲区大小为 1024 字节。
JVM 在堆内仅保存堆外内存的引用,此引用由 DirectByteBuffer 对象来表示,它类似于指针的概念。
当 DirectByteBuffer 对象开始初始化之际,就会造就一个与之对应的 Cleaner 对象。
这个 Cleaner 对象在合适的时刻会执行 unsafe.freeMemory(address),以此来回收这块堆外内存。通常是在进行 full gc 的时候进行回收。
DirectByteBuffer 对象在某次 YGC 中被回收时,只有 Cleaner 对象知晓堆外内存的地址。
当下一次执行 FGC 时,Cleaner 对象会把自身从 Cleaner 链表上删除掉,并且会触发 clean 方法来清理堆外内存。
此时,堆外内存会被回收。并且,Cleaner 对象也将在下次 YGC 的时候被回收。
如果 JVM 始终没有执行 FGC,那么就无法促使 Cleaner 对象去执行 clean 方法,这样一来,堆外内存也始终无法得到释放。
5、内存泄漏的分析
当堆外内存占用量较大时,可尝试强制执行 Full Gc 以查看堆外内存是否会下降。这样做会调用 cleaner 来释放堆外内存。如果堆外内存有下降,那么很有可能是 DirectByteBuffer 导致的。
最好在启动参数方面增加 -XX:MaxDirectMemorySize=x[m|g]。
语言风格:保持原文风格,去掉最前面的序号,把长句拆分成多个小分句,分句越短越好。例如:MaxDirectMemorySize 为 500m 。
堆外内存最大可分配 500M,一旦超过这个范围就会出现 OOM。如果不进行设置,那么默认的堆外内存大小就是机器的内存。
在 sun.misc.VM.java 中,是怎样设置默认的 directmemorysize 的呢?它是通过特定的方式和机制来进行设置的,具体的设置过程包含了一系列的操作和参数调整等。
直接内存的最大值通过特定的方法来获取。
if (booted)
return directMemory;
System 获取了属性 p,这个 p 就是 System.getProperties() 所返回的内容。
s 等于移除后的结果 。
System.setProperties(p);
if (s != null) {
if (s.equals("-1")) {
-XX:MaxDirectMemorySize 未被指定,采用默认值。
如果不设置 XX:MaxDirectMemorySize ,那么它就会取默认值,而这个默认值就是最大内存。
Runtime 运行时对象获取到系统的最大内存值,然后将其赋值给 directMemory 变量。
} else {
long l = Long.parseLong(s);
if (l > -1)
directMemory = l;
}
}
return directMemory;
}
复制代码
6、应用场景
使用堆外内存有两大场景
一些与 nio 有关的 API 需要使用堆外内存。操作系统内核能够直接将数据写入堆外内存,不像普通 API 那样,操作系统内核需要缓存一份,程序读取时再复制一份到程序空间。这样就减少了复制的过程,从而提高了性能,Netty 就是采用这种方式进行操作的。
在专门为业务进行调优的时候,会在业务管理的范围之外开辟出一块内存,接着由自己来对其进行管理。
比如一个连接来了,要给 1000 个小对象申请 1M 。连接断开后,会直接批量释放这 1M 内存。因为写代码的人最了解内存中的情况,所以他们可以自己写代码,用最高效的办法来使用内存。(如果是 GC 的话,那可是管理 1000 个对象的负担,GC 不知道这些对象用完后可以直接批量释放)
7、其他知识
使用堆外内存的两种途径,一种是上面的方式
有一种是通过 JNI 来编写 Java 的 C/C++扩展。JNI 负责自身的内存管理,与 JVM 没有关联,只需直接使用即可。
总结:堆外内存可直接用于对物理内存的使用。这部分操作能够直接被理解为指针的操作。使用堆外内存使得 jvm 的边界得以扩展。
缺点在于脱离了 jvm 的管理,这对于那些对内存管理不太熟悉的同学而言,可能会存在风险。
这是一把双刃剑,用的好则无敌,用不好则万劫不复,
在使用之前一定要了解,不然只会埋下深坑。