首先,先解释几个概念:
*虚引用:虚引用算是最弱的引用类型,不能通过他来获取目标对象,虚引用主要被用来跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否即将被垃圾回收。
*内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境。比如socket I/0操作或者文件的读写操作等
*用户态:上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源
*系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口
调用JNI时其实就是从用户态切换到了内核态,再进行的系统调用。
下面进入正题:
DirectByteBuffer 与堆外内存
堆外内存不由JVM管理,但是在JVM中可以通过DirectByteBuffer 类对象,DirectByteBuffer中的unsafe.allocateMemory(size);是个一个native方法,这个方法分配的是堆外内存,通过C的malloc来进行分配的。分配的内存是系统本地的内存。
堆外内存与堆内内存的数据拷贝
比如从一个文件中读取数据到堆内内存中,其实是先读取到堆外内存,再从堆外拷贝到堆内内存的。
使用DirectByteBuffer的话可以直接在堆外分配一个内存来存储数据,程序通过调用Unsafe类的native方法直接将数据读/写到堆外内存中。这种方式就不存在堆内内存和堆外内存数据拷贝的操作了。这样在进行I/O操作时,只需要将这个堆外内存地址传给JNI的I/O的函数就好了。(NIO的buffer就是这么做的,后面再说)。而在正常的IO操作中需要使用InputStream/OutputStream等作为堆内内存的中介。
// 将文件写入inputStream
InputStream inputStream = new FileInputStream("file.xml");
//设置outputStream
OutputStream outputStream = new FileOutputStream("file-new.xml");
int bytesWritten = 0;
int byteCount = 0;
byte[] bytes = new byte[1024];
while ((byteCount = inputStream.read(bytes)) != -1) //将inputStream的数据(即文件写进去的)写入字节数组
{
outputStream.write(bytes, bytesWritten, byteCount); //将字节数组里的数据写入outputStream(即写入了文件中)
bytesWritten += byteCount;
}
inputStream.close();
outputStream.close();
所以例如通过socket传输文件时,如果使用普通的InputStream /outputStream则需要将文件数据读入JVM的堆内内存,再拷贝到堆外内存再传输。但是如果使用NIO的buffer就不需要了,只需要通过DirectByteBuffer中堆外内存的地址来传给IO函数就行了。
DirectByteBuffer堆外内存的创建和回收
1.创建与使用
DirectByteBuffer的构造函数如下:
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); //调用native方法分配堆外内存(C的malloc)
} 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; // adress是分配的内存的地址,后面操作内存会用到
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
所以DirectByteBuffer里并没有存储数据,里面存放的是堆外内存的地址,每次get/put数据都是从堆外内存直接存取。所以使用它进行I/O操作时只需将堆外内存地址传给JNI的I/O的函数就可以了,而不需要将堆外内存的数据拷贝到堆内内存,然后再将数据存取文件
DirectByteBuffer获取数据的方法如下:
public ByteBuffer get(byte[] dst, int offset, int length) {
if (((long)length << 0) > Bits.JNI_COPY_TO_ARRAY_THRESHOLD) {
checkBounds(offset, length, dst.length);
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
if (length > rem)
throw new BufferUnderflowException();
long dstOffset = arrayBaseOffset + ((long)offset << 0);
unsafe.copyMemory(null, //调用native方法直接拷贝堆外内存的数据
ix(pos),
dst,
dstOffset,
(long)length << 0);
position(pos + length);
} else {
super.get(dst, offset, length);
}
return this;
}
2.回收
当堆外内存不够时会触发一个clean()方法将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。如果释放完还不够会触发一次full gc。如果再不够就会抛出OutOfMemoryError(“Direct buffer memory”)异常
---------------------------------------我是分割线----------------------------------------
说了这么多,那么DirectByteBuffer操作堆外内存和NIO有什么关系呢?
我们已经知道NIO的特色在于他的selector、channel、buffer的设计,它的一大优势就是非阻塞,即通过selector来轮询监听事件,根据事件发生来分配线程处理,而不像阻塞IO那样每个连接都需要在server端有线程等待。
除了以上优势以外,NIO的另一个好处在于它使用的Buffer,它的buffer可以双向读写操作(通过flip()实现转换),包含了InputStream和OutputStream的功能,另外通过使用ByteBuffer可以直接将文件从堆外内存存取,而不用使用JVM的堆内内存作为中介。
网友评论