我准备战斗到最后,不是因为我勇敢,是我想见证一切。 --双雪涛《猎人》
[TOC]
Thinking
- 一个技术,为什么要用它,解决了那些问题?
- 如果不用会怎么样,有没有其它的解决方法?
- 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
- 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
- 这些问题你又如何去解决的呢?
本文基于Netty 4.1.45.Final-SNAPSHOT
1、NIO堆外内存与零拷贝
NIO堆外内存
在上述NIO Buffer 讲解中,我们隐约的提到过为什么要使用Direct Buffer
小节中提到过直接内存(堆外内存)与堆内存(Non - Direct Buffer)的区别:
这里会涉及到 Java 的内存模型
Direct Buffer:
- 所分配的内存不在 JVM 堆上, 不受 GC 的管理.(但是 Direct Buffer 的 Java 对象是由 GC 管理的(会将内存地址映射到一个标记上), 因此当发生 GC, 对象被回收时, Direct Buffer 也会被释放)
- 因为 Direct Buffer 不在 JVM 堆上分配, 因此 Direct Buffer 对应用程序的内存占用的影响就不那么明显(实际上还是占用了这么多内存, 但是 JVM 不好统计到非 JVM 管理的内存.)
- 申请和释放 Direct Buffer 的开销比较大. 因此正确的使用 Direct Buffer 的方式是在初始化时申请一个 Buffer, 然后不断复用此 buffer, 在程序结束后才释放此 buffer.
- 使用 Direct Buffer 时, 当进行一些底层的系统 IO 操作时, 效率会比较高, 因为此时 JVM 不需要拷贝 buffer 中的内存到中间临时缓冲区中.
Non-Direct Buffer:
- 直接在 JVM 堆上进行内存的分配, 本质上是 byte[] 数组的封装.
- 因为 Non-Direct Buffer 在 JVM 堆中, 因此当进行操作系统底层 IO 操作中时, 会将此 buffer 的内存复制到中间临时缓冲区中. 因此 Non-Direct Buffer 的效率就较低.
总结对比:
- 之所以使用堆外内存,是为了避免每次使用buffe如对象时,都会将此对象复制到中间林是缓冲区中,因此Non-Direct Buffer效率会非常低下。
- 堆外内存(直接内存--direct byte buffer)则可以直接使用,避免了对象的复制,提高了效率。
基于上述总结,我们先看一下下面创建Buffer 的两种方法的代码:
@Test
public void test01() throws Exception {
FileInputStream in = new FileInputStream("src/main/resources/data/DirectorBuffer.txt");
FileOutputStream out = new FileOutputStream("src/main/resources/data/DirectorBuffer-out.txt");
// 获取文件Channel
FileChannel inChannel = in.getChannel();
FileChannel outChannel = out.getChannel();
// 普通获取Buffer
ByteBuffer allocate = ByteBuffer.allocate(1024);
// 获取 堆外内存 Buffer
ByteBuffer allocateDirect = ByteBuffer.allocateDirect(1024);
// 从源码 分析两种的区别。
int count = inChannel.read(allocate);
while (count != -1) {
log.info("read :{}", count);
allocate.flip();
outChannel.write(allocate);
allocate.clear();
// 防止死循环
count = inChannel.read(allocate);
}
inChannel.close();
outChannel.close();
}
}
ByteBuffer.allocate(1024);
跟随进入源码:public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity); } HeapByteBuffer(int cap, int lim) { // package-private super(-1, 0, lim, cap, new byte[cap], 0); /* hb = new byte[cap]; offset = 0; */ }
-
该方法是直接new HeapByteBuffer 对象,在堆内存中直接申请字节数组内存空间用于存储数据。
- 直接在 JVM 堆上进行内存的分配, 本质上是 byte[] 数组的封装.
- 但是在每次使用时,都会设计到copy操作,性能会低下。
ByteBuffer.allocateDirect(1024)
创建堆外内存。// Allocates a new direct byte buffer. 分配一个新的直接字节缓冲区 public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); } DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); // 《1》 int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = unsafe.allocateMemory(size); // 《2》 } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; // 《3》 } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
-
从源码中看出,其实都是用的
NEW
关键字,宏观角度上两种方式创建的对象都是在堆内存中的。但是new DirectByteBuffer(capacity)
则是基于堆外内存(直接内存 Direct)。在上述源码中导入的包设计到import sun.misc.Cleaner; import sun.misc.Unsafe; import sun.misc.VM;
从这个角度也可以看出,这些以sun开头的类(JDK中为本地方法,非开源的。)
《1》
处,VM.isDirectMemoryPageAligned()
本地方法的调用。《2》处:调用Unsafe方法用于分配内存。
unsafe.setMemory(base, size, (byte) 0)
设置内存。(这些方法都是native 本地方法。)-
《3》处:将分配到的内存地址 映射到该标记。(该标记为底层父类Buffer 中维护的一个成员变量 long address --->因为在堆外内存中生成的数据,必须有个映射地址,不然JVM 并不能找到该对象,因为堆外内存并不受JVM管理。)
// Used only by direct buffers 只适用于直接缓冲区 // NOTE: hoisted here for speed in JNI GetDirectBufferAddress -> static native long getDirectBufferAddress(Buffer var0); // 为了提高速度,将其悬挂在JNI GetDirectBufferAddress中 long address;
图解Direct Memory/Non Direct Memory
- 上图所示:提到两个问题
- JVM管理内的堆内存中的对象具体是怎么进行I/O操作的。
- 为何要引入这种机制,使用堆外内存呢?
- 那么在ByteBuffer创建的堆外内存对象是否被JVM管理呢?GC是否会回收该类对象呢?
问题
JVM管理内的堆内存中的对象具体是怎么进行I/O操作的。
当我们使用创建对象时,大多是new出来的对象都是存放在堆内存中的,受jvm管理。受GC的管理。
当对内存中的对象进行I/O操作时,JVM会将堆内中的对象数据完整的copy一份到堆外内存(物理内存)中,再由该物理内存中的对象进行具体的I/O操作。
这样一来,在堆内的对象或者数据需要进行I/O操作时,都需要进行一步copy操作。(这里就引入了 NIO中的领copy操作了。后续详解。)
为何要引入这种机制,使用堆外内存呢?
就是为了性能。
- 使用堆外内存,减少了垃圾回收机制(GC会暂停其他的工作)
- 加快了I/O操作的进度
- 堆内在flush到远程时,会先复制到直接内存中,然后在发送。
- 而堆外内存(本身就是物理机内存)几乎省略了这步。
那么在ByteBuffer创建的堆外内存对象是否被JVM管理呢?GC是否会回收该类对象呢?
使用ByteBuffer创建的直接缓冲对象实际上是受JVM管理的。其他使用Unsafe创建的堆外内存对象则完全由自己控制。
ByteBuffer allocateDirect = ByteBuffer.allocateDirect(1024);
当这段代码执行会在堆外内存中占用
1k
的内存,Java堆内只会占用一个对象的指针引用大小。(顶层父类中维护的成员变量 address)// Used only by direct buffers // NOTE: hoisted here for speed in JNI GetDirectBufferAddress long address;
堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。(物理内存可以扩展到很大很大。这里提及到的只是极端情况。)
*DirectByteBuffer**分配出去的内存其实也是由**GC**负责回收的,而不像**Unsafe**是完全自行管理的***,Hotspot在GC时会扫描DirectByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存。
使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。
堆外内存的好处
可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;
理论上能减少GC暂停时间;
可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;
它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据
2、零拷贝 zero copy
上面探讨的所有内容,其实已经完整的带出了零拷贝。
ByteBuffer创建的直接缓冲区就是利用零拷贝,来提高性能的。
堆外内存中的数据进行I/O操作时,不用将数据拷贝到堆外内存中去,所以就节省了一次拷贝操作(不用进行拷贝操作),所以成为零拷贝。
Netty 充分的利用此种操作,用来大大的提升了性能与速度。(高性能)
3、内存映射 MappedByteBuffer
用于直接内存映射操作。深入浅出MappedByteBuffer
4、Selector 选择器源码解析
//TODO
JNI(Java Native Interface)
引用:
本文仅供笔者本人学习,有错误的地方还望指出,一起进步!望海涵!
转载请注明出处!
欢迎关注我的公共号,无广告,不打扰。不定时更新Java后端知识,我们一起超神。
——努力努力再努力xLg
加油!