直接内存设计逻辑
在我看周志明的《深入理解 Java 虚拟机 第三版》2.2.7 小节,里面关于 Java 直接内存的描述如下。
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置 -Xmx 等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。
- 我们对上面书中的话进行提炼
- 直接内存并不是JVM中的一块内存区域。
- 由于在 JDK 1.4 中引入了 NIO 机制,为此实现了一种通过 native 函数直接分配对外内存的,而这一切是通过以下两个概念实现的:
通道(Channel);缓冲区(Buffer); - 通过存储在 Java 堆里面的 DirectByteBuffer 对象对这块内存的引用进行操作;
- 因避免了 Java 堆和 Native 堆(native heap)中来回复制数据,所以在一些场景中显著提高了性能;
- 直接内存出现 OutOfMemoryError 异常的原因是物理机器的内存是受限的,但是我们通常会忘记需要为直接内存在物理机中预留相关内存空间;
直接内存所处的地位
- 我试图通过下面两张图来讲清楚直接内存:
- 图一,我们知道我们Java代码时运行于JVM之上的,当我们使用普通的IO流,想要读取电脑中磁盘的数据时,我们先要将磁盘中的数据拷贝到系统缓冲区,而我们知道JVM是我们Java的操作系统,Java拿不能从系统缓冲区直接拿取,JVM中堆时存放对象的实例,我们需要讲系统缓冲区中的数据拷贝到堆内存中才能被我们Java代码使用。
上面讲解我们知道,普通IO流我们拿磁盘中的数据我们需要经过两次拷贝拿取,这是非常耗时的,于是我们想要尝试改进它,于是才有了第二张图的改进方法。
- 图二,这个时候我们搞出来了一个直接内存,这样我们Java代码可以从直接内存拿数据,我们磁盘也可以直接往直接内存中放数据,这样就节省了很多时间。
我们注意直接内存,不属于JVM,它是我们操作系统开辟的一片区域。我们只不过通过堆中的DirectByteBuffer引用来引用操作这片区域,当然直接内存不属于JVM,当然也不属于堆,JVM堆垃圾回收就管不着它。
下面两张图也是我在网上找的,我觉觉得可以很好说明直接内存和和普通JVM读取区别。
我们是如何使用直接内存的(NIO中怎样使用直接内存)
- 为了讲清楚后面比较重要的直接内存的垃圾回收相关的知识。我这里还要在唠叨一下,JVM里面到底什么引用什么?
我们把周志明老师书中一句话先放在这:
在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。
在NIO中我们通常写代码创建ByteBuffer对象这个对象的引用因为是局部变量她在栈中,栈当然是引用了堆中的实例数据,那我们堆中的实例数据,并不是说直接就给个直接内存的放到那里,当然是通过存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这个DirectByteBuffer 对象才引用的是直接内存,只不过我们栈中引用想要用直接内存,这时堆中的DirectByteBuffer 对象将直接内存的引用赋给我们Buffer。通过下面图,希望大家可以更好的理解。
直接内存分配和回收
这里我们注意在堆中我们维护了一个DirectByteBuffer 对象,这个对象因为是在JVM堆中那么它当然可以被堆垃圾回收器回收。这里我们应该就会产生一个疑问?那我们这里回收了DirectByteBuffer 对象,那么我们直接内存中的数据怎么办?我们前面不是说我们堆垃圾回收器回收堆里垃圾,并不能回收直接内存,那这样不是会造成内存泄漏?
这里我们通过JDK源码来一步一步分析。
- 当我们想要通过NIO中Buffer来引用直接内存时,我们这样写:
public static void main( String[] args )
{
ByteBuffer byteBuffer=ByteBuffer.allocateDirect(1024);
}
这里我们看到我们调用ByteBuffer的allocateDirect(1024)方法。我们来看看这个方法的源码:
-
ByteBuffer.allocateDirect方法
我们看到它创建了DirectByteBuffer,调用了它的构造方法,和我们前面讲的一样ByteBuffer引用堆中的DirectByteBuffer实例对象。 -
DirectByteBuffer构造函数
这里我们看到底层我们是通过Unsafe类分配的直接内存空间。 -
这里我们看看Cleaner,我们直接内存垃圾回收和它有很大关系啦。
从Cleaner类继承关系我们能看出Cleaner是一个虚引用对象,
我们看刚才那段代码
Cleaner.create(this, new Deallocator(base, size, cap));
虚引用有一个特点,当我们被虚引用关联的对象被垃圾回收了,那么它就会调用clean方法。我们看clean方法,它调用一个启动了一个线程的run。
这个线程就是会调用我们任务对象的run方法,而我们任务对象就是后面new那个对象Deallocator
- Deallocator
这里转了一圈我们终于知道最后是怎样释放直接内存的中的数据,用的Unsafe类的freeMemory。所以我们在堆中的DirectByteBuffer 对象被垃圾回收了,那么就会导致关联的虚引用类型调用Unsafe类的freeMemory来释放直接内存空间。所以走了一圈,还是得主动调用Unsafe类的freeMemory。
- 在我们代码中怎样来回收直接内存?
通过前面原理我们知道,如果我们不去管堆中的DirectByteBuffer 对象,那么在我们垃圾回收DirectByteBuffer 对象后,就会调用Unsafe类的freeMemory来回收直接内存。但是如果我们想要主动去回收直接内存,那么我们就可以通过反射来得到Unsafe对象,来回收直接内存。而不用我们去回收DirectByteBuffer 对象,因为使用垃圾回收非常的损耗性能。