JAVA中的引用

JDK1.2之后,Java扩充了引用的概念,将引用分为强引用、软引用、弱引用和虚引用四种。

  • 强引用
    类似于”Object a = new Object()”这类的引用,只要垃圾强引用存在,垃圾回收器就不会回收掉被引用的对象。

  • 软引用
    对于软引用关联的对象,在系统将要发生内存溢出异常之前,会把这些对象列入垃圾回收范围中进行回收。如果这次回收还没有足够内存,则抛出内存异常。
    使用SoftReference类实现软引用

软引用的回收策略在不同的JVM实现会略有不同,javadoc中说明:
Virtual machine implementations are, however, encouraged to bias against clearing recently-created or recently-used soft references.
也就是说JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的referent最近使用情况和创建时间来综合决定是否回收该referent。

  • 弱引用
    强度比软引用更弱,被弱引用关联的对象只能存活到下一次垃圾回收发生之前。当发生GC时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
    使用WeakReference类实现弱引用

  • 虚引用
    一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象的实例(PhantomReference的get方法总是返回null)。为一个对象设置虚引用关联的唯一目的就是能够在这个对象被垃圾回收器回收掉后收到一个通知。
    使用PhantomReference类实现虚引用

除了强引用外,Java还引入了SoftReference,WeakReference,PhantomReference,FinalReference ,这些类放在java.lang.ref包下,类的继承体系如下图

Reference类结构.png

用途

WeakReference和SoftReference都可以用来实现cache
PhantomReference用作跟踪垃圾回收

使用

WeakReference

示例:

WeakReference<String> weakString = new WeakReference<>(“abc”);

WeakReference 多与ReferenceQueue一块使用:
对象(“abc”)被回收后,会把弱引用对象(weakString)放入队列ReferenceQueue中

WeakReference继承Reference,其中只有两个构造函数:

public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent) {
        super(referent);
    }
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

WeakReference(T referent, ReferenceQueue<? super T> q):与上面的构造方法比较,多了个ReferenceQueue,在对象被回收后,会把弱引用对象,也就是WeakReference对象或者其子类的对象,放入队列ReferenceQueue中,注意不是被弱引用的对象,被弱引用的对象已经被回收了。

使用示例见:
关于Java中的WeakReference - 简书

Reference && ReferenceQueue

Reference

public abstract class Reference<T> {
    //即Reference所包装的引用对象
    private T referent;         /* Treated specially by GC */

    //ReferenceQueue本身通过链表实现队列,ReferenceQueue对象同时保存了一个Reference类型的head节点,Reference封装了next字段,这样就是可以组成一个单向链表。
    volatile ReferenceQueue<? super T> queue;

    /* When active:   NULL
     *     pending:   this
     *    Enqueued:   next reference in queue (or this if last)
     *    Inactive:   this
     */
    @SuppressWarnings("rawtypes")
    Reference next;

    /* When active:   next element in a discovered reference list maintained by GC (or this if last)
     *     pending:   next element in the pending list (or null if last)
     *   otherwise:   NULL
     */ 
    //由jvm调用
    transient private Reference<T> discovered;  /* used by VM */


    /* List of References waiting to be enqueued.  The collector adds
     * References to this list, while the Reference-handler thread removes
     * them.  This list is protected by the above lock object. The
     * list uses the discovered field to link its elements.
     */
    private static Reference<Object> pending = null;

    ...
 }

Reference对象有四种状态:

  • active
    GC会特殊对待此状态的引用,一旦被引用的对象的可达性发生变化(如失去强引用,只剩弱引用,可以被回收),GC会将引用放入pending队列并将其状态改为pending状态
  • pending
    位于pending队列,等待ReferenceHandler线程将引用入队queue
  • enqueue
    ReferenceHandler将引用入队queue
  • inactive
    引用从queue出队后的最终状态,该状态不可变
Reference对象有四种状态.png

ReferenceQueue

public class ReferenceQueue<T> {

    /**
     * Constructs a new reference-object queue.
     */
    public ReferenceQueue() { }

    private static class Null<S> extends ReferenceQueue<S> {
        boolean enqueue(Reference<? extends S> r) {
            return false;
        }
    }

    static ReferenceQueue<Object> NULL = new Null<>();
    static ReferenceQueue<Object> ENQUEUED = new Null<>();
    private volatile Reference<? extends T> head = null;
}

这两个字段的主要功能:NULL是当我们构造Reference实例时queue传入null时,会默认使用NULL,这样在enqueue时判断queue是否为NULL,如果为NULL直接返回,入队失败。ENQUEUED的作用是防止重复入队,reference后会把其queue字段赋值为ENQUEUED,当再次入队时会直接返回失败。

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

Reference与ReferenceQueue之间是如何工作的呢?

Reference里有个静态字段pending,同时还通过静态代码块启动了Reference-handler thread。当一个Reference的referent被回收时,垃圾回收器会把reference添加到pending这个链表里,然后Reference-handler thread不断的读取pending中的reference,把它加入到对应的ReferenceQueue中。
当reference与referenQueue联合使用的主要作用就是当reference指向的referent回收时,提供一种通知机制,通过queue取到这些reference,来做额外的处理工作。

 static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
...
}

ThreadGroup

使用线程组的好处是可以对这一组的线程进行整体操作。
void setDaemon(boolean daemon)//更改此线程组的后台程序状态。
void setMaxPriority(int pri)//设置线程组的最高优先级

ReferenceHandler类调用tryHandlePending

 /* High-priority thread to enqueue pending References */
    private static class ReferenceHandler extends Thread {
...
        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
    }
 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;
                }
            }
        }
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

PhantomReference

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

PhantomReference的get方法总是返回null,因此无法访问对应的引用对象;
其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

Phantom references are most often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism.
Phantom Reference can be used in situations, where sometime using finalize() is not sensible thing to do.This reference type differs from the other types defined in java.lang.ref Package because it isn’t meant to be used to access the object, but as a signal that the object has already been finalized, and the garbage collector is ready to reclaim its memory.

代码示例:blog.reference.PhantomTest

FinalReference/finalizers

finalizers作用:通过使用finalizers 中FinalizerThread来执行finalize方法()

原理:

实现了object的finalize()的类在创建时会新建一个FinalizerReference,这个对象是强引用类型,实现了finalize()的对象,下面直接叫原对象。原对象没有被其他对象引用时(FinalizeReference除外),执行GC不会马上被清除掉,而是放入一个静态链表中(ReferenceQueue),有一个守护线程专门去维护这个链表,如何维护呢?就是轮到该线程执行时就弹出里面的对象,执行它们的finalize(),对应的FinalizerReference对象在下次执行GC时就会被清理掉。
一个堆的FinalizerReference会组成一条双向链表,垃圾回收器应该会持有链表头(链表头在FinalizerReference中为一个静态成员)。

覆盖了finalize方法的对象至少需要两次GC才可能被回收。第一次GC把覆盖了finalize方法的对象对应的Finalizer reference加入referenceQueue等待FinalizerThread来执行finalize方法。第二次GC才有可能释放finalizee对象本身,前提是FinalizerThread已经执行完finalize方法了,并把Finalizer reference从Finalizer静态unfinalized链表中剔除,因为这个链表和Finalizer reference对finalizee构成的是一个强引用。

Item 8: Avoid finalizers and cleaners
Java的Finalizer引发的内存溢出 - 冰花ぃ雪魄 - 博客园

问题

为什么会泄漏?
直接原因就是守护线程优先级比较低(Thread.MAX_PRIORITY- 2),运行的时间比较少。如果较短时间内创建较多的原对象,就会因为守护线程来不及弹出原对象而使FinalizerReference和原对象都得不到回收。无论怎样调用GC都没有用的,因为只要原对象没有被守护线程弹出执行其finalize()方法,FinalizerReference对象就不会被GC回收。

引申问题:
1、private static Finalizer unfinalized= null; //做什么用?
unfinalized: 维护了一个未执行finalize方法的reference链表。维护静态字段unfinalized的目的是为了一直保持对未执行finalize方法的reference的强引用,防止被gc回收掉。
第一次GC时,会把该finalizee对应的reference放到Finalizer的refereneQueue中;
接着,FinalizerThread来执行finalizee的finalize方法,并把当前Finalizer从unfinalized中剔除。
当下一次GC发生时,由于unfinalized已经不再持有该对象的referent,故该对象被直接回收掉。

2、finalizer.setPriority(Thread.MAX_PRIORITY- 2);//为什么守护线程优先级比较低?
每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。
并非线程优先级越高的就一定先执行,哪个线程的先运行取决于CPU的调度;
默认情况下main线程具有普通的优先级,而它创建的线程也具有普通优先级。
Thread对象的setPriority(int x)和getPriority()来设置和获得优先级。
MAX_PRIORITY :值是10
MIN_PRIORITY :值是1
NORM_PRIORITY :值是5(主方法默认优先级)

执行过程

  1. 对象初始化时(构造函数返回之前调用)调用register方法,register方法会把本对象封装为Finalizer放入静态unfinalized链表中。
    JVM通过VM参数 RegisterFinalizersAtInit 的值来确定何时调用register,RegisterFinalizersAtInit默认为true,则会在构造函数返回之前调用call_register_finalizer方法。如果通过-XX:-RegisterFinalizersAtInit 设为false,则会在对象空间分配好之后就调用call_register_finalizer。
  2. JVM在执行GC时,实现了finalize()的对象没有被其他对象引用时(FinalizeReference除外),会把该finalizee对应的reference放到Finalizer的refereneQueue中,等待FinalizerThread来执行finalizee的finalize方法。
  3. 下次GC时,finalizee对象才能被GC回收。
final class Finalizer extends FinalReference<Object> { 
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private static Finalizer unfinalized = null;
    private static final Object lock = new Object();

    /* Invoked by VM */
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        add();
    }
    private void add() {
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }
private static class FinalizerThread extends Thread {
        private volatile boolean running;
        FinalizerThread(ThreadGroup g) {
            super(g, "Finalizer");
        }
        public void run() {
            if (running)
                return;
            // Finalizer thread starts before System.initializeSystemClass
            // is called.  Wait until JavaLangAccess is available
            while (!VM.isBooted()) {
                // delay until VM completes initialization
                    VM.awaitBooted();
            }
            final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
            running = true;
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue  //finalize方法抛异常时不处理
                }
            }
        }
    }
private void runFinalizer(JavaLangAccess jla) {
    synchronized (this) {
        if (hasBeenFinalized()) return;
        remove();
    }
    try {
        Object finalizee = this.get();
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
            jla.invokeFinalize(finalizee);

            /* Clear stack slot containing this variable, to decrease
               the chances of false retention with a conservative GC */
            finalizee = null;
        }
    } catch (Throwable x) { }
    super.clear();
}

其中:
// Finalizer thread starts before System.initializeSystemClass is called. Wait until JavaLangAccess is available
表示System.initializeSystemClass初始后sun.misc.VM#booted=true,才往下执行,否则等待。
java中System类简介(转) - 沧海一滴 - 博客园
使用JavaLangAccess和SharedSecrets来获取JVM中的实例 - yums467的专栏 - CSDN博客

如何执行finalize方法的?
sun.misc.SharedSecrets#getJavaLangAccess 用于获取系统初始化后的对象
sun.misc.JavaLangAccess#invokeFinalize用于执行参数对象的finalize()方法
原理是:
java.lang.System中,静态初始化registerNatives()方法时,会调用 initializeSystemClass方法,进而执行setJavaLangAccess方法;setJavaLangAccess方法会对sun.misc.SharedSecrets.javaLangAccess赋对象,对象中invokeFinalize(Object o)的实现就是执行o.finalize()方法。

public final class System {

    /* register the natives via the static initializer.
     *
     * VM will invoke the initializeSystemClass method to complete
     * the initialization for this class separated from clinit.
     * Note that to use properties set by the VM, see the constraints
     * described in the initializeSystemClass method.
     */
    private static native void registerNatives();
    static {
        registerNatives();
    }
   private static void initializeSystemClass() {
        // register shared secrets
        setJavaLangAccess();
          ...
        // Subsystems that are invoked during initialization can invoke
        // sun.misc.VM.isBooted() in order to avoid doing things that should
        // wait until the application class loader has been set up.
        // IMPORTANT: Ensure that this remains the last initialization action!
        sun.misc.VM.booted();
    }

java.lang.System#setJavaLangAccess

private static void setJavaLangAccess() {
        // Allow privileged classes outside of java.lang
        sun.misc.SharedSecrets.setJavaLangAccess(new sun.misc.JavaLangAccess(){
            public sun.reflect.ConstantPool getConstantPool(Class<?> klass) {
                return klass.getConstantPool();
            }
              ......
            public void invokeFinalize(Object o) throws Throwable {
                o.finalize();
            }
        });
    }

参考

转Java 的强引用、弱引用、软引用、虚引用 - gudi - 博客园

理解Java-Reference - 简书
Java Reference详解 - robin-yao的个人页面 - 开源中国

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 据说,生日的阳历与阴历重叠周期是19年,我刚好是第二次重叠,屈指一算,已经与不惑之年相望。 30岁以后,对年龄数字...
    今小汐阅读 1,101评论 14 19
  • 小时候看玛丽苏电视剧,里面有一个男主的镜头,倒比他给女主承包全世界的行为,更让我印象深刻。 霸道总裁给小白兔女主布...
    WaitForWhy阅读 637评论 0 1
  • 系统有前台用户操作界面 系统还有后台供公司内部人员使用维护平台两者在使用的过程中就是对"控制器","视图模版","...
    张金宇阅读 160评论 0 0