java 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括 RAM 及 SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx 等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。

NIO 的 Buffer 提供一个可以直接访问系统物理内存的类——DirectBuffer。DirectBuffer 类继承自 ByteBuffer,但和普通的 ByteBuffer 不同。普通的 ByteBuffer 仍在 JVM 堆上分配内存,其最大内存受到最大堆内存的 限制。而 DirectBuffer 直接分配在物理内存中,并不占用堆空间。在访问普通的 ByteBuffer 时,系统总是会使用一个“内核缓冲区”进行操作。而 DirectBuffer 所处的位置,就相当于这个“内核缓冲区”。因此,使用 DirectBuffer 是一种更加接近内存底层的方法,所以它的速度比普通的 ByteBuffer 更快。
申请 DirectBuffer 代码如下:

ByteBuffer.allocateDirect();

下面,使用 DirectBuffer 和普通的 ByteBuffer 进行一段性能测试。

  • 使用 DirectBuffer:
long start = System.currentTimeMillis();
        ByteBuffer buffer = ByteBuffer.allocateDirect(500);//分配500个字节的DirectBuffer
        for (int i = 0; i < 100000; i ++) {
            for (int j = 0; j < 99; j ++) {
                buffer.putInt(j);           //向DirectBuffer写入数据
            }
            buffer.flip();
            for (int j = 0; j < 99; j ++) {
                buffer.get();                   //从DirectBuffer中读取数据
            }
            buffer.clear();
        }
        System.out.println("DirectBuffer use : " + ( System.currentTimeMillis() - start ) + "ms");

执行结果:

DirectBuffer use : 20ms
  • 使用 ByteBuffer:
long start = System.currentTimeMillis();
        ByteBuffer buffer = ByteBuffer.allocate(500);//分配500个字节的ByteBuffer
        for (int i = 0; i < 100000; i ++) {
            for (int j = 0; j < 99; j ++) {
                buffer.putInt(j);           //向DirectBuffer写入数据
            }
            buffer.flip();
            for (int j = 0; j < 99; j ++) {
                buffer.get();                   //从DirectBuffer中读取数据
            }
            buffer.clear();
        }
        System.out.println("ByteBuffer use : " + ( System.currentTimeMillis() - start ) + "ms");

执行结果:

ByteBuffer use : 33ms

以后两段测试代码分别使用了 DirectBuffer 和堆上的 ByteBuffer,并进行了大量的读写访问。测试结果是 DirectBuffer 相对耗时 20ms, 而 ByteBuffer 相应耗时 33ms.虽然都很快,但从比例上来说,DirectBuffer 接近快了一倍。如果把外层的循环次数由 10 万改为 100 万,DirectBuffer use : 105ms,而 ByteBuffer use : 225ms,快了一倍多。

不过,虽然有访问速度上的优势,但是在创建和销毁 DirectBuffer 的花费却远比 ByteBuffer 高。

for (int i = 0 ;i < 20000; i ++) {
            ByteBuffer b = ByteBuffer.allocateDirect(1000);
        }
        for (int i = 0 ;i < 20000; i ++) {
            ByteBuffer b = ByteBuffer.allocate(1000);
}

上面的两个 for 循环表示分别请求每种类型的 Buffer 20M, 设置运行时参数-XX:MaxDirectMemorySize=10M -Xmx10M, 运行以下代码,使用 DirectBuffer 的代码段相对耗时 297ms,而使用 ByteBuffer 的相对耗时仅 15ms。因此可知,频繁的创建和销毁 DirectBuffer 远远大于在堆上分配内存空间。

DirectBuffer 的读写操作比普通 Buffer 快,但它的创建、销毁却比普通 Buffer 慢。