Java堆外内存申请 java 堆外内存_jvm 堆外内存

去年我面试了一些程序员,等级从低到高都有。但是没有一个人能够说明白什么是堆外内存!

今天我们就一起来简单的说一说 Java 中的堆外内存。这一块可能 90% 的 Java 程序员都不清楚,希望你看过本文后,成为那 10% 中的大神级别的潜力股。

堆外内存是相对于堆内内存的一个概念。堆内内存是由 JVM 所管控的 Java 进程内存,我们平时在 Java 中创建的对象都处于堆内内存中,并且它们遵循 JVM 的内存管理机制,JVM 会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于 JVM 管控之外的一块内存区域,因此它是不受 JVM 的管控。

现实中有非常多的框架可能就使用了堆外内存,而且这些框架很可能你都使用过,但是你可能并不了解它们是否有使用过堆外内存。

比如:Ehcache 中的一些版本,各种 NIO 框架,Dubbo,Memcache 等。

在 Java 中有几个非常重要的类,比如:DirectByteBuffer、ByteBuffer、HeapByteBuffer等。使用它们我们可以操作堆外内存。比如下面的代码:

Java堆外内存申请 java 堆外内存_Java堆外内存申请_02

通过 ByteBuffer 使用堆外内存,将堆外内存设置为 40M。DirectByteBuffer 关联的几个类的类图如下:



Java堆外内存申请 java 堆外内存_Java堆外内存申请_03

DirectByteBuffer 该类本身还是位于 Java 内存模型的堆中。堆内内存是 JVM 可以直接管控、操纵。而 DirectByteBuffer 中的 unsafe.allocateMemory(size); 是一个 native 方法,这个方法分配的是堆外内存,通过 C 的 malloc 来进行分配的。分配的内存是系统本地的内存,并不在 Java 的内存中,也不属于 JVM 管控范围。那么既然通过 DirectByteBuffer 分配的堆外内存不受 JVM 管控,那么这些内存该如何回收呢?

这和我公众号上一篇推文《手把手教你通过Java代码体验强引用、软引用、弱引用、虚引用的区别》的内容就有些相关性了。因为 DirectByteBuffer 是通过虚引用(Phantom Reference)来实现堆外内存的释放的。

PhantomReference 是所有“弱引用”中最弱的引用类型。不同于软引用和弱引用,虚引用无法通过 get() 方法来取得目标对象的强引用从而使用目标对象,观察源码可以发现 get() 被重写为永远返回 null。

那虚引用到底有什么作用?其实虚引用主要被用来跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否即将被垃圾回收,从而采取行动。它并不被期待用来取得目标对象的引用,而目标对象被回收前,它的引用会被放入一个 ReferenceQueue 对象中,从而达到跟踪对象垃圾回收的作用。

为什么很多高性能的框架会使用堆外内存呢?很重要的一个原因就是它不受 JVM 管控,不受 GC 影响。因此堆外内存的优缺点可以总结如下。

堆外内存,其实就是不受JVM控制的内存。相比于堆内内存有几个优势:

  • 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作(可能使用多线程或者时间片的方式,你根本感觉不到)。
  • 加快了复制的速度。因为堆内在 flush 到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。
  • 可以在进程间共享,减少 JVM 间的对象复制,使得 JVM 的分割部署更容易实现。
  • 可以扩展至更大的内存空间。比如超过 1TB 甚至比主存还大的空间。

有优点就有缺点,缺点总结如下:

  • 堆外内存难以控制,如果内存泄漏,那么很难排查。
  • 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。

站在系统设计的角度来看,使用堆外内存可以为你的设计提供更多可能。最重要的提升并不在于性能,而是决定性的。