前言
关于直接内存、零拷贝等技术和原理,之前有认真了解过,但是没有直接尝试使用并测试过。
直到最近项目中真实遇到一个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文件出来,可以帮助排查问题。