Java中的强-软-弱-虚引用

Posted by W-M on November 23, 2017

最近复习Java NIO相关知识时遇到了一个问题,我们可以通过ByteBuffer.allocateDirect()来分配直接内存,分配直接内存的好处是对于那些较大的且长期存活频繁使用的Buffer,通过使用直接内存,可以减少数据从内核缓冲区到用户缓冲区的复制操作从而提高I/O性能,但是通过DirectBuffer分配的内存不属于JVM管理,那么在堆内存不足触发GC操作时,对于这部分直接内存JVM是如何处理的呢?GC时会将其释放吗?通过查询资料得知,在通过ByteBuffer.allocateDirect()方法分配直接内存时,allocateDirect()方法会为每一个创建出来的DirectBuffer都关联一个相应的Cleaner,通过此Cleaner监测DirectBuffer的回收事件,当DirectBuffer确定可被回收时,就会调用其相应的clean方法来回收此DirectBuffer占用的堆外内存。

那么为什么可以通过Cleaner对象来监测DirectBuffer的回收事件呢?这是因为Cleaner类是虚引用(PhantomReference)的子类,当检测到DirectBuffer对象是虚引用不可达的时候,会由JVM将此DirectBuffer对象关联的虚引用状态由Active变为Pending,之后会由JVM将引用加入Reference类中的static Pending队列时调用notify(猜测)唤醒Reference类中的ReferenceHandler线程的tryHandlePending()方法,调用Cleaner的clean方法完成堆外内存的清理操作。

接下来就来具体介绍下Java中的强、软、弱、虚引用,之后具体介绍DirectBuffer直接内存清理过程。

文章中很多地方属于个人猜测,请辩证观看!


Java中的强、软、弱、虚引用及其异同

1.强引用

默认引用实现,即我们通过使用new关键字创建的对象返回的引用。当一个对象在通过对象可达性分析算法被判断为强引用可达时,即使即将产生OOM,也不会将此对象回收。

强引用可达:不使用任何java.lang.ref包中的类即可获取的对象,比如下面的Food对象。

Food food = new Food();//创建了Food对象,并保存了Food对象的强引用food

2.软引用(SoftReference)

当一个对象在通过对象可达性分析算法被判断为软引用可达时,SoftReference会尽可能长的保留引用直到JVM内存不足时才会被GC尝试回收, 适合于缓存应用。

软引用可达:指向一个对象的所有引用中不包含强引用,包含软引用,则被判定为软引用可达。

Food food = new Food();
SoftReference<Food> softFood = new SoftReference<>(food);
food = null;//此时通过第一句代码创建的Food对象是软引用可达的,假设此时JVM中可使用堆内存充足
System.gc();//建议JVM进行GC操作。即使建议被采纳,真的进行了GC操作,由于此时堆内存充足,softFood所引用的对象对应的堆内存也不会被回收
food = softFood.get();//仍然可以获取到原来的对象

3.弱引用(WeakReference)与WeakHashMap

当一个对象在通过对象可达性分析算法被判断为弱引用可达时,若此时发生垃圾回收,不管此时堆内存是否充足,WeakReference所引用的对象对应的堆内存都会被GC尝试回收,至于回收是否成功,通过弱引用并不能得知。

弱引用可达:指向一个对象的所有引用中不包含强引用与软引用且包含弱引用,则被判定为弱引用可达。

Food food = new Food();
WeakReference<Food> weakFood = new WeakReference<>(food);
food = null;//此时通过第一句代码创建的Food对象是弱引用可达的
//建议JVM进行GC操作。假设建议被采纳,真的进行了GC操作,不管此时堆内存是否充足,
//weakFood所引用的对象对应的堆内存都会尝试被回收,至于回收是否成功,通过weakFood并不能得知
System.gc();
food = weakFood.get();//此时get方法返回的对象为null

WeakHashMap:如果map里面的key只有map本身引用时,GC时就会将key对应的Entry清除掉。只有当需要使用的map的key的生命周期是由该key的外部引用而不是由key本身决定时,WeakHashMap才有用处。

WeakHashMap应用举例:用弱引用堵住内存泄漏

4.虚引用(PhantomReference)

当一个对象在通过对象可达性分析算法被判断为弱引用可达时,若此时发生垃圾回收,与弱引用类似,不管此时堆内存是否充足,PhantomReference所引用的对象对应的堆内存都会被GC尝试回收。与弱引用、软引用不同,虚引用在创建的时候必须为其提供一个相应的ReferenceQueue(弱引用、软引用是可选的),我们可以通过一个虚引用被加入引用队列来得知此虚引用引用的对象对应的堆内存满足回收条件,在本次或下次GC中可以被回收。

虚引用可达:指向一个对象的所有引用中不包含强引用、软引用、弱引用,且包含虚引用,则被判定为虚引用可达。

ReferenceQueue<Object> queue = new ReferenceQueue<>();
Food food = new Food();
PhantomReference<Food> phantomFood = new PhantomReference<>(food, queue);
food = null;//此时通过第一句代码创建的Food对象是虚引用可达的
System.gc();//建议JVM进行GC操作。假设建议被采纳,真的进行了GC操作,不管此时堆内存是否充足,phantomFood所引用的对象对应的堆内存都会尝试被回收
Thread.sleep(10000);
if((phantomFood = queue.poll()) != null) {
	//第三句代码中创建的虚引用入队代表此虚引用引用的对象对应的堆内存此时可以被回收

	//在当前对象确定满足被回收条件之后可以进行一些其它操作,比如food对象比较大,为了防止内存泄漏,我们想做到在前一个food对象确定可以被GC回收时才创建下一个对象
	···

	phantomFood.clear();//对于虚引用,要手动调用clear方法确保其引用的对象对应的堆内存可以真正被回收
} 

PhantomReference应用举例:当我们需要监测一个对象在内存中的状态,希望在对象确定可以从内存中移除时获得一个通知以进行一些相关操作(比如加载下一个对象)时可以使用虚引用。在对象连接池中,还可通过虚引用来控制连接池中连接的数量。

5.什么时候才能确定一个对象占用的堆内存是可被回收的?(参考深入理解JVM)

在JVM垃圾回收过程中,即使是在可达性分析算法中不可达的对象,也并非是”非死不可”的,这时候它们暂时处于”缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它,这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是:如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统的奔溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()方法中成功拯救自己,只需重新与引用链上的任何一个对象建立关联即可,比如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收“的集合,如果对象这时候还没有逃脱,那基本上它就真的被回收了。也就是说,在两次标记过后仍然处于”即将回收”集合中的对象基本可以确定其占用的堆内存是可被回收的。

简言之,在回收一个对象的堆内存过程中可以分为两种状态:

  • 等待执行此对象的finalize方法,此时不确定此对象对应的堆内存是否可被回收
  • 此对象的finalize方法被执行完成,对象对应的堆内存处于可回收状态

对象自我拯救举例:

public class SaveSelfInFinalize {

    private static SaveSelfInFinalize SAVE_HOOK = null;

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new SaveSelfInFinalize();

        //第一次GC,在finalize方法中SAVE_HOOK对象会自救,重新关联到GC Roots
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(1000);//finalize方法优先级很低,暂停1s以等待其执行
        isAlive(SAVE_HOOK);

        //第二次GC,由于SAVE_HOOK对象的finalize方法已经被触发过一次,本次不会再触发
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(1000);//finalize方法优先级很低,暂停1s以等待其执行
        isAlive(SAVE_HOOK);
    }

    private static void isAlive(Object o) {
        if (o != null)
            System.out.println("yes, alive");
        else
            System.out.println("no, dead");
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            System.out.println("finalize executed!");
            SAVE_HOOK = this;
        } finally {
            super.finalize();
        }
    }

}

输出结果:
	finalize executed!
	yes, alive
	no, dead

由输出结果可以看出,SAVE_HOOK对象尝试在finalize方法中自救,且第一次自救确实成功了,但第二次自救失败了,这是因为任何一个对象的finalize方法只会被系统自动调用一次。

6.软引用、弱引用、虚引用分别在什么时候可以加入ReferenceQueue?

在创建软、弱、虚引用对象时我们都可以通过相应构造函数为其提供一个相关联的引用队列,并且对于虚引用来说,此引用队列还是必须要提供的,那么我们提供的ReferenceQueue有什么作用呢?

接下来从Reference类的四种状态说起来解释ReferenceQueue的作用。在Reference的相关源码中我们可以看到Reference有四种状态,分别为Active,Pending,Enqueued,Inactive。

  • Active:一个Reference对象在刚创建时处于Active状态,当GC检测到Reference所指向的referent可达性发生变化时,如果此Reference对象创建时关联了一个ReferenceQueue对象,则将其变为Pending状态并将其加入Reference类中的static Pending队列;否则直接变为Inactive状态。
  • Pending:由JVM维护Pending list,由ReferenceHandler线程的tryHandlePending()方法处理Pend list的Reference完成其入队(即将此Reference填入其对应的ReferenceQueue)操作,如果此时入队的Reference是一个Cleaner类型的对象,还会执行其对应的clean方法。
  • Enqueued:当一个引用入队之后,我们就可以通过相应的ReferenceQueue中将其取出,取出之后此Reference对象的状态就变为Inactive。一个引用只会入队一次。
  • Inactive:一旦引用对象处于这个状态,其状态就不会再改变。

由Pending状态变为Enqueued状态时,软引用与弱引用经历的过程大致相同,虚引用与此二者有所不同。当GC检测到一个对象处于软可达或者弱可达状态时,会先对指向此对象的所有软引用或弱引用执行clear方法(执行此clear方法的作用是将此引用中的referent对象置为Null)之后才将此引用加入到Pending list,再由tryHandler线程将其加入到与其对应的ReferenceQueue中;而虚引用在将其加入到对应的Pending list中时并没有执行clear方法,也就是说由Pending list转换到ReferenceQueue中的虚引用的referent对象仍然指向堆中的实际对象。

当一个对象被确定为软引用或弱引用可达时,在对此对象对应的所有软引用或弱引用执行了clear方法之后才会执行此对象的finalize方法,但是一个软引用或弱引用可达的对象其finalize方法的执行顺序与其入队的顺序是不确定的;当一个对象被确定为虚引用可达时,只有在其finalized方法被执行之后,此对象对应的虚引用才会加入到相应的引用队列,并且这些虚引用对应的clear方法并没有被执行。

在上文的叙述中曾经提到软引用与弱引用可达的对象在堆内存不足的时候都会被尝试回收,但是回收是否成功通过此软引用或弱引用我们并不能得知;通过上面的解释我们可以知道这个原因:软引用与弱引用在referent对象的finalize方法执行之前就会变为Inactive状态,但是如果referent对象在finalize方法中完成自救,此referent对象是不会被回收的。而被判定为虚引用可达的对象,在此对象的所有虚引用被加入到相应的引用队列并执行了其对应的clear方法之后,此对象一定是可被回收成功的。因为虚引用是在其对应的referent对象的finalize方法执行之后才会加入到相应的引用队列,只要在虚引用中执行clear方法清除此虚引用对referent对象的引用,就可以确保对referent对象的GC一定会成功。也就是说虽然由虚引用被加入到对应的引用队列可以判断其对应的referent对象对应的堆内存处于可回收状态,但是如果我们不对此虚引用手动执行clear方法,此referent对象对应的堆内存只有等到其对应的所有的虚引用都不可达时才会被回收的,这一点如果不注意的话可能引起内存泄漏问题。

由虚引用的这个特性我们可以使用虚引用来更新对象销毁状态,比如确定某个大对象可被回收之后才加载下一个大对象。而采用软引用或弱引用来跟踪对象的销毁状态是不靠谱的,因为由软引用与弱引用并不能得知其referent对象对应的堆内存是否可被回收。

在上文的叙述中还提到过一个引用只会进入其对应的队列一次,这里就又产生了一个疑问;对于软引用与弱引用来说这种情况我们可以理解,当软引用与弱引用入队之前其clear方法一定会被JVM调用,入队之后其所指向的referent对象为空,即使此次GC其对应的referent没有被回收成功,下次GC的时候此引用也不会再接收到系统的通知;但是当一个对象被确定为虚引用可达并且已经将其加入到了对应的引用队列,我们知道此时虚引用仍然持有对referent对象的引用;如果我们不将其从队列中取出,或者将其从队列中取出却不执行clear方法,在本次GC时此referent对应的堆内存是不会被回收的;那么在下次GC时,此referent对象对应的虚引用是否会被再次加入到引用队列呢?答案是不会,具体见下面的ReferenceQueue类对应的源码:

private static class Null<S> extends ReferenceQueue<S> {
	//覆盖父类的enqueue方法
    boolean enqueue(Reference<? extends S> r) {
        return false;
    }
}
static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();
//引用入队相关代码
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
    synchronized (lock) {
        // Check that since getting the lock this reference hasn't already been
        // enqueued (and even then removed)
        ReferenceQueue<?> queue = r.queue;
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        assert queue == this;
		//重点在这一句,入队之后会将此引用持有的队列引用置为Reference.ENQUEUED,此Reference之后再次调用enqueue方法尝试入队
		//调用的实际是子类Null的enqueue方法,会直接返回false
        r.queue = ENQUEUED;
        r.next = (head == null) ? r : head;
        head = r;
        queueLength++;
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(1);
        }
        lock.notifyAll();
        return true;
    }
}
//引用出队相关代码
public Reference<? extends T> poll() {
    if (head == null)
        return null;
    synchronized (lock) {
        return reallyPoll();//实际调用reallyPoll方法完成出队操作
    }
}
private Reference<? extends T> reallyPoll() {       /* Must hold lock */
    Reference<? extends T> r = head;
    if (r != null) {
        head = (r.next == r) ?
            null :
            r.next; // Unchecked due to the next field having a raw type in Reference
		//这句代码是重点,引用出队之后会将其持有的引用队列置为ReferenceQueue.NULL,下次再次尝试入队时调用的实际是
		//子类Null的enqueue方法,会直接返回false
        r.queue = NULL;
        r.next = r;
        queueLength--;
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(-1);
        }
        return r;
    }
    return null;
}

综上所述,一个reference只会入队一次。

7.为什么每次调用phantomFood.get()方法返回结果都为null上文却还说虚引用即使被加入引用队列之后还持有对referent的引用?

每次调用虚引用的get()方法返回结果都为null是由于虚引用覆盖了父类的get()方法使得每次返回结果为null,这是为了保证虚引用存在的意义:跟踪对象的销毁状态,而不是由虚引用获得其对应的referent对象。

虚引用get方法为null并不能代表其持有的对referent对象的引用为null,通过以下代码可以证实这一点:

class TestFinalize {

    private String name;

    public TestFinalize(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}

public class PhantomReferenceAndFinalize {

    private final static ReferenceQueue<TestFinalize> queue = new ReferenceQueue<>();

    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        TestFinalize tf1 = new TestFinalize("tf1");
        PhantomReference<TestFinalize> pr = new PhantomReference<>(tf1, queue);
        System.out.println("before gc; pr: " + pr.getClass().getName() + " " + pr.hashCode());
        System.out.println("before gc; tf1: " + tf1.getClass().getName() + " " + tf1.hashCode());
        tf1 = null;
        System.gc();
        Thread.sleep(5000);
        Object pr1;
        if ((pr1 = queue.poll()) != null) {
            System.out.println("after gc; pr1: " + pr1.getClass().getName() + " " + pr1.hashCode());
        }
        Field referent = Reference.class.getDeclaredField("referent");
        referent.setAccessible(true);
        Object o = referent.get(pr1);
        if (o != null) {
            System.out.println("after gc; o: " + o.getClass().getName() + " " + o.hashCode());
        }
        System.out.println("pr1 == pr ? " + (pr1 == pr));
    }

}
//通过输出结果证明虚引用入队之后仍持有对referent对象的引用
输出结果:
	before gc; pr: java.lang.ref.PhantomReference 2001049719
	before gc; tf1: com.IOTest.TestFinalize 1528902577
	after gc; pr1: java.lang.ref.PhantomReference 2001049719
	after gc; o: com.IOTest.TestFinalize 1528902577
	pr1 == pr ? true

8.引用优先级问题

从最强到最弱,不同级别的可达性反映了一个对象的生命周期。

  • 强可达:不通过Reference类对象就可以被某些线程获得的对象定义为强可达,一个新创建的对象在创建它的线程中就属于强可达的。
  • 软可达:一个对象不是强可达,但是在可达性分析过程中有指向此对象的软引用。
  • 弱可达:一个对象既不是强可达也不是软可达,但是有指向此对象的弱引用。
  • 虚可达:除了虚引用外没有任何其他类型引用。

举例来说,如果一个对象没有指向它的强引用,但是同时包含指向它的软引用、弱引用、虚引用,那么此时认为它是软引用可达的,可认为此对象在内存充足的情况下不会被回收。

9.为什么使用虚引用而不使用finalize方法来跟踪对象的销毁状态?

  • finalize方法中对象可以进行自救,finalize方法执行后不能确定对象占据的堆内存可以被回收,通过虚引用可以做到。
  • finalize中不建议进行耗时操作,而且虚拟机不保证finalize方法可以正常运行完成。这是因为如果由于程序员的编程错误引起一个对象的finalize方法中执行缓慢,甚至发生了死循环,可能导致F-Queue中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

10.使用Java提供的通过虚引用来自动清理堆外内存的方式靠谱吗?虚引用可以用来进行资源清理吗?

当我们通过ByteBuffer的allocateDirect()方法来分配内存时,方法内部会判断当前分配出去的直接内存区域大小是否达到了设定的阈值(通过-XX:MaxDirectMemorySize=2560m 设置),如果达到了,则调用System.gc()方法建议JVM进行一次GC,由于每个分配出去的DirectBuffer对象都关联了一个Cleaner对象,所以如果在GC时DirectBuffer对象是虚可达的,其对应的直接内存就会被回收。这个过程中需要注意几个问题:

  1. 必须在DirectByteBuffer对象是虚可达的时候其对应的直接内存才可被回收,如果程序员编程时不注意这个问题很可能造成直接内存区溢出。
  2. System.gc()仅仅是建议进行gc,实际是否执行不能确定。如果在程序执行时手动指定-XX:+DisableExplicitGC参数,调用System.gc()不会有任何作用。
  3. 如果堆内存一直充足,那么可能每次进行的都是minor gc,就有可能存在某些DirectByteBuffer对象熬过多次minor gc后进入老年代,之后如果这些老年代的对象是虚引用可达的话,仅仅进行minor gc是不能回收其对应的直接内存的。这样就会导致老年代中可能存在已死的DirectByteBuffer对应的直接内存不能释放。

综上所述,这种自动清理堆外内存的方式不一定靠谱,与之对应的,我们也可以通过手动方法在DirectBuffer对象使用完成后手动回收其对应的直接内存。手动回收与自动回收调用的实际都是DirectByteBuffer关联的Cleaner的clean方法。手动回收示例代码如下:

ByteBuffer bb = ByteBuffer.allocateDirect(1024 * 1024 * 8);
((DirectBuffer) bb).cleaner().clean();

虚引用主要作用是通过其得知对象可以被回收,即跟踪对象的销毁状态。比如我们加载某种大对象,内存中限制最多加载10个,可以为每个大对象关联一个虚引用,当通过虚引用得知大对象对应的内存确定可被回收时才加载下一个大对象,这样可以防止由于内存泄漏(我们在程序中将某个大对象的引用置为NULL,以为没有指向此大对象的引用了,但是在某处可能还存在)引起的OOM。

虚引用也可以用来做资源清理,但是仅限于程序员十分清楚这些资源可以在垃圾回收时才被清理,对于常用资源清理(如文件描述符,直接内存)最好还是手动清理,否则可能出现还没有发生GC,已经没有可用的文件描述符的情况。

还可以把虚引用应用对象连接池中,通过虚引用来控制连接池中连接的数量。


Java DirectBuffer对应的堆外内存释放源码分析及自写程序模拟其释放过程

1、DirectByteBuffer对应的堆外内存释放源码分析

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{
	···
	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);
	    }
	
	}

	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));
		//当检测到分配出去的直接内存大小达到阈值时,在reserveMemory方法中调用System.gc建议回收已经不用的直接内存
	    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;
	    }
		//为此DirectBuffer对象关联一个Cleaner,在此对象虚可达时调用cleaner对象的clean方法回收直接内存
		//回收内存的具体操作由Deallocator的run方法指定
	    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
	    att = null;
	}
	···
}

//Cleaner类是PhantomReference类的子类,当referent虚可达时,它的clean方法会被调用,执行指定的任务
public class Cleaner extends PhantomReference<Object> {
	···
}
//当将一个Reference类对象由Pending状态变为Enqueued状态时,如果此Reference类对象是一个Cleaner类的对象,ReferenceHandler线程中运行的tryHandlePending()方法调用Cleaner的clean方法完成堆外内存的清理操作。
public abstract class Reference<T> {
	static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // unlink 'r' from 'pending' chain
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    if (waitForNotify) {
                        lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }

        // Fast path for cleaners
        if (c != null) {
            c.clean();
            return true;
        }

        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }
}

由于在创建DirectByteBuffer类型对象的时候将其与一个Cleaner类对象相关联,所以当GC时如果检测到此对象是虚引用可达并尝试将其入队,就会调用Cleaner类型的clean方法进行堆外直接内存清理。

2、自写程序模拟Cleaner实现堆外内存释放

public class UnsafeUtil {
    private static class UnsafeHolder {
        private static Unsafe unsafe;

        static {
            Field field = Unsafe.class.getDeclaredFields()[0];
            field.setAccessible(true);
            try {
                unsafe = (Unsafe) field.get(null);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static Unsafe getUnsafeInstance() {
        return UnsafeHolder.unsafe;
    }
}

public class MyCleaner {
    private static ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
    private static Map<Reference<Object>, Runnable> refMap = new ConcurrentHashMap<>();

    static {
        new Thread(new CleanerThread()).start();
    }

    public static void makeCleanRelation(Object o, Runnable task) {
        Reference<Object> ref = new PhantomReference<>(o, refQueue);
        refMap.put(ref, task);
    }

    private static class CleanerThread implements Runnable {
        private Reference<Object> tempRef;

        @Override
        public void run() {
            while (true) {
                if ((tempRef = (Reference<Object>) refQueue.poll()) != null) {
					tempRef.clear();//clear之后或者tempRef不可达时,虚引用指向的referent对象对应的堆内内存才能被回收。
                    Runnable task = refMap.remove(tempRef);
                    new Thread(task).start();
                }
            }
        }
    }
}

public class FreeMemoryTask implements Runnable {
    private long address = 0;

    public FreeMemoryTask(long address)
    {
        this.address = address;
    }

    @Override
    public void run()
    {
        System.out.println("runing FreeMemoryTask");
        if (address == 0)
        {
            System.out.println("already released");
        } else
        {
            UnsafeUtil.getUnsafeInstance().freeMemory(address);
        }
    }
}

public class Test {
    private static final int _1MB = 1024 * 1024;
    private long address = 0;

    public Test() {
        address = UnsafeUtil.getUnsafeInstance().allocateMemory(8 * _1MB);
    }

    public static void main(String[] args) {
        while (true) {
            System.out.println("分配内存");
            Test test = new Test();
            MyCleaner.makeCleanRelation(test, new FreeMemoryTask(test.address));
            test = null;
            System.gc();
        }
    }
}

执行程序可以看到虽然不断通过Unsafe类分配直接内存,但是并没有发生OOM,证明我们的内存回收是有效果的。

遗留问题:直接通过Unsafe类分配堆外内存,设置最大堆外直接内存jvm参数是无用的吗???

(完)