美文网首页
JDK 源码阅读 Reference

JDK 源码阅读 Reference

作者: linux服务器开发 | 来源:发表于2018-09-18 14:28 被阅读24次

Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM会爆出OOME,也不会去回收存在引用的对象。

如果只提供强引用,我们就很难写出“这个对象不是很重要,如果内存不足GC回收掉也是可以的”这种语义的代码。Java在1.2版本中完善了引用体系,提供了4中引用类型:强引用,软引用,弱引用,虚引用。使用这些引用类型,我们不但可以控制垃圾回收器对对象的回收策略,同时还能在对象被回收后得到通知,进行相应的后续操作。

引用与可达性分类

Java目前有4中引用类型:

强引用(Strong Reference):普通的的引用类型,new一个对象默认得到的引用就是强引用,只要对象存在强引用,就不会被GC。

软引用(Soft Reference):相对较弱的引用,垃圾回收器会在内存不足时回收弱引用指向的对象。JVM会在抛出OOME前清理所有弱引用指向的对象,如果清理完还是内存不足,才会抛出OOME。所以软引用一般用于实现内存敏感缓存。

弱引用(Weak Reference):更弱的引用类型,垃圾回收器在GC时会回收此对象,也可以用于实现缓存,比如JDK提供的WeakHashMap。

虚引用(Phantom Reference):一种特殊的引用类型,不能通过虚引用获取到关联对象,只是用于获取对象被回收的通知。

相较于传统的引用计数算法,Java使用可达性分析来判断一个对象是否存活。其基本思路是从GC Root开始向下搜索,如果对象与GC Root之间存在引用链,则对象是可达的。对象的可达性与引用类型密切相关。Java有5中类型的可达性:

强可达(Strongly Reachable):如果线程能通过强引用访问到对象,那么这个对象就是强可达的。

软可达(Soft Reachable):如果一个对象不是强可达的,但是可以通过软引用访问到,那么这个对象就是软可达的

弱可达(Weak Reachable):如果一个对象不是强可达或者软可达的,但是可以通过弱引用访问到,那么这个对象就是弱可达的。

虚可达(Phantom Reachable):如果一个对象不是强可达,软可达或者弱可达,并且这个对象已经finalize过了,并且有虚引用指向该对象,那么这个对象就是虚可达的。

不可达(Unreachable):如果对象不能通过上述的几种方式访问到,则对象是不可达的,可以被回收。

对象的引用类型与可达性听着有点乱,好像是一回事,我们这里实例分析一下:

上面这个例子中,A~D,每个对象只存在一个引用,分别是:A-强引用,B-软引用,C-弱引用,D-虚引用,所以他们的可达性为:A-强可达,B-软可达,C-弱可达,D-虚可达。因为E没有存在和GC Root的引用链,所以它是不可达。

在看一个复杂的例子:

A依然只有一个强引用,所以A是强可达

B存在两个引用,强引用和软引用,但是B可以通过强引用访问到,所以B是强可达

C只能通过弱引用访问到,所以是弱可达

D存在弱引用和虚引用,所以是弱可达

E虽然存在F的强引用,但是GC Root无法访问到它,所以它依然是不可达。

同时可以看出,对象的可达性是会发生变化的,随着运行时引用对象的引用类型的变化,可达性也会发生变化,可以参考下图:

Reference总体结构

Reference类是所有引用类型的基类,Java提供了具体引用类型的具体实现:

SoftReference:软引用,堆内存不足时,垃圾回收器会回收对应引用

WeakReference:弱引用,每次垃圾回收都会回收其引用

PhantomReference:虚引用,对引用无影响,只用于获取对象被回收的通知

FinalReference:Java用于实现finalization的一个内部类

因为默认的引用就是强引用,所以没有强引用的Reference实现类。

Reference的核心

Java的多种引用类型实现,不是通过扩展语法实现的,而是利用类实现的,Reference类表示一个引用,其核心代码就是一个成员变量reference:

1

2

3

4

5

6

7

8

9

10

publicabstractclassReference<T> {

    privateT referent; // 会被GC特殊对待

    // 获取Reference管理的对象

    publicT get() {

        returnthis.referent;

    }

    // ...

}

如果JVM没有对这个变量做特殊处理,它依然只是一个普通的强引用,之所以会出现不同的引用类型,是因为JVM垃圾回收器硬编码识别SoftReference,WeakReference,PhantomReference等这些具体的类,对其reference变量进行特殊对象,才有了不同的引用类型的效果。

上文提到了Reference及其子类有两大功能:

实现特定的引用类型

用户可以对象被回收后得到通知

第一个功能已经解释过了,第二个功能是如何做到的呢?

一种思路是在新建一个Reference实例是,添加一个回调,当java.lang.ref.Reference#referent被回收时,JVM调用该回调,这种思路比较符合一般的通知模型,但是对于引用与垃圾回收这种底层场景来说,会导致实现复杂,性能不高的问题,比如需要考虑在什么线程中执行这个回调,回调执行阻塞怎么办等等。

所以Reference使用了一种更加原始的方式来做通知,就是把引用对象被回收的Reference添加到一个队列中,用户后续自己去从队列中获取并使用。

理解了设计后对应到代码上就好理解了,Reference有一个queue成员变量,用于存储引用对象被回收的Reference实例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

publicabstractclassReference<T> {

    // 会被GC特殊对待

    privateT referent;

    // reference被回收后,当前Reference实例会被添加到这个队列中

    volatileReferenceQueue<? superT> queue;

    // 只传入reference的构造函数,意味着用户只需要特殊的引用类型,不关心对象何时被GC

    Reference(T referent) {

        this(referent, null);

    }

    // 传入referent和ReferenceQueue的构造函数,reference被回收后,会添加到queue中

    Reference(T referent, ReferenceQueue<? superT> queue) {

        this.referent = referent;

        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;

    }

    // ...

}

Reference的状态

Reference对象是有状态的。一共有4中状态:

Active:新创建的实例的状态,由垃圾回收器进行处理,如果实例的可达性处于合适的状态,垃圾回收器会切换实例的状态为Pending或者Inactive。如果Reference注册了ReferenceQueue,则会切换为Pending,并且Reference会加入pending-Reference链表中,如果没有注册ReferenceQueue,会切换为Inactive。

Pending:在pending-Reference链表中的Reference的状态,这些Reference等待被加入ReferenceQueue中。

Enqueued:在ReferenceQueue队列中的Reference的状态,如果Reference从队列中移除,会进入Inactive状态

Inactive:Reference的最终状态

Reference对象图如下:

除了上文提到的ReferenceQueue,这里出现了一个新的数据结构:pending-Reference。这个链表是用来干什么的呢?

上文提到了,reference引用的对象被回收后,该Reference实例会被添加到ReferenceQueue中,但是这个不是垃圾回收器来做的,这个操作还是有一定逻辑的,如果垃圾回收器还需要执行这个操作,会降低其效率。从另外一方面想,Reference实例会被添加到ReferenceQueue中的实效性要求不高,所以也没必要在回收时立马加入ReferenceQueue。

所以垃圾回收器做的是一个更轻量级的操作:把Reference添加到pending-Reference链表中。Reference对象中有一个pending成员变量,是静态变量,它就是这个pending-Reference链表的头结点。要组成链表,还需要一个指针,指向下一个节点,这个对应的是java.lang.ref.Reference#discovered这个成员变量。

可以看一下代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

publicabstractclassReference<T> {

    // 会被GC特殊对待

    privateT referent;

    // reference被回收后,当前Reference实例会被添加到这个队列中

    volatileReferenceQueue<? superT> queue;

    // 全局唯一的pending-Reference列表

    privatestaticReference<Object> pending = null;

    // Reference为Active:由垃圾回收器管理的已发现的引用列表(这个不在本文讨论访问内)

    // Reference为Pending:在pending列表中的下一个元素,如果没有为null

    // 其他状态:NULL

    transientprivateReference<T> discovered;  /* used by VM */

    // ...

}

ReferenceHandler线程

通过上文的讨论,我们知道一个Reference实例化后状态为Active,其引用的对象被回收后,垃圾回收器将其加入到pending-Reference链表,等待加入ReferenceQueue。这个过程是如何实现的呢?

这个过程不能对垃圾回收器产生影响,所以不能在垃圾回收线程中执行,也就需要一个独立的线程来负责。这个线程就是ReferenceHandler,它定义在Reference类中:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

// 用于控制垃圾回收器操作与Pending状态的Reference入队操作不冲突执行的全局锁

// 垃圾回收器开始一轮垃圾回收前要获取此锁

// 所以所有占用这个锁的代码必须尽快完成,不能生成新对象,也不能调用用户代码

staticprivateclassLock { };

privatestaticLock lock = newLock();

privatestaticclassReferenceHandler extendsThread {

    ReferenceHandler(ThreadGroup g, String name) {

        super(g, name);

    }

    publicvoidrun() {

        // 这个线程一直执行

        for(;;) {

            Reference<Object> r;

            // 获取锁,避免与垃圾回收器同时操作

            synchronized(lock) {

                // 判断pending-Reference链表是否有数据

                if(pending != null) {

                    // 如果有Pending Reference,从列表中取出

                    r = pending;

                    pending = r.discovered;

                    r.discovered = null;

                } else{

                    // 如果没有Pending Reference,调用wait等待

                    //

                    // wait等待锁,是可能抛出OOME的,

                    // 因为可能发生InterruptedException异常,然后就需要实例化这个异常对象,

                    // 如果此时内存不足,就可能抛出OOME,所以这里需要捕获OutOfMemoryError,

                    // 避免因为OOME而导致ReferenceHandler进程静默退出

                    try{

                        try{

                            lock.wait();

                        } catch(OutOfMemoryError x) { }

                    } catch(InterruptedException x) { }

                    continue;

                }

            }

            // 如果Reference是Cleaner,调用其clean方法

            // 这与Cleaner机制有关系,不在此文的讨论访问

            if(r instanceofCleaner) {

                ((Cleaner)r).clean();

                continue;

            }

            // 把Reference添加到关联的ReferenceQueue中

            // 如果Reference构造时没有关联ReferenceQueue,会关联ReferenceQueue.NULL,这里就不会进行入队操作了

            ReferenceQueue<Object> q = r.queue;

            if(q != ReferenceQueue.NULL) q.enqueue(r);

        }

    }

}

ReferenceHandler线程是在Reference的static块中启动的:

1

2

3

4

5

6

7

8

9

10

11

12

13

static{

    // 获取system ThreadGroup

    ThreadGroup tg = Thread.currentThread().getThreadGroup();

    for(ThreadGroup tgn = tg;

         tgn != null;

         tg = tgn, tgn = tg.getParent());

    Thread handler = newReferenceHandler(tg, "Reference Handler");

    // ReferenceHandler线程有最高优先级

    handler.setPriority(Thread.MAX_PRIORITY);

    handler.setDaemon(true);

    handler.start();

}

综上,ReferenceHandler是一个最高优先级的线程,其逻辑是从Pending-Reference链表中取出Reference,添加到其关联的Reference-Queue中。

ReferenceQueue

Reference-Queue也是一个链表:

1

2

3

4

publicclassReferenceQueue<T> {

    privatevolatileReference<? extendsT> head = null;

    // ...

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

// ReferenceQueue中的这个锁用于保护链表队列在多线程环境下的正确性

staticprivateclassLock { };

privateLock lock = newLock();

booleanenqueue(Reference<? extendsT> r) { /* Called only by Reference class */

    synchronized(lock) {

        // 判断Reference是否需要入队

        ReferenceQueue<?> queue = r.queue;

        if((queue == NULL) || (queue == ENQUEUED)) {

            returnfalse;

        }

        assertqueue == this;

        // Reference入队后,其queue变量设置为ENQUEUED

        r.queue = ENQUEUED;

        // Reference的next变量指向ReferenceQueue中下一个元素

        r.next = (head == null) ? r : head;

        head = r;

        queueLength++;

        if(r instanceofFinalReference) {

            sun.misc.VM.addFinalRefCount(1);

        }

        lock.notifyAll();

        returntrue;

    }

}

通过上面的代码,可以知道java.lang.ref.Reference#next的用途了:

1

2

3

4

5

6

7

8

9

10

publicabstractclassReference<T> {

    /* When active:   NULL

     *     pending:   this

     *    Enqueued:   指向ReferenceQueue中的下一个元素,如果没有,指向this

     *    Inactive:   this

     */

    Reference next;

    // ...

}

总结

一个使用Reference+ReferenceQueue的完整流程如下:

在这里给大家提供一个学习交流的平台,java架构师群: 867748702

具有1-5工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加群。

在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加群。

如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的可以加群。

________________________________________________________________________________________________

加Java架构师进阶交流群获取Java工程化、高性能及分布式、高性能、深入浅出。高架构。

性能调优、Spring,MyBatis,Netty源码分析和大数据等多个知识点高级进阶干货的直播免费学习权限

都是大牛带飞 让你少走很多的弯路的 群号是: 867748702对了 小白勿进 最好是有开发经验

注:加群要求

1、具有工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加。

2、在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加。

3、如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的,可以加。

4、觉得自己很牛B,一般需求都能搞定。但是所学的知识点没有系统化,很难在技术领域继续突破的可以加。

5.阿里Java高级大牛直播讲解知识点,分享知识,多年工作经验的梳理和总结,带着大家全面、科学地建立自己的技术体系和技术认知!

相关文章

网友评论

      本文标题:JDK 源码阅读 Reference

      本文链接:https://www.haomeiwen.com/subject/ndounftx.html