我准备战斗到最后,不是因为我勇敢,是我想见证一切。--双雪涛《猎人》
[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 选择器源码解析
深入浅出NIO之Selector实现原理
//TODO
复制
JNI(Java Native Interface)
引用:
JAVA堆内内存、堆外内存
本文仅供笔者本人学习,有错误的地方还望指出,一起进步!望海涵!
转载请注明出处!
欢迎关注我的公共号,无广告,不打扰。不定时更新Java后端知识,我们一起超神。
——努力努力再努力xLg
加油!