堆外内存的介绍及释放

本文详细探讨了Java中的堆外内存管理,包括其分配、使用场景、特点和回收机制。通过NIO的ByteBuffer.allocateDirect()和Unsafe类进行堆外内存分配,并解释了如何通过Cleaner和PhantomReference实现堆外内存的自动回收。此外,文章还提到了手动释放堆外内存的方法,以及在finalize方法中释放内存的重要性。
摘要由CSDN通过智能技术生成
热爱美好的事物,是一种追求

介绍

____堆外内存意味着把内存对象分配在 Java 虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),

作用:能缩短垃圾回收时间

​ 适合生命期中等或较长的对象

特点

  • 对于大内存有良好的伸缩性,可以自行扩展
  • 对垃圾回收停顿的改善可以明显感觉到,不影响用户线程
  • 在进程间可以共享,减少虚拟机间的复制

如果使用的多,那么你可能会考虑硬盘的速度会有影响

堆外内存的回收

堆外内存的分配很简单,比如如下

ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);

或者

Unsafe unsafe = Unsafe.getUnsafe()
unsafe.allocateMemory(2 * 1024 * 1024);

分配知道了,我们知道堆内内存是基于JVM的自动垃圾回收,那么堆外内存呢?

先抛出来,一般FullGC时会回收

不过注意不要使用**-XX:+DisableExplicitGC**,打开则让System.gc()无效,内存无法有效回收,导致OOM
问题:堆内内存充足,堆外内存造成物理内存耗光,仍然没有触发FullGC


NIO方式分配堆外内存的回收

先看第一种方式,

​ 我们知道,JVM垃圾回收要么会在内存不足时回收,要么在空闲时也会回收,JVM自己管理

矛盾点在于:堆内内存还没有达到可以FullGC的点,然而堆外内存已经消耗完毕了,可能会造成OOM,这就很尴尬

​ 所以需要我们手动释放,那怎么手动释放,看下源码 ByteBuffer.allocateDirect()

在这里插入图片描述
可以看到都做了mark,position,cap的初始化,

那么其中Bits.reserveMemory(size, cap);:用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小,具体执行中需要首先用 tryReserveMemory 方法来判断系统内存(堆外内存)是否足够;分为多种情况这里暂不细说

分配内存后大家看

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

类中维护了这个定义:private final Cleaner cleaner;,就是它来维护清理相关,Deallocator是一个内部类,是用来执行清理动作的,如下

在这里插入图片描述
可以看到实现了Runnable接口,调用了unsafe.freeMemory(address),释放内存,那是如何触发的呢?先往下看 Cleaner.create

    private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
    }

这时候再贴一下刚才的入口

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

this为当前内存块,var1为new Deallocator,里面是清理动作,new Cleaner(var0,var1);

    private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
    }

可以看到 super(var1, dummyQueue); 这个queue给了Reference抽象类的volatile ReferenceQueue<? super T> queue;这个后面再说
var2赋值给了thunk,它是什么,如下
在这里插入图片描述
那thunk是怎么用的呢?看下面有个clean方法中 this.thunk.run();

执行了,执行什么???

执行的就是刚才DirectByteBuffer中的Deallocator这个内部类的unsafe.freeMemory(address);清理动作
在这里插入图片描述
也就是说我们如果主动调用这个clean就可以了

比如说我们创建了byteBuffer,可以判断调用

if (byteBuffer instanceof DirectBuffer) {
    ((DirectBuffer)byteBuffer).cleaner().clean();
}

那么话再说Cleaner这个类,queue给了Reference抽象类的volatile ReferenceQueue<? super T> queue;

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

上面初始化Clean的时候会调用父类 Reference

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

Cleaner的clean方法被Reference的tryHandlePending调用
在这里插入图片描述
ReferenceHandler是Reference的内部类
在这里插入图片描述
它又在Reference的static块中new了,并且设置后台线程执行
在这里插入图片描述
那么现在有个问题,这个static在调用构造器时并没有被触发???也就没有自动清理,这个问题大家看下怎么回事,可以留言


unsafe方式分配内存的手动回收

    private long address = 0;

    private Unsafe unsafe = Unsafe.getUnsafe();

    // 让对象占用堆内存,触发[Full GC
    private byte[] bytes = null;

    public TestMain() {
        address = unsafe.allocateMemory(2 * 1024 * 1024);
        bytes = new byte[1024 * 1024];
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize." + bytes.length);
        unsafe.freeMemory(address);
    }

覆盖了finalize方法,手动释放分配的堆外内存,如果堆中的对象被回收,那么相应的也会释放占用的堆外内存

	private byte[] bytes = null;

这行代码主要目的是为了触发堆内存的垃圾回收行为,顺带执行对象的finalize释放堆外内存。如果没有这行代码或者是分配的字节数组比较小,程序运行一段时间后还是会报OutOfMemoryError。这是因为每当创建1个RevisedObjectInHeap对象的时候,占用的堆内存很小(就几十个字节左右),但是却需要占用2M的堆外内存。这样堆内存还很充足(这种情况下不会执行堆内存的垃圾回收),但是堆外内存已经不足,所以就不会报OutOfMemoryError。

此处参考:https://blog.csdn.net/hellozhxy/article/details/102728206

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是小酒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值