1. 组成
1.1 类关系
1.2 Buffer
Buffer 即缓冲器,它将特定基础类型(可以是byte、char、short、int、long、double、float、boolean的任意一种)的元素的线性有限序列作为缓冲容器, 缓冲区的基本属性是其容量,限制和位置。
1.2.1 属性
- 容量(Capacity)
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能
被改变。 - 上界(Limit)
缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。 - 位置(Position)
下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。 - 标记(Mark)
一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =
mark。标记在设定前是未定义的(undefined)。
标记,位置,上界和容量值的以下不变量保持不变:
0 <= mark <= position <= limit <= capacity
1.2.2 API
API的设计采用级联调用,类似StringBuffer的设计,方便链式调用
public abstract class Buffer {
public final int capacity( )
public final int position( )
public final Buffer position (int newPositio
public final int limit( )
public final Buffer limit (int newLimit)
public final Buffer mark( )
public final Buffer reset( )
public final Buffer clear( )
public final Buffer flip( )
public final Buffer rewind( )
public final int remaining( )
public final boolean hasRemaining( )
public abstract boolean isReadOnly( );
}
读取(get)
我们知道,Buffer是一个上层抽象类,定义了缓冲概念的方法模型,具体的缓存数据存储是有实现它的子类也就是具体的基本数据类型的子类(byte、char、short、int、long、double、float、boolean的任意一种)进行实现限定数据类型的存储和提供对应获取数据的类型的。
填充(put)
相当于get方法的对应方法,需要注意的是get和put是结对出现的,缓冲数据存储是顺序的,因此读取也是要遵循顺序,否则会得到非期望结果甚至异常。
翻转(flip & rewind)
flip() 函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态。这里可以这么理解,当写入数据的时候,position、limit等标记属性都会进行变动符合写入状态下的写模式操作,当读取的时候必然要对position、limit等标记属性进行变更以符合读模式的要求,相反情况也是如此,因此flip函数提供的就是这种读写模式互相切换的功能。
rewind() 它只是将位置值设回 0。您可以使用它后退,重读已经被翻转的缓冲区中的数据。
释放(clear)
将position重置为0,此时缓冲区内存数据并未发生变更,只是改变了标识
1.2.3 创建方式
- 直接开辟内存空间
CharBuffer charBuffer = CharBuffer.allocate (100) - 使用自己指定的内存空间
char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray);
1.2 HeapByteBuffer
以 堆内存 实现缓存区,即JVM内存实现,此时缓存区是通过 字节数组 数据结构进行实现
1.3 HeapByteBufferR
以堆内存实现缓存区,且只读不可写,该类是一个包内封装类不能直接声明,可以通过调用byteBuffer.asReadOnlyBuffer() 获得。
1.4 DirectByteBuffer
以 堆外内存 实现缓存区
构造方法
通过DirectByteBuffer类的依赖看到主要依赖JVM底层的**sun.misc.Cleaner
、sun.misc.Unsafe、sun.misc.VM**实现
- Cleaner 负责堆外内存回收
- Unsafe 进行内存分配
- VM 提供基本功能支持
DirectByteBuffer(int cap) { // package-private 私有包
super(-1, 0, cap, cap);
// 是否为页对齐的内存
boolean pa = VM.isDirectMemoryPageAligned();
// 获取页尺寸,pageSize大小
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 {
//使用JNI方式
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
系统内核交互
编写一段简易代码,调用ByteBuffer.allocateDirect(10) 来开辟堆外内存,以Linux OS作为操作系统
/**
* created by guanjian on 2021/1/18 9:34
*/
public class DirectByteBufferRTest {
public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
byteBuffer.put((byte)1);
System.out.println(byteBuffer.toString());
}
}
通过强大的strace命令对应用进程和操作系统接口方法交互进行跟踪和输出,
strace -o log.txt -T -tt -e trace=memory java DirectByteBufferTest
如下:
09:48:00.328916 mmap(0x7fa415428000, 235008, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa415428000 <0.000007>
09:48:00.329024 mmap(NULL, 37494, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa415e5b000 <0.000008>
09:48:00.329108 mmap(NULL, 3150136, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa414555000 <0.000009>
09:48:00.329134 mprotect(0x7fa414656000, 2093056, PROT_NONE) = 0 <0.000010>
09:48:00.329160 mmap(0x7fa414855000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x100000) = 0x7fa414855000 <0.000017>
09:48:00.329296 mprotect(0x7fa414855000, 4096, PROT_READ) = 0 <0.000009>
09:48:00.331534 munmap(0x7fa415e5b000, 37494) = 0 <0.000013>
09:48:00.331600 mmap(NULL, 1052672, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fa414454000 <0.000008>
09:48:00.331638 mprotect(0x7fa414454000, 4096, PROT_NONE) = 0 <0.000007>
09:48:00.378451 +++ exited with 0 +++
可以看到分别调用了mmap、mprotect、munmap
- mmap 进行内存映射
- mprotect 对内存区域进行保护
- munmap 对内存映射进行解除
感兴趣的话可以继续深入了解函数的作用,这里不展开。
mmap内存映射
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式, 因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据: 一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
内存回收问题
- 系统使用Netty进行服务器之间的通信,而NIO使用的DirectByteBuffer,其申请的native memory只有在old gen GC(full GC/major GC或者concurrent GC)时才可以回收。主要原理是Full GC的会对old gen做reference processing,进而可以触发Cleaner对已死的DirectByteBuffer对象做清理工作;而如果很长一段时间没做过GC或者只做了young GC,则不会在old gen触发Cleaner的工作,那么就可能让本来已经死了的、但已经晋升到old gen的DirectByteBuffer关联的native memory得不到及时释放;
- 为DirectByteBuffer分配空间的过程中,会通过System.gc()的显示调用强迫已经无用的DirectByteBuffer对象释放掉他们关联的native memory;但是如果系统在运行的时候,设置了JVM参数:-XX:DisableExplicitGC,那么System.gc()的显示调用将变成空调用;
- 此时只有自然触发old gen GC进行内存回收,或者等待native memory占用达到了设置的MaxDirectMemorySize之后,出现内存溢出;
- 如果不使用-XX:MaxDirectMemorySize={size}进行设置,MaxDirectMemorySize其值将默认为JVM可以使用最大对内存(-Xmx)减去一个Survivor space的值。
优缺点
优点:
- 不会占用JVM内存空间,从操作系统中开辟内存空间
- 由于不占用JVM内存空间,因此不会出现受Young Gen GC控制,在老年代GC(full GC/major GC或者concurrent GC)时才可以回收
- 底层通过操作系统OS内存文件映射进行操作,省去了JVM内存复制到系统内存的过程,实现零拷贝。原本读取和写入均需要 用户空间 <–> 内核空间 <–> 磁盘存储 的四次copy传输,现在通过mmap实现了用户空间共享内存直接映射磁盘文件,减少了内核空间的拷贝。
缺点:
- 不受JVM垃圾回收机制控制,虽然提供了内存回收机制,出现问题不容易排查
1.5 DirectByteBufferR
以堆外内存实现缓存区,且只读不可写,类似HeapByteBufferR
1.6 MappedByteBuffer
是ByteBuffer对File文件流进行处理的实现,底层也是支持mmap来直接操作磁盘文件,减少内核空间拷贝实现高性能操作的
2. API
下面通过代码示例和注释说明一些常用方法,allocateXXX、flip、get、put、rewind、clear
/**
* created by guanjian on 2021/1/18 9:34
*/
public class DirectByteBufferTest {
public static void main(String[] args) throws InterruptedException {
/**
* allocateXXX
* 开辟内存空间
*/
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
/**
* put
* 写入操作
*/
byteBuffer.putInt(1);//一个int占用4字节
byteBuffer.putInt(233);//一个int占用4字节
System.out.println(byteBuffer);//[pos=8 lim=10 cap=10]
/**
* flip
*
* 从写模式转换到读模式,这里需要调用flip,这里其实是对ByteBuffer中的变量position、limit、capacity进行重新复制来支持读取操作,
* 因为底层是通过有限线性容器进行存储的,通过变更变量控制进行写入和读取切换实现空间共享
*/
byteBuffer.flip();
System.out.println(byteBuffer);//[pos=0 lim=8 cap=10]
/**
* get
* 读取操作
*/
System.out.println(byteBuffer.getInt());//这里获取按照存储顺序对称取出,底层存储的都是byte数组,getXXX和putXXX只不过是对Java基本类型与byte之间的封装来保存或者读取
System.out.println(byteBuffer);//[pos=4 lim=8 cap=10]
/**
* rewind
* 将position标记为0,可以从头继续读取
*/
byteBuffer.rewind();//复位后可以从0重新开始读
System.out.println(byteBuffer);//[pos=0 lim=8 cap=10]
System.out.println(byteBuffer.getInt());//由于position=0,继续读取第一个元素
/**
* clear
* 清除,初始化
* 其实这里的内存空间并没有释放,而是对内部position、mark、limit、capacity变量这些进行初始化
*/
byteBuffer.clear();
System.out.println(byteBuffer);//[pos=0 lim=10 cap=10]
}
}
【控制台输出】
java.nio.DirectByteBuffer[pos=8 lim=10 cap=10]
java.nio.DirectByteBuffer[pos=0 lim=8 cap=10]
1
java.nio.DirectByteBuffer[pos=4 lim=8 cap=10]
java.nio.DirectByteBuffer[pos=0 lim=8 cap=10]
1
java.nio.DirectByteBuffer[pos=0 lim=10 cap=10]
3. 参考
https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html
https://www.cnblogs.com/sally-zhou/p/6501623.html
https://blog.csdn.net/luckywang1103/article/details/50619251
https://segmentfault.com/a/1190000014616732
https://blog.csdn.net/Caoyuan_001/article/details/84543119
《Java NIO》