定义

  • Direct Memory
  • 常见于 NIO 操作时,用于数据缓冲区(ByteBuffer)
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理
  • 属于系统操作的内存,不属于JVM内存

观察下面案例

/** * 演示 ByteBuffer 作用 */
public class Demo1_9 {
    static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}

  • 传统的IO使用时间接近3秒,而directBuffer用了不到1秒

传统的IO原理如下图所示:
* 由于出现了两块缓冲区,因为Java读取不到系统缓冲区,相当于复制了两份

ByteBuffer原理如下图所示:

  • 当调用ByteBuffer.allocateDirect(_1Mb)。会在操作系统划出一份直接内存,Java代码和系统都可以共用该内存,比传统少了一次文件的复制,所以会有成倍的优化。

观察下面案例

/** * 演示直接内存溢出 */
public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        // jdk8 对方法区的实现称为元空间
    }
}
  • 可以发现直接内存也会出现内存溢出

观察下面的案例

/** * 禁用显式回收对直接内存的影响 */
public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    /* * -XX:+DisableExplicitGC 显式的 */
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
        System.in.read();
    }
}
  • 可以观察到使用垃圾回收,会回收掉直接内存

观测下面代码

/** * 直接内存分配的底层原理:Unsafe */
public class Demo1_27 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);//返回一个内存地址
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);//释放该地址的内存
        System.in.read();
    }

    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}
  • <mark>直接内存的回收和释放,通过的是Unsafe类的管理</mark>

allocateDirect方法源码分析

 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;




    }

可以观察到,最后还是调用的UNSAFE.allocateMemory(size)方法调用,UNSAFE则就是Unsafe

static final Unsafe UNSAFE = Unsafe.getUnsafe();

而释放内存也是调用 UNSAFE.freeMemory(address)方法实现

 private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }

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

通过Cleaner.create(this, new Deallocator(base, size, cap)),让Cleaner创建一个对ByteBuffer对象的虚引用,当ByteBuffer被JVM回收掉时,就会将直接内存回收掉

分配和回收原理

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存

<mark>:-XX:+DisableExplicitGC :Explicit是显式的意思,会让代码中的垃圾回收禁用,经常会在JVM调优时使用。一般情况下,不建议用GC机制回收直接内存,而使用Unsafe管理直接内存</mark>