堆外内存

堆外内存,又称直接内存,主要用于提升IO效率,减少数据拷贝,降低GC压力。其分配和回收可能带来性能开销,但通过对象池可以优化。堆外内存溢出可能导致"Direct buffer memory" OOM错误,可通过设置-XX:MaxDirectMemorySize限制大小。Cleaner对象与PhantomReference关联,当DirectByteBuffer被GC回收时,Cleaner负责堆外内存的释放。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

堆外内存定义 (狭义)

  创建Java.nio.DirectByteBuffer时分配的内存。

堆外内存优缺点

  • 优点: 提升了IO效率(减少了从堆内到堆外的数据拷贝);减小了GC压力(不参与GC,有特定的回收机制);
  • 缺点: 分配和回收堆外内存比较耗时;(解决方案:通过对象池避免频繁地创建和销毁堆外内存)

为什么堆外内存能够提升IO效率?

  因为从堆内向磁盘/网卡读写数据时,数据会被先复制到堆外内存,然后堆外内存的数据被拷贝到硬件,如下图所示,直接写入堆外内存可避免堆内到堆外的一次数据拷贝
在这里插入图片描述
在这里插入图片描述
堆内拷贝到堆外的原因: 操作系统把内存中的数据写入磁盘或网络时,要求数据所在的内存区域不能变动,但是JVM的GC机制会对内存进行整理,导致数据内存地址发生变化,所以无奈,JDK只能先拷贝到堆外内存(不受GC影响),然后把这个地址发给操作系统。

堆外内存申请

  JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请,底层通过unsafe.allocateMemory(size)实现。Netty、Mina等框架提供的接口也是基于ByteBuffer封装的。
这里写图片描述

堆外内存回收

  JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象,用于保存堆外内存的元信息(开始地址、大小和容量等),当DirectByteBuffer被GC回收后,Cleaner对象被放入ReferenceQueue中,然后由ReferenceHandler守护线程调用unsafe.freeMemory(address),回收堆外内存。
这里写图片描述

  • 主动回收(推荐): 对于Sun的JDK,只要从DirectByteBuffer里取出那个sun.misc.Cleaner,然后调用它的clean()就行;
  • 基于 GC 回收: 堆内的DirectByteBuffer对象被GC时,会调用cleaner回收其引用的堆外内存。问题是YGC只会将将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收,如果有大量的DirectByteBuffer对象移到了old区,但是又一直没有做CMS GC或者FGC,而只进行YGC,物理内存会被慢慢耗光,触发OOM;
    这里写图片描述

堆外内存溢出

  Java.nio.DirectByteBuffer所需的内存超过了物理分配的堆外内存,出现"java.lang.OutOfMemoryError: Direct buffer memory"。

堆外内存使用注意

  java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。

 private static class Deallocator
        implements Runnable
    {

        private static Unsafe unsafe = Unsafe.getUnsafe();

        private long address;
        private long size;
        private int capacity;

        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);
        }

    }

堆外内存默认大小

堆外内存默认值: (-Xmx值) - (1个survivor大小)

为什么Cleaner对象能够被放入ReferenceQueue中?

  Cleaner对象关联了一个PhantomReference引用,如果GC过程中某个对象除了只有PhantomReference引用它之外,并没有其他地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在GC完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块。

参考:

  1. Direct Buffer
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值