90%的程序员可能都不了解的堆外内存

/ Java / 没有评论 / 1457浏览

90%的程序员可能都不了解的堆外内存

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

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

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

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

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

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

/**
 * @VM args:-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails
 * -XX:+DisableExplicitGC //增加此参数一会儿就会内存溢出
 * java.lang.OutOfMemoryError: Direct buffer memory
 */
public static void TestDirectByteBuffer() {
    List<ByteBuffer> list = new ArrayList<ByteBuffer>();
    while(true) {
        ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);
        //list.add(buffer);
    }
}

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

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控制的内存。相比于堆内内存有几个优势:

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

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

关于堆外内存这一块,你在面试 Dubbo 或者 Netty 等框架的时候,可能就会遇到。深入源码却理解它们,会有不一样的发现。