ByteBuffer的另一个子类DirectByteBuffer,直接在堆外分配内存。
ByteBuffer的allocateDirect方法获得这个对象
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer直接在堆外分配内存,在某些场景的时候,能够省去java堆到内核的内存复制过程,比如socket发送数据的时候,需要先把数据从java堆复制到内核,再发送出去。
DirectByteBuffer源码比HeapByteBuffer复杂
主要是申请堆外内存的过程,也就是构造方法。其他的get put等方法都是native方法,逻辑意义和HeapByteBuffer差不多,研究意义不大。只研究构造方法。
构造方法
DirectByteBuffer(int cap) { // package-private
// 初始化mark,position,limit,capacity
super(-1, 0, cap, cap);
// 是否进行页对齐,是的话,则实际申请的内存可能会增加达到对齐效果
// 默认关闭,可以通过-XX:+PageAlignDirectMemory控制
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
// 页对齐的话,实际申请的内存可能会比cap大,这里不会
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
// 预定内存,预定不到则进行回收堆外内存,再预定不到则进行Full gc,下文有代码分析
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;
}
/*
创建堆外内存回收Cleanner,Cleanner对象是一个PhantomFerence幽灵引用,
DirectByteBuffer对象的堆内存回收了之后,幽灵引用Cleanner会通知Reference
对象的守护进程ReferenceHandler对其堆外内存进行回收,调用Cleanner的
clean方法,clean方法调用的是Deallocator对象的run方法,run方法调用的是
unsafe.freeMemory回收堆外内存。
堆外内存minor gc和full gc的时候都不会进行回收(下文有测试),而是ReferenceHandle守护进程调用
cleanner对象的clean方法进行回收。只不过gc 回收了DirectByteBuffer之后,gc会通知Cleanner进行回收
*
*/
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
// 预定内存
static void reserveMemory(long size, int cap) {
// memoryLimitSet初始值为false
// 获取允许的最大堆外内存赋值给maxMemory,默认是64MB,可以通过-XX:MaxDirectMemorySize参数控制
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// 尝试预定堆外内存
// cap <= 最大的堆外内存-已经使用的堆外内存的cap大小,则说明内存足够,否则内存不够,返回false
// 预定成功,则返回
// optimist!
if (tryReserveMemory(size, cap)) {
return;
}
// 预定失败,进入这里
/* 加载Reference类的时候,Reference的静态代码块调用了SharedSecrets的set方法,所以这里获取的就是Reference静态代码块set进去的JavaLangRefAccess
* SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
public boolean tryHandlePendingReference() {
return Reference.tryHandlePending(false);
}
});
*/
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// retry while helping enqueue pending Reference objects
// which includes executing pending Cleaner(s) which includes
// Cleaner(s) that free direct buffer memory
/*
尝试进行堆外内存回收,这里调用的实际是Reference.tryHandlePending(false);false代表非堵塞调用,回收不了就返回false
不堵塞, Reference.tryHandlePending(false)实际调用的是Cleaner的clean方法回
收堆外内存, Cleaner是DirectByteBuffer的构造方法创建的:cleaner =
Cleaner.create(this, new Deallocator(base, size, cap));所以Cleaner的clean方法
实际是Deallocator的run方法,run方法执行的是的unsafe.freeMemory回收堆外内存
*/
unsafe.freeMemory(address);
while (jlra.tryHandlePendingReference()) {
// 回收了堆外内存之后,再进行预定内存
if (tryReserveMemory(size, cap)) {
return;
}
}
// 还分配不到则进行full gc垃圾回收
// trigger VM's Reference processing
System.gc();
// a retry loop with exponential back-off delays
// (this gives VM some time to do it's job)
boolean interrupted = false;
try {
long sleepTime = 1;
int sleeps = 0;
while (true) {
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
if (!jlra.tryHandlePendingReference()) {
try {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
} catch (InterruptedException e) {
interrupted = true;
}
}
}
// no luck
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
// 尝试预定堆外内存
// cap <= 最大的堆外内存-已经使用的堆外内存的cap大小,则说明内存足够,否则内存不够,返回false
private static boolean tryReserveMemory(long size, int cap) {
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
long totalCap;
while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
// 已预定内存大小reservedMemory增加size
reservedMemory.addAndGet(size);
count.incrementAndGet();
return true;
}
}
return false;
}
总结这一过程:
DirectByteBuffer有两部分,一部分就是DirectByteBuffer这个对象,这个对象是在java堆中,另一部分就是其开辟的堆外内存,创建这个对象的时候,会先判断是否有足够的堆外内存空间,如果没有,则尝试进行回收一次堆外内存,回收了之后再预定一次空间,空间还不够的话,则执行System.gc,进行full gc(因为很可能堆内内存还没来得及gc,堆外内存就不够了),full gc回收了DirectByteBuffer对象的堆内内存。因为创建DirectByteBuffer的时候,创建了Cleanner这个幽灵引用,所以,DirectByteBuffer被gc回收了之后,gc会给Reference的pending设置值,ReferenceHandler守护线程会轮询pengding,发现pending不为空,则进行处理,调用Cleanner的clean方法,clean方法会调用Deallocator对象的run方法,run方法执行的是unsafe.freeMemory(address),address是直接内存的内存地址,是unsafe.allocateMemory(size)申请内存的时候返回的。
所以:对于直接内存的回收,只有一个方法unsafe.freeMemory(address)。gc对直接内存的影响是间接的,是触发了Cleanner幽灵引用的clean方法调用了unsafe.freeMemory(address)。
下面做一个测试:测试Full gc不会对直接内存回收。
设计jvm参数 :-verbose:gc -XX:+PrintGCDetails -XX:MaxDirectMemorySize=40M
最大直接内存40M,打印垃圾回收日志
public class RevisedObjectInHeap
{
Unsafe unsafe;
public Unsafe getUnsafe() {
// 通过反射得到theUnsafe对应的Field对象
Field field = null;
try {
field = Unsafe.class.getDeclaredField("theUnsafe");
// 设置该Field为可访问
field.setAccessible(true);
// 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
unsafe = (Unsafe) field.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return unsafe;
}
private long address = 0;
public RevisedObjectInHeap()
{
Unsafe unsafe = getUnsafe();
address = unsafe.allocateMemory(2 * 1024 * 1024);
// unsafe.freeMemory(address);
// bytes = new byte[1024 * 1024];
}
public static void main(String[] args) throws InterruptedException {
while (true)
{
RevisedObjectInHeap heap = new RevisedObjectInHeap();
System.gc();
}
}
}
结果:垃圾回收一直在循环运行,但是最终还是包OOM错误。但是如果执行unsafe.freeMemory(address)释放内存,则不会报OOM错误。
顺便提一句:FileChannel的map映射也和直接内存有关系。
内存映射的原理都是操作系统支持才可行的:linux和windows都有内存映射机制,大概原理就是,将磁盘的文件和缓冲区建立一个映射关系,然后访问缓冲区就想访问磁盘文件一样,自然,访问缓冲区非常快,所以read和write也非常快。(做过测试,这种方式会快5、6倍)因为磁盘文件和缓冲区不是java堆的数据,所以自然使用直接内存会省去一次赋值,快很多,使用的Buffer是MappedByteBuffer。
大概的代码如下:
public class MapMemoryBuffer {
public static void main(String[] args) throws Exception {
RandomAccessFile f = new RandomAccessFile("C:/hinusDocs/hello.txt", "rw");
FileChannel fc = f.getChannel();
MappedByteBuffer buf = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size());
while (buf.hasRemaining()) {
System.out.print((char)buf.get());
}
System.out.println();
}
}
MappedByteBuffer var5 = (MappedByteBuffer)directByteBufferConstructor.newInstance(new Integer(var0), new Long(var1), var3, var4);
网友评论