由Reference展开的学习

在阅读Thinking in Java的Containers in depth一章中的Holding references时,提到了一个工具包java.lang.ref,说这是个为Java垃圾回收提供了很大的灵活性的包。

并引出了抽象类Reference还有它的三个子类,书上看了好几次都一脸懵逼……最后百度了很久现在简单记录总结一下。

一、为什么要有这个Reference类呢?

一般的对象在程序中,都是可获取的,也就是有直接引用的,或者用更专业的词(等等会介绍)就是强引用的对象。一般对于这样可以获得的对象,JVM的垃圾回收器Garbage Collection(简称gc)是不会回收它的内存的。但当一个对象是不可获得的,或者说没有指向它的引用的时候,gc会回收这个对象的内存。

但如果有些这样的情况,我们想继续持有对这个对象的引用,希望以后能继续访问这个对象,但同时又希望gc能在内存紧缺或正确的时候回收它的内存。这个时候就轮到我们的Reference类登场了。

这个类可以看作是一个普通的引用和你的对象之间的一个代理。注意,如果你想用Reference达到上面说的效果,这个被代理引用的对象,除了现在这个Reference对象包装的引用,不能有其他的普通引用(其他强引用),否则JVM发现后不会在正确的时候对它进行回收的。

二、Reference类

Java中的引用有四种,这里先简单提一下:强引用,软引用,弱引用还有虚引用。

强引用在Java中没有对应的类,其他三个引用类型都是Reference类的子类,分别是SoftReference、WeakReference、PhantomReference。

等等还会针对这四个引用进行详细地介绍。

然后我们来看看Reference类里面重要的field:

private T referent;
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;  

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;

referent:

既然要充当某个引用的代理,肯定要包着这个实际对象的引用吧,这个referent就是这个实际对象的引用。

static Reference<Object>和Reference<T>discovered:

这两个field组成了个pending单向链表。pending是static field,所以是Reference类中唯一的一个。Pending为链表的头节点,

discovered为当前的Pending链表中我这个Reference节点的下一个节点的引用。

注意!!这个链表呢是JVM的gc构建的!!  JVM大概是这样的,就gc在准备回收的时候,如果发现了普通引用的代理也就是Reference类的实例有以下特点:

  1. Reference所引用或者说所代理的实际引用指向的那个实际对象,这个实际对象除了这个Reference引用外,不存在其他强引用(普通引用)了。

  2. 这个Reference对象,在创建的时候,构造器中指定了ReferenceQueue。

那么JVM就会把这整个Reference instance放进这个pending链表中。

哎如果不满足条件2,也就是说Reference instance没有指定ReferenceQueue的话,那么这个Reference对象是不会进入Pending链表的,会被直接回收掉。

总之这个Pending和discovered是由JVM进行赋值。

然后这个Pending链表呢,主要是由一个叫做ReferenceHander的线程来处理的,这个线程是JVM的一个内部线程,也是Reference的一个内部嵌套类(static类),它的任务捏,就是把Pending上准备回收内存的实际对象的代理引用Reference instance拿出来,然后放到它自己的private ReferenceQueue中去。

ReferenceQueue queue和next:

这两个组成了一个队列——ReferenceQueue

ReferenceQueue并不是一个链表数据结构,它只持有这个链表的表头对象header,这个链表是由Refence对象里面的next成员变量构建起来的,next也就是链表当前节点的下一个节点(只有next的引用,它是单向链表),所以Reference对象本身就是一个链表的节点。

那么这个队列是干嘛用的呢?

可以看作是实际对象被回收时,作为通知用的。    从刚刚对pending链表的介绍我们知道,ReferenceQueue的数据也就是Reference instance是从Pending链表中由ReferenceHander线程搞过来的。所以通过这个ReferenceQueue队列,你就可以知道哪些代理引用所指的实际对象要被回收了,然后你可以从这个ReferenceQueue中通过Reference拿到要回收的实际对象,然后做些操作。

这里还要介绍两个ReferenceQueue类中的static field:

//当Reference对象创建时没有指定queue或Reference对象已经处于inactive状态
staticReferenceQueue<Object> NULL = new Null<>();

//当Reference已经被ReferenceHander线程从pending队列移到queue里面时
static ReferenceQueue<Object> ENQUEUED = new Null<>();

第一个NULL是一个Null对象,Null不是我们的平常用的null,而是一个ReferenceQueue对象的一个继承了ReferenceQueue的内部类,它重写了入队方法enqueue,这个方法只有一个操作,直接返回 false,也就是这个对列不会存取任何数据,它起到状态标识的作用。

当你构造Reference instance的时候,如果构造器中没有传入ReferenceQueue,或者传入的这个队列为null,那么这个Reference的instance的成员变量queue就会被赋值为ReferenceQueue.NULL。

第二个ENQUEUED也是类似的道理,是个状态标识的变量。一旦这个Reference instance被从Pending链表中拿出来放进它的ReferenceQueue队列中后,这个Reference instance的成员变量queue就会被赋值成ReferenceQueue.ENQUEUED。

Reference的状态及其转换

Reference一共有四个状态:

1.       Active:活动状态,对象存在强引用状态,还没有被回收;

2.       Pending:垃圾回收器将没有强引用的Reference对象放入到pending队列中,等待ReferenceHander线程处理(前提是这个Reference对象创建的时候传入了ReferenceQueue,否则的话对象会直接进入Inactive状态)。也就是马上要回收的对象;

3.       Enqueued:ReferenceHander线程将pending队列中的对象取出来放到ReferenceQueue队列里;

当引用实例被添加到它注册在的引用队列中时,该实例处于Enqueued状态。当某个引用实例被从引用队列中删除后,该实例将从Enqueued状态变为Inactive状态。如果某个引用实例没有注册在一个引用队列中,该实例将永远不会进入Enqueued状态。

4.       Inactive:处于此状态的Reference对象可以被回收,并且其内部封装的对象也可以被回收掉了,
有两个路径可以进入此状态,

  路径一:在创建时没有传入ReferenceQueue的Reference对象,被Reference封装的对象在没有强引用时,指向它的Reference对象会直接进入此状态;

  路径二、此Reference对象经过前面三个状态后,已经由外部从ReferenceQueue中获取到,并且已经处理掉了。

上个状态转换图:

事实上Reference类并没有显示地定义内部状态值,JVM仅需要通过成员queue和next的值就可以判断当前引用实例处于哪个状态:

  • Active:queue为创建引用实例时传入的ReferenceQueue的实例或是ReferenceQueue.NULL;next为null
  • Pending:queue为创建引用实例时传入的ReferenceQueue的实例;next为this(这里其实想了很久不太知道为什么,这个状态不是还没把Reference instance搞到ReferenceQueue里面吗??不理了hh)
  • Enqueued:queue为ReferenceQueue.ENQUEUED;next为队列中下一个需要被处理的实例或是this如果该实例为队列中的最后一个
  • Inactive:queue为ReferenceQueue.NULL;next为this

一个检测一个对象是否被回收的例子:

当我们想检测一个对象是否被回收了,那么我们就可以采用 Reference + ReferenceQueue,大概需要几个步骤:

  • 创建一个引用队列 queue
  • 创建 Refrence 对象,并关联引用队列 queue
  • 在 reference 被回收的时候,refrence 会被添加到 queue 中
//创建一个引用队列  

ReferenceQueue queue = new ReferenceQueue();  

// 创建弱引用,此时状态为Active,并且Reference.pending为空,当前Reference.queue = 上面创建的queue,并且next=null  

WeakReference reference = new WeakReference(new Object(), queue);  

System.out.println(reference);  

// 当GC执行后,由于是弱引用,所以回收该object对象,并且置于pending上,此时reference的状态为PENDING  

System.gc();  

/* ReferenceHandler从pending中取下该元素,并且将该元素放入到queue中,此时Reference状态为ENQUEUED,Reference.queue = ReferenceENQUEUED */  

/* 当从queue里面取出该元素,则变为INACTIVE,Reference.queue = Reference.NULL */  

Reference reference1 = queue.remove();
System.out.println(reference1);

那这个可以用来干什么了?

可以用来检测内存泄露, github 上面 的 leekCanary 就是采用这种原理来检测的。

  1. 监听 Activity 的生命周期
  2. 在 onDestroy 的时候,创建相应的 Refrence 和 RefrenceQueue,并启动后台进程去检测
  3. 一段时间之后,从 RefrenceQueue 读取,若读取不到相应 activity 的 Refrence,有可能发生泄露了,这个时候,再促发 gc,一段时间之后,再去读取,若在从 RefrenceQueue 还是读取不到相应 activity 的 refrence,可以断定是发生内存泄露了
  4. 发生内瘘泄露之后,dump,分析 hprof 文件,找到泄露路径

关于Java的几种引用类型

强引用:

叫StrongReference,

这个引用在Java中没有相应的类与之对应,但是强引用比较普遍Object obj = new Object(); obj就为一个强引用,obj=null后, 该对象可能会被JVM回收。

如果一个对象具有强引用,则垃圾回收器始终不会回收此对象。当内存不足时,JVM情愿抛出OOM异常使程序异常终止也不会靠回收强引用的对象来解决内存不足的问题。

软引用:

SoftReference类

如果一个对象只有软引用,则在内存充足的情况下是不会回收此对象的,但是,在内部不足即将要抛出OOM异常时就会回收此对象来解决内存不足的问题。

看个例子:

public class TestSoftReference {
        private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
        public static void main(String[] args){
            Object obj = new Object();
            SoftReference<Object> sf = new SoftReference(obj,rq);
            System.out.println(sf.get()!=null);
            System.gc();
            obj = null;  //这里就是要让这个对象除了软引用中封装的强引用外,没有别的强引用了
            System.out.println(sf.get()!=null);

        }
    }

运行结果均为:true。

这也就说明了当内存充足的时候一个对象只有软引用也不会被JVM回收。

弱引用:

WeakReference类

WeakReference 基本与SoftReference 类似,只是回收的策略不同。

只要 GC 发现一个对象只有弱引用,则就会回收此弱引用对象。但是由于GC所在的线程优先级比较低,不会立即发现所有弱引用对象并进行回收。只要GC对它所管辖的内存区域进行扫描时发现了弱引用对象就进行回收。

看一个例子:

public class TestWeakReference {
        private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
        public static void main(String[] args) {
            Object obj = new Object();
            WeakReference<Object> wr = new WeakReference(obj,rq);
            System.out.println(wr.get()!=null);
            obj = null;
            System.gc();
            System.out.println(wr.get()!=null);//false,这是因为WeakReference被回收
        }

    }

运行结果为: true 、false

在指向 obj = null 语句之前,Object对象有两条引用路径,其中一条为obj强引用类型,另一条为wr弱引用类型。此时无论如何也不会进行垃圾回收。当执行了obj = null.Object 对象就只具有弱引用,并且我们进行了显示的垃圾回收。因此此具有弱引用的对象就被GC给回收了。

虚引用

PhantomReference类

PhantomReference,即虚引用,虚引用并不会影响对象的生命周期。虚引用的作用为:跟踪垃圾回收器收集对象这一活动的情况。

当GC一旦发现了虚引用对象,则会将PhantomReference对象插入ReferenceQueue队列,而此时PhantomReference对象并没有被垃圾回收器回收,而是要等到ReferenceQueue被你真正的处理后才会被回收。

注意:PhantomReference必须要和ReferenceQueue联合使用,SoftReference和WeakReference可以选择和ReferenceQueue联合使用也可以不选择,这使他们的区别之一。

接下来看一个虚引用的例子。

public class TestPhantomReference {

        private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
        public static void main(String[] args){

            Object obj = new Object();
            PhantomReference<Object> pr = new PhantomReference<Object>(obj, rq);
            System.out.println(pr.get());
            obj = null;
            System.gc();
            System.out.println(pr.get());
            Reference<Object> r = (Reference<Object>)rq.poll();
            if(r!=null){
                System.out.println("回收");
            }
        }
    }

运行结果:null null 回收

根据上面的例子有两点需要说明:

  • PhantomReference的get方法无论在上面情况下都是返回null。这个在PhantomReference源码中可以看到。
  • 在上面的代码中,如果obj被置为null,当GC发现虚引用,GC会将把 PhantomReference 对象pr加入到队列ReferenceQueue中,注意此时pr所指向的对象并没有被回收,在我们现实的调用了 rq.poll() 返回 Reference 对象之后当GC第二次发现虚引用,而此时 JVM 将虚引用pr插入到队列 rq 会插入失败,此时 GC 才会对虚引用对象进行回收。(这一点意思大概就是,其实也和别的引用一样嘛,在referenceQueue中的虚引用被处理后,比如像这样被poll出来后,才会被回收。  这里描述的应该是源码的逻辑吧)

参考文章:

  《一提到Reference 百分之九十九的java程序员都懵逼了》——https://blog.csdn.net/zqz_zqz/article/details/79474314

  《java 源码系列 - 带你读懂 Reference 和 ReferenceQueue》——https://blog.csdn.net/gdutxiaoxu/article/details/80738581

  《java中的Reference 》——https://www.cnblogs.com/zyzl/p/5540248.html

  《Java源码剖析——彻底搞懂Reference和ReferenceQueue》——http://www.wxueyuan.com/blog/articles/2017/12/01/1512114060269.html

  

原文地址:https://www.cnblogs.com/wangshen31/p/10363556.html

时间: 2024-10-28 20:33:50

由Reference展开的学习的相关文章

【转】 C++易混知识点4: 自己编写一个智能指针(Reference Counting)学习auto_ptr和reference counting

这篇文章建大的介绍了如何编写一个智能指针. 介绍: 什么是智能指针?答案想必大家都知道,智能指针的目的就是更好的管理好内存和动态分配的资源,智能指针是一个智能的指针,顾名思义,他可以帮助我们管理内存.不必担心内存泄露的问题.实际上,智能指针是一个行为类似于指针的类,通过这个类我们来管理动态内存的分配和销毁.方便客户端的使用.相比于一般指针,智能指针主要体现在它使用的容易和便捷性. 转载请注明出处: http://blog.csdn.net/elfprincexu 使用一般指针的问题: 一般情况下

ACM课程学习总结

ACM课程学习总结报告 通过一个学期的ACM课程的学习,我学习了到了许多算法方面的知识,感受到了算法知识的精彩与博大,以及算法在解决问题时的巨大作用.此篇ACM课程学习总结报告将从以下方面展开: 学习ACM算法知识之前的具备的知识基础 学习过程及知识梳理 心得体会及收获 一,学习ACM算法知识之前具备的知识基础 在开始这一学期的课程之前,大一上学期及寒假期间我学习了C++标准库中的STL,了解了一些通用操作,各种类型的容器的特性,以及一些算法.关于算法,只学习了一些简单的遍历,递归.并未深入学习

20145301第六周学习总结

20145301第六周学习总结 教材学习内容总结 第十章 输入/输出 10.1 InputStream与OutputStream InputStream与OutputStream  流(Stream)是对「输入输出」的抽象,注意「输入输出」是相对程序而言的  InputStream与OutputStream InputStream.OutStream提供串流基本操作,如果想要为输入/输出的数据做加工处理,则可以使用打包器类.常用的打包器具备缓冲区作用的BufferedOutputStream.B

20145301第五周学习总结

20145301第五周学习总结 教材学习内容总结 第八章 8.1 语法与继承构架 什么是异常?在 Java 编程语言中,异常类定义程序中可能遇到的轻微的错误条件.可以写代码来处理异常并继续程序执行,而不是让程序中断. Java 提供了一种异常处理模型,它使您能检查异常并进行相应的处理.它实现的是异常处理的抓抛模型.使用此模型,您只需要注意有必要加以处理的异常情况.Java 提供的这种异常处理模型,代替了用返回值或参数机制从方法返回异常码的手段. 在 Java 中,所有的异常都有一个共同的祖先 T

神经网络的学习 Neural Networks learing

1.一些基本符号 2.COST函数 ================Backpropagation Algorithm============= 1.要计算的东西 2.向前传递向量图,但为了计算上图的偏导,要用到后向传递算法 3.后向传递算法 4.小题目 ==============Backpropagation Intuition============== 1.前向计算与后向计算很类似 2.仅考虑一个例子,COST函数化简 3.倒着算theta =======Implementation N

如何学习开源框架

最近一段时间在学习SSH框架,学习的时候笔记是写在纸上的,估计只有自己能看懂,最近看了<Struts2技术内幕>,在读到如何学习开源框架时,感觉说的很对,很好,学习方法值得我们借鉴. 如何学习开源框架 正确的学习方法不仅能够事半功倍,也能够使我们更加接近真理.在这里为大家总结了一些学习方法和最佳实践,希望对正在为学习开源框架犯愁的朋友带来一些启示. 阅读.仔细阅读.反复阅读每个开源框架自带的Reference 这是学习框架最为重要,也是最开始最需要做的事情.不幸的是,事实上,绝大多数程序员对此

深度学习入门1

发布这个系列,一来是为了总结自己的学习,二来也是希望给深度学习初学者一些入门的指导.好废话不多说了,我们直接进入主题,这一节先说一下,深度学习发展历程. 1958,感知器(相当于生物的神经元) 1969,Minsky提出感知器模型具有局限性,导致神经网络研究陷入低潮 1980s, 多层神经网络(模拟人类神经元连接) 1986, 反向传播(用于训练模型参数) 1989, 一层隐藏层已经足够,不需要"深'(深度学习模型设置的问题) 2006,  受限玻尔兹曼机出现(提出解决vanish gradie

Python爬取CSDN博客文章

之前解析出问题,刚刚看到,这次仔细审查了 0 url :http://blog.csdn.net/youyou1543724847/article/details/52818339Redis一点基础的东西目录 1.基础底层数据结构 2.windows下环境搭建 3.java里连接redis数据库 4.关于认证 5.redis高级功能总结1.基础底层数据结构1.1.简单动态字符串SDS定义: ...47分钟前1 url :http://blog.csdn.net/youyou1543724847/

5.7新特性

1. 背景 MySQL 5.7在2015-10-21发布了GA版本,即5.7.9,目前小版本已经到了5.7.12.5.7新增了许多新的feature和优化,接下来一个系列,我们就一起来尝尝鲜.首先这次主要是预览feature的变化以及兼容性问题.后面的系列,会针对重要的feature展开来学习. 2 安全相关的特性 2.1 认证插件 mysql.user表中的plugin更改成not null,5.7开始不再支持mysql_old_password的认证插件,推荐全部使用mysql_native