JVM内存结构笔记05-直接内存


定义

  • 直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
  • 直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
  • NIO(Non-Blocking I/O,也被称为 New I/O)引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
  • 直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

直接内存(Direct Memory)和堆外内存(Off-Heap Memory)

直接内存:通常指的是通过Java的java.nio.ByteBuffer类及其allocateDirect方法分配的内存。这种内存不是从JVM的堆中分配的,而是直接从操作系统的本地内存中分配的。因此,它不受JVM堆大小的限制,并且可以减少数据从JVM堆到本地内存之间的复制操作,这对于提高I/O密集型应用的性能特别有用。
堆外内存:是指所有在 Java 堆之外的内存,它是一个相对宽泛的概念,不仅仅包括直接内存,还包括了如本地方法栈、方法区等占用的内存,以及通过 JNI(Java Native Interface)调用本地代码分配的内存等。

直接内存的特点

1. 不属于 JVM 运行时数据区

直接内存是 JVM 外部的内存区域,不受 JVM 堆大小的限制(-Xmx 参数不影响直接内存)。
它由操作系统直接管理,JVM 只是通过本地方法调用操作系统的内存分配函数。

2. 高性能

直接内存的读写性能通常高于堆内存,因为它避免了数据在 JVM 堆和本地内存之间的拷贝。
直接内存适用于需要频繁与本地代码(如操作系统或硬件)交互的场景。

3. 手动管理

直接内存的分配和释放需要手动管理,JVM 不会自动回收直接内存。
如果直接内存使用不当,可能会导致内存泄漏或 OutOfMemoryError。

直接内存与堆内存的区别

在这里插入图片描述

直接内存是否会被gc回收

代码示例

public class Demo {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * 让System.gc()失效:
     * -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();
    }
}

运行项目
因为使用的是直接内存,所以在任务管理器中查看
在这里插入图片描述
控制台点击回车,运行代码执行gc回收
发现直接内存被释放了
在这里插入图片描述

思考:垃圾回收不会管理直接内存,所以为什么这个垃圾回收会导致直接内存被释放掉

原因:

  1. DirectByteBuffer对象:当你使用ByteBuffer.allocateDirect(int capacity)来分配直接内存时,实际上创建的是一个DirectByteBuffer实例。这个对象不仅包含指向直接内存块的引用,还包含了其他元数据信息。
  2. Cleaner机制:DirectByteBuffer类内部使用了sun.misc.Cleaner机制。这是一个基于引用队列(ReferenceQueue)和虚引用(PhantomReference)的清洁器,用于在DirectByteBuffer对象变得不可达时触发清理动作。当没有强引用指向某个DirectByteBuffer对象,并且该对象进入垃圾回收阶段时,相关的Cleaner会被触发。
  3. 释放直接内存:当Cleaner被触发时,它会执行一个清理操作,这个操作通常包括调用本地方法来释放之前通过JNI分配的直接内存。也就是说,虽然直接内存本身不由JVM的垃圾收集器直接管理,但DirectByteBuffer对象是堆上的对象,当它被垃圾回收时,其关联的直接内存也会被释放。

如上面代码示例中,使用ByteBuffer.allocateDirect(n)在非堆内存中分配了 n 字节的空间,还在堆上创建了一个DirectByteBuffer对象,该对象持有对这块直接内存的引用。
当没有任何强引用指向byteBuffer对象时(例如,将byteBuffer = null),并且经过垃圾回收后,如果DirectByteBuffer对象被认定为可回收,那么它的Cleaner就会被触发,进而释放掉那块直接内存。

分配和回收原理

使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法。

ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存。

使用Unsafe对象验证Unsafe 对象完成直接内存的分配回收

public class Demo1 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        // base为内存地址
        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);
        }
    }
}

运行代码
在这里插入图片描述
控制台点击回车,发现直接内存被释放
在这里插入图片描述

查看源码

public static void main(String[] args) throws IOException {
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
}


java.nio.DirectByteBuffer#DirectByteBuffer(int)

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;
    }
    //回调任务对象Deallocator
    //进入Deallocator
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    //Cleaner是一个虚引用类型,当this(DirectByteBuffer对象)被垃圾回收时,会触发Cleaner的clean()
    att = null;
}


java.nio.DirectByteBuffer.Deallocator#run

private static class Deallocator implements Runnable{
    ...
    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        //在这里释放直接内存
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }
}

返回上级进入Cleaner.create

Cleaner是一个虚引用类型,当this(DirectByteBuffer对象)被垃圾回收时,会触发Cleaner的clean()
sun.misc.Cleaner#clean

public class Cleaner extends PhantomReference<Object> {
    public void clean() {
        if (remove(this)) {
            try {
                //执行
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }
    
                        System.exit(1);
                        return null;
                    }
                });
            }
        }
    }
}

在以上代码示例中,通过配置 -XX:+DisableExplicitGC 让System.gc()失效(因为System.gc()会回收新生代和老年代的内存影响效率),所以会导致直接内存无法被回收(程序结束时才会回收),此时可以通过Unsafe.freeMemory(long) 来控制直接内存的回收。


相关文章:
JVM内存结构

JVM垃圾回收

JVM类加载与字节码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值