java1.4引入了nio.DirectByteBuffer,可能大部分同学在工作中跟直接内存打交道的并不多,本篇将结合网上资料查阅和自己的理解来对java直接内存和非直接内存做一个细致的整理。如有不合理之处,欢迎评论区留言。
Netty服务端接收到消息体存储在ByteBuf中,接下来我们通过Debug的方式来跟踪一下ByteBuff的内存分配方式。
客户端发送的消息体在服务端AbstractNioByteChannel.read()中接收处理,在此处断点跟踪:
image.png
留意到PooledByteBufAllocator的属性:directByDefault为true;
进入byteBuf = allocHandle.allocate(allocator);
image.png
image.png
image.png
预执行表达式PlatformDependent.hasUnsafe()返回true,进入directBuffer内存申请;
继续往下看:
image.png
此处先做个最大申请内存的校验,最终我们跟踪到申请直接内存的相关代码:
image.png
从方法说明可以看到:直接内存申请后必须调用freeDirectNoCleaner来手动释放内存
PlatformDependent0中相关代码逻辑如下:
//调用sun.misc.Unsafe分配直接内存
static ByteBuffer allocateDirectNoCleaner(int capacity) {
return newDirectBuffer(UNSAFE.allocateMemory(capacity), capacity);
}
//根据申请的内存地址和内存大小创建DerectByteBuff对象
static ByteBuffer newDirectBuffer(long address, int capacity) {
ObjectUtil.checkPositiveOrZero(capacity, "capacity");
try {
return (ByteBuffer) DIRECT_BUFFER_CONSTRUCTOR.newInstance(address, capacity);
} catch (Throwable cause) {
// Not expected to ever throw!
if (cause instanceof Error) {
throw (Error) cause;
}
throw new Error(cause);
}
}
关于直接内存的分配我们用到了sum.misc.Unsafe,类中包含了大量的native方法。研究过jdk源码的同学会发现该类在很多jdk类中都有用到,比如AQS用到的Unsafe.park/Unsafe.unpark、原子操作compareAndSwapXXX、以及此处的allocateMemory/freeMemory等等
通过以上的代码跟踪我们看到Netty关于消息体的存储使用了直接内存的分配方式,这样做的优势在哪呢?
我们来做一个性能测试:
public class MemoryTest {
public static void heapAccess() {
long startTime = System.currentTimeMillis();
ByteBuffer buffer = ByteBuffer.allocate(1000);
for (int i = 0; i < 100000; i++) {
for (int j = 0; j < 200; j++) {
buffer.putInt(j);
}
buffer.flip();
for (int j = 0; j < 200; j++) {
buffer.getInt();
}
buffer.clear();
}
long endTime = System.currentTimeMillis();
System.out.println("堆内存访问:" + (endTime - startTime));
}
public static void directAccess() {
long startTime = System.currentTimeMillis();
ByteBuffer buffer = ByteBuffer.allocateDirect(1000);
for (int i = 0; i < 100000; i++) {
for (int j = 0; j < 200; j++) {
buffer.putInt(j);
}
buffer.flip();
for (int j = 0; j < 200; j++) {
buffer.getInt();
}
buffer.clear();
}
long endTime = System.currentTimeMillis();
System.out.println("直接内存访问:" + (endTime - startTime));
}
public static void heapAllocate() {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
ByteBuffer.allocate(100);
}
long endTime = System.currentTimeMillis();
System.out.println("堆内存申请:" + (endTime - startTime));
}
public static void directAllocate() {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
ByteBuffer.allocateDirect(100);
}
long endTime = System.currentTimeMillis();
System.out.println("直接内存申请:" + (endTime - startTime));
}
public static void main(String args[]) {
for (int i = 0; i < 10; i ++) {
heapAccess();
directAccess();
}
System.out.println();
for (int i = 0; i < 10; i ++) {
heapAllocate();
directAllocate();
}
}
}
输出结果:
堆内存访问:62
直接内存访问:48
堆内存访问:39
直接内存访问:26
堆内存访问:61
直接内存访问:46
堆内存访问:98
直接内存访问:76
堆内存访问:59
直接内存访问:77
堆内存访问:47
直接内存访问:27
堆内存访问:40
直接内存访问:21
堆内存访问:55
直接内存访问:21
堆内存访问:50
直接内存访问:30
堆内存访问:50
直接内存访问:25
堆内存申请:12
直接内存申请:29
堆内存申请:40
直接内存申请:31
堆内存申请:21
直接内存申请:28
堆内存申请:1
直接内存申请:22
堆内存申请:6
直接内存申请:73
堆内存申请:1
直接内存申请:17
堆内存申请:1
直接内存申请:18
堆内存申请:1
直接内存申请:373
堆内存申请:2
直接内存申请:38
堆内存申请:3
直接内存申请:35
可以看出直接内存申请较慢,但访问效率高。
为什么呢???
回顾一下java内存模型:
image.png
我加上内核空间和用户空间的概念
我的理解是堆内存的申请是直接从已分配的堆空间中取一块出来使用,不经过内存申请系统调用,而直接内存的申请则需要本地方法通过系统调用完成。
笔者尝试用strace命令去跟综了下ByteBuffer.allocateDirect和ByteBuffer.allocate涉及的系统调用,然而并没有发现不一样的地方,感兴趣的朋友欢迎评论区交流。
而关于直接内存和非直接内存的访问效率,我们看看ByteBuffer上的一段官方说明:
image.png
在java虚拟机实现上,本地IO会直接操作直接内存(直接内存=>缺页中断=>硬盘),而非直接内存则需要二次拷贝(堆内存=>缺页中断=>内核空间=>硬盘)。
网友评论