驽马十驾 驽马十驾

驽马十驾,功在不舍

目录
堆外内存的总结
/      

堆外内存的总结

以下是对以前资料的整理,汇总如下!

简述

狭义上来讲,堆外内存通常指的就是DirectByteBuffer

为什么要使用堆外内存

  • 如果使用堆内的内存,可能导致GC的多次触发。堆外内存不受GC管理,受系统管理
  • 减少IO延迟,提高效率,也就是我们说的零拷贝。

ByteBuffer有2个实现,DirectByteBufferHeapByteBuffer

DirectByteBuffer的缺点是分配是比较耗时,所以通常都是开局申请一堆,然后自己进行管理。【不理解】

DirectByteBuffer还有一个缺点就是GC的冰山对象问题。

  • 首先DirectBytBuffer被GC的时候,会调用cleaner进行堆外的内存清理

  • 但是因为DirectByteBuffer只是一个引用,撑过了几轮YoungGC后,就会进入Old区,那里可不方便进行GC

  • 那么堆外的一堆内存就没有办法清理的吗?其实不会的,分配的时候还有Bits起到宏观调控的作用。

    • DirectByteBuffer创建的时候,会先向Bits类申请,该类会通过MaxDirectMemorySize进行判定是否需要进行后续操作
    • 如果内存不够了,会调用System.gc进行一次FullGC,所以千万不要禁用:-XX:+DisableExplicitGC,100ms后再次判定,如果还不够那么就会OOM
    • 如果内存够,采用会Unsafe进行分配堆外内存。
    • 需要说明的是堆外内存的OOM,通过:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/gc是没作用的,因为这是堆外,不受GC管理。

分配

假如分配一个堆外内存,通常采用

ByteBuffer.allocateDirect(1024)

我们看下他内部代码,代码有格式调整

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;
    }
  • 关键还是DirectByteBuffer的构造函数
  • Bits是JDK对于堆外内存使用量的一个全局管理器,能不能继续分配,需不需要GC都需要他作为调控
  • UNSAFE.allocateMemory 是具体分配对外内存的,本质还是JNI来分配
  • 创建的cleaner是为了正常GC的时候,将堆外内存给清理掉

销毁

Deallocator是一个DirectByteBuffer的内部私有类,同时实现了Runable的类,其中的run方法实现如下

private static class Deallocator
        implements Runnable {

            public void run() {
                if (address == 0) {
                    // Paranoia
                    return;
                }
                UNSAFE.freeMemory(address);
                address = 0;
                Bits.unreserveMemory(size, capacity);
            }
    }

核心就是调用:Unsafe.freeMemory,这个方法内部调用的是一个native方法freeMemory0(address);

这个类是在DirectByteBuffer构造的时候,创建的

DirectByteBuffer(int cap) {                   
        // ...
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    }

创建了一个类的成员变量cleaner,这个类就负责销毁对外内存,那么他是如何实现内存销毁的了?

public class Cleaner
    extends PhantomReference<Object>{
    ....
}

最核心的就是Cleaner继承了PhantomReference,每次GC的时候,会清理DirectByteBuffer指向堆外的真实内存。

这个调用链其实就比较清晰了:

  1. ByteBuffer对象被GC
  2. cleaner是其成员变量,所以会被GC
  3. GC时触发了Cleaner的PhantomReference作用
  4. Cleaner包裹了Deallocator
  5. 最终调用run中的UNSAFE.freeMemory(address)

Cleaner 类继承了 PhantomReference 类,并且在自己的 clean() 方法中启动了清理线程,当 DirectByteBuffer 被 GC 之前 cleaner 对象会被放入一个引用队列(ReferenceQueue),JVM 会启动一个低优先级线程扫描这个队列,并且执行 Cleaner 的 clean 方法来做清理工作。

这个类在哪些地方被调用了?

其他

打印堆外对象的信息

@Scheduled(initialDelay = 3000, fixedDelay = 4000)
    public void direct66() throws Exception {
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        ObjectName objectName = new ObjectName("java.nio:type=BufferPool,name=direct");
        MBeanInfo info = mbs.getMBeanInfo(objectName);
        for (MBeanAttributeInfo i : info.getAttributes()) {
            System.out.println(i.getName() + ":" + mbs.getAttribute(objectName, i.getName()));
        }
    }
骐骥一跃,不能十步。驽马十驾,功在不舍。