java NIO 相关(三) NIO 直接缓冲区和非直接缓冲区
非直接缓冲区
通过allocate() 方法分配缓冲区,将缓冲区建立在jvm的内存
直接与非直接缓冲区字节缓冲区要么是直接的,要么是非直接的·如果为直接字节缓冲区·则Java虐拟机会尽最大努力直接在此缓冲区上执行本机|/0操作·也就是说,在每次调用基础操作系统的一个本机1/0操作之前(或之后),虐拟机都会尽量避免将缓冲区的内容复制到中间缓冲区巾(或从中间缓冲区中复制内容)。
直接缓冲区
通过allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率
直接字节缓冲区可以通过调用此类的allocateDirect()工厂方法来创建·此方法返回的缓冲区透行分配和取消分配所需成本通常于非直接缓冲区·
直接缓冲区的内容可以驻留在常規的垃圾回收堆之外·因此.它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机|/0操作影响的大型、持久的缓冲区·一情况下,最好舣在直接缓冲区能在程序性能方面帚来明显好处时分配它们
直接字节缓冲区还可以通过FileChanneI的map()方法将文件区域直接暌射到内存中来创建·该方法返回MappedByteBuffer·Java平台的实现有助于通过JNI从本机代码创建直接字节缓冲区·如果以上这些缓冲区中的某个缓冲区实蚪指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或鞘后的某时间导致出不定的异常·字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定.供此方法是为了能够在性能关键型代码中挾行显式缓冲区管理·
图解直接缓冲区和非直接缓冲区
源码分析
直接缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public 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;
*/
}
可以看到直接在堆中创建空间也就是数组非直接缓冲区
public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
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);
} 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;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
可以看到是根据
VM.isDirectMemoryPageAligned() 方法直接调用了内存分,让操作系统开辟缓存空间
DirectByteBuffer(堆外内存)
DirectByteBuffer 继承自 MappedByteBuffer,它们都是使用的堆外内存,不受 JVM 堆大小的 限制,只是前者仅仅是分配内存,后者是将文件映射到内存中。
可以通过 ByteBuf.allocateDirect 方法获取
堆外内存的特点(大对象;加快内存拷贝;减轻 GC 压力)
- 对于大内存有良好的伸缩性(支持分配大块内存)
- 对垃圾回收停顿的改善可以明显感觉到(堆外内存,减少 GC 对堆内存回收的压力)
- 在进程间可以共享,减少虚拟机间的复制,加快复制速度(减少堆内内存拷贝到堆
外内存的过程) - 还可以使用 池+堆外内存 的组合方式,来对生命周期较短,但涉及到 I/O 操作的对象进行堆外内存的再使用。( Netty 中就使用了该方式 )
堆外内存的一些问题
- 堆外内存回收问题(不手工回收会导致内存溢出,手工回收就失去了 Java 的优势);
- 数据结构变得有些别扭。要么就是需要一个简单的数据结构以便于直接映射到堆外内存, 要么就使用复杂的数据结构并序列化及反序列化到内存中。很明显使用序列化的话会比较头 疼且存在性能瓶颈。使用序列化比使用堆对象的性能还差。
堆外内存的释放
java.nio.DirectByteBuffer 对象在创建过程中会先通过 Unsafe 接口直接通过 os::malloc 来分配 内存,然后将内存的起始地址和大小存到 java.nio.DirectByteBuffer 对象里,这样就可以直接 操作这些内存。这些内存只有在 DirectByteBuffer 回收掉之后才有机会被回收,因此如果这 些对象大部分都移到了 old,但是一直没有触发 CMS GC 或者 Full GC,那么悲剧将会发生, 因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过 -XX:MaxDirectMemorySize 来指定最大的堆外内存大小,当使用达到了阈值的时候将调用 System.gc 来做一次 full gc,以此来回收掉没有被使用的堆外内存。
GC 方式:
存在于堆内的 DirectByteBuffer 对象很小,只存着基地址和大小等几个属性,和一个 Cleaner, 但它代表着后面所分配的一大段内存,是所谓的冰山对象。通过前面说的 Cleaner,堆内的 DirectByteBuffer 对象被 GC 时,它背后的堆外内存也会被回收。
当新生代满了,就会发生 minor gc;如果此时对象还没失效,就不会被回收;撑过几次 minorgc 后,对象被迁移到老生代;当老生代也满了,就会发生 full gc。 这里可以看到一种尴尬的情况,因为 DirectByteBuffer 本身的个头很小,只要熬过了 minor gc, 即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发 full gc,如果没有别 的大块头进入老生代触发 full gc,就一直在那耗着,占着一大片堆外内存不释放。
这时,就只能靠前面提到的申请额度超限时触发的 System.gc()来救场了。但这道最后的保 险其实也不很好,首先它会中断整个进程,然后它让当前线程睡了整整一百毫秒,而且如果 gc 没在一百毫秒内完成,它仍然会无情的抛出 OOM 异常。
那为什么 System.gc()会释放 DirectByteBuffer 呢?
每个 DirectByteBuffer 关联着其对应的 Cleaner,Cleaner 是 PhantomReference 的子类,虚 引用主要被用来跟踪对象被垃圾回收的状态,通过查看 ReferenceQueue 中是否包含对象所 对应的虚引用来判断它是否即将被垃圾回收。
当GC时发现DirectByteBuffer除了PhantomReference外已不可达,就会把它放进 Reference 类 pending list 静态变量里。然后另有一条 ReferenceHandler 线程,名字叫 “Reference Handler”的,关注着这个 pending list,如果看到有对象类型是 Cleaner,就会执行它的 clean(), 其他类型就放入应用构造 Reference 时传入的 ReferenceQueue 中,这样应用的代码可以从 Queue 里拖出这些理论上已死的对象,做爱做的事情——这是一种比 finalizer 更轻量更好的 机制。
手工方式:
如果想立即释放掉一个 MappedByteBuffer/DirectByteBuffer,因为 JDK 没有提供公开 API, 只能使用反射的方法去 unmap;
或者使用 Cleaner 的 clean 方法。