直接内存(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慢。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐