关于直接内存(DirectMemory)

0 阅读4分钟

前言

关于直接内存、零拷贝等技术和原理,之前有认真了解过,但是没有直接尝试使用并测试过。

直到最近项目中真实遇到一个jvm直接崩溃的问题,所以才有了本篇的记录,也借此机会认真测试下直接内存是否可以带来性能的提升。

DirectMemory

首先回顾下DirectMemory,DirectMemory是JVM提供的一种内存分配方式,DirectMemory和普通内存的区别是:DirectMemory是直接分配在操作系统内存中的,而普通内存是分配在JVM内存中的,DirectMemory不属于java虚拟机运行时数据区的一部分。

创建DirectMemory的方式

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

可以参照之前的一篇文章:Buffer/ByteBuffer/ByteBuf详解

零拷贝

DirectMemory的优势是主要体现在IO的场景,比如:数据落盘、网络传输等等。

因为IO(磁盘 / 网络)底层内核只能直接操作堆外物理内存,所以使用普通jvm内存进行IO操作,会存在数据拷贝到堆外的问题,从而造成性能消耗。

零拷贝

上图中HeapByteBuffer也是我们最常用的方式指向对中内存byte[]的地址,当读取IO数据时先把数据拷贝到直接内存,再拷贝到jvm内存中,两次拷贝

而DirectByteBuffer直接指向直接内存,省去了一步拷贝工作,这种技术也叫零拷贝,读取数据更快

测试

为了验证DirectMemory的优势,我写了一个简单的测试程序,使用FileChannel读取Buffer中的内容进行文件写入IO操作,对比普通内存和DirectMemory的读取性能:

/**
 * 测试IO性能
 *
 * @param direct 是否使用DirectMemory
 * @param size   测试数据大小
 * @return 耗时(μs)
 * @throws IOException
 */
public static int testIO(boolean direct, int size) throws IOException {
    ByteBuffer buf;
    if (direct) {
        buf = ByteBuffer.allocateDirect(size);
    } else {
        buf = ByteBuffer.allocate(size);
    }

    Path p = Files.createTempFile(direct?"dm-":"m-", ".dat");

    try {
        long t1, t2;

        // write
        try (FileChannel ch = FileChannel.open(p,
                StandardOpenOption.CREATE,
                StandardOpenOption.TRUNCATE_EXISTING,
                StandardOpenOption.WRITE)) {

            for (int i = 0; i < buf.capacity(); i++) {
                buf.put((byte) i);
            }
            //  ready for writing
            buf.flip();
            t1 = System.nanoTime();
            while (buf.hasRemaining()) {
                ch.write(buf);
            }
            t2 = System.nanoTime();
            // force write to disk
            ch.force(true);
        }
        buf.clear();
        return (int) ((t2 - t1) / 1_000);
    } finally {
        try {
            Files.deleteIfExists(p);
        } catch (IOException ignored) {
        }
    }
}

以上方法,通过direct参数传入是否使用DirectMemory,size参数传入测试数据大小,返回耗时(μs),开始测试:

public static void main(String[] args) throws Exception {
    int size = 1024 * 1024 * 4;
    System.out.println("direct memory IO time(μs): " + testIO(true, size));
    System.out.println("memory IO time(μs): " + testIO(false, size));
}

输出如下:

// 1
direct memory IO time(μs): 2217
memory IO time(μs): 6651
// 2
memory IO time(μs): 6803
direct memory IO time(μs): 2316
// 3
direct memory IO time(μs): 1713
memory IO time(μs): 2853
// 4
direct memory IO time(μs): 1983
memory IO time(μs): 3599
...

测试进行了多次,效果还是比较明显的,direct memory IO性能要高于普通内存IO。

溢出

我尝试下试下DirectMemory溢出的情况,结果返现正常使用ByteBuffer.allocateDirect方法即使溢出也会有正常的错误打印,属于业务级别的日志,不会导致jvm内部崩溃。

所以尝试了下Unsafe方法去创建DirectMemory,结果发现会直接导致jvm崩溃,代码如下:

public class DirectBufferCrash {
    public static void main(String[] args) throws Exception {
        // 获取Unsafe
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        // 分配一块堆外内存
        long addr = unsafe.allocateMemory(1024);
        // 关键:越界写内存,直接篡改非法地址
        // 往分配地址后面偏移超大位置写数据,内存越界
        unsafe.putByte(addr + 9999999, (byte) 123);
    }
}

最终导致jvm崩溃,没有业务级别的错误日志打印(也没有控制台的输出),但这种jvm本身崩溃会dump出一个hs_err_pidxxxx.log文件出来,可以帮助排查问题。

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x000000006bbc790e, pid=23280, tid=0x0000000000002a18
#
# JRE version: Java(TM) SE Runtime Environment (8.0_191-b12) (build 1.8.0_191-b12)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.191-b12 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# V  [jvm.dll+0x1e790e]
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# D:\projects\pure-java\hs_err_pid23280.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp
#

除此之外还有java本地JNI方法调用,也是导致jvm崩溃的常见场景,此时没有任何错误日志,但jvm会dump出一个hs_err_pidxxxx.log文件出来,可以帮助排查问题。