详解java中Reference的实现与相应的执行过程
一、reference类型(除强引用)
可以理解为reference的直接子类都是由jvm定制化处理的,因此在代码中直接继承于reference类型没有任何作用.只能继承于它的子类,相应的子类类型包括以下几种.(忽略没有在java中使用的,如jnireference)
softreference
weakreference
finalreference
phantomreference
上面的引用类型在相应的javadoc中也有提及.finalreference专门为finalize方法设计,另外几个也有特定的应用场景.其中softreference用在内存相关的缓存当中,weakreference用在与回收相关的大多数场景.phantomreference用在与包装对象回收回调场景当中(比如资源泄漏检测).
可以直接在ide中查看几个类型的子类信息,即可了解在大多数框架中,都是通过继承相应的类型用在什么场景当中,以便于我们实际进行选型处理.
二、reference构造函数
其内部提供2个构造函数,一个带queue,一个不带queue.其中queue的意义在于,我们可以在外部对这个queue进行监控.即如果有对象即将被回收,那么相应的reference对象就会被放到这个queue里.我们拿到reference,就可以再作一些事务.
而如果不带的话,就只有不断地轮训reference对象,通过判断里面的get是否返回null(phantomreference对象不能这样作,其get始终返回null,因此它只有带queue的构造函数).这两种方法均有相应的使用场景,取决于实际的应用.如weakhashmap中就选择去查询queue的数据,来判定是否有对象将被回收.而threadlocalmap,则采用判断get()是否为null来作处理.
相应的构造函数如下所示:
reference(t referent) { this(referent, null); } reference(t referent, referencequeue<? super t> queue) { this.referent = referent; this.queue = (queue == null) ? referencequeue.null : queue; }
这里面的null队列,即可以理解为不需要对其队列中的数据作任何处理的队列.并且其内部也不会存取任何数据.
在上面的对象中,referent表示其引用的对象,即我们在构造的时候,需要被包装在其中的对象.对象即将被回收的定义即此对象除了被reference引用之外没有其它引用了(并非确实没有被引用,而是gcroot可达性不可达,以避免循环引用的问题).
queue即是对象即被回收时所要通知的队列,当对象即被回收时,整个reference对象(而不是被回收的对象)会被放到queue里面,然后外部程序即可通过监控这个queue拿到相应的数据了.
三、referencequeue及reference引用链
这里的queue名义上是一个队列,但实际内部并非有实际的存储结构,它的存储是依赖于内部节点之间的关系来表达.可以理解为queue是一个类似于链表的结构,这里的节点其实就是reference本身.可以理解为queue为一个链表的容器,其自己仅存储当前的head节点,而后面的节点由每个reference节点自己通过next来保持即可.
reference状态值
每个引用对象都有相应的状态描述,即描述自己以及包装的对象当前处于一个什么样的状态,以方便进行查询,定位或处理.
1、active:活动状态,即相应的对象为强引用状态,还没有被回收,这个状态下对象不会放到queue当中.在这个状态下next为null,queue为定义时所引用的queue.
2、pending:准备放入queue当中,在这个状态下要处理的对象将挨个地排队放到queue当中.在这个时间窗口期,相应的对象为pending状态.不管什么reference,进入到此状态的,即可认为相应的此状态下,next为自己(由jvm设置),queue为定义时所引用的queue.
3、enqueued:相应的对象已经为待回收,并且相应的引用对象已经放到queue当中了.准备由外部线程来询循queue获取相应的数据.此状态下,next为下一个要处理的对象,queue为特殊标识对象enqueued.
4、inactive:即此对象已经由外部从queue中获取到,并且已经处理掉了.即意味着此引用对象可以被回收,并且对内部封装的对象也可以被回收掉了(实际的回收运行取决于clear动作是否被调用).可以理解为进入到此状态的肯定是应该被回收掉的.
jvm并不需要定义状态值来判断相应引用的状态处于哪个状态,只需要通过计算next和queue即可进行判断.
四、referencequeue#head
始终保存当前队列中最新要被处理的节点,可以认为queue为一个后进先出的队列.当新的节点进入时,采取以下的逻辑
newe.next = head;head=newe;
然后,在获取的时候,采取相应的逻辑
tmp = head;head=tmp.next;return tmp;
五、reference#next
即描述当前引用节点所存储的下一个即将被处理的节点.但next仅在放到queue中才会有意义.为了描述相应的状态值,在放到队列当中后,其queue就不会再引用这个队列了.而是引用一个特殊的enqueued.因为已经放到队列当中,并且不会再次放到队列当中.
六、reference#referent
即描述当前引用所引用的实际对象,正如在注解中所述,其会被仔细地处理.即此什么什么时候会被回收,如果一旦被回收,则会直接置为null,而外部程序可通过通过引用对象本身(而不是referent)了解到,回收行为的产生.
七、referencequeue#enqueue 待处理引用入队
此过程即在reference对象从active->pending->enqued的过程. 此方法为处理pending状态的对象为enqued状态.相应的过程即为之前的逻辑,即将一个节点入队操作,相应的代码如下所示.
r.queue = enqueued; r.next = (head == null) ? r : head; head = r; queuelength++; lock.notifyall();
最后的nitify即通知外部程序之前阻塞在当前队列之上的情况.(即之前一直没有拿到待处理的对象)
八、reference#tryhandlepending
即处理reference对象从active到pending状态的变化.在reference对象内部,有一个static字段,其相应的声明如下:
/* 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;
可以理解为jvm在gc时会将要处理的对象放到这个静态字段上面.同时,另一个字段discovered,表示要处理的对象的下一个对象.即可以理解要处理的对象也是一个链表,通过discovered进行排队,这边只需要不停地拿到pending,然后再通过discovered不断地拿到下一个对象即可.因为这个pending对象,两个线程都可能访问,因此需要加锁处理.
相应的处理过程如下所示:
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; } //将处理对象入队,即进入到enqued状态 referencequeue<? super object> q = r.queue; if (q != referencequeue.null) q.enqueue(r);
九、reference#clear
清除引用对象所引用的原对象,这样通过get()方法就不能再访问到原对象了.从相应的设计思路来说,既然都进入到queue对象里面,就表示相应的对象需要被回收了,因为没有再访问原对象的必要.此方法不会由jvm调用,而jvm是直接通过字段操作清除相应的引用,其具体实现与当前方法相一致.
clear的语义就是将referent置null.
weakreference对象进入到queue之后,相应的referent为null.
softreference对象,如果对象在内存足够时,不会进入到queue,自然相应的reference不会为null.如果需要被处理(内存不够或其它策略),则置相应的referent为null,然后进入到queue.
finalreference对象,因为需要调用其finalize对象,因此其reference即使入queue,其referent也不会为null,即不会clear掉.
phantomreference对象,因为本身get实现为返回null.因此clear的作用不是很大.因为不管enqueue还是没有,都不会清除掉.
十、referencehandler enqueue线程
上面提到jvm会将要处理的对象设置到pending对象当中,因此肯定有一个线程来进行不断的enqueue操作,此线程即引用处理器线程,其优先级为max_priority,即最高.相应的启动过程为静态初始化创建,可以理解为当任何使用到reference对象或类时,此线程即会被创建并启动.相应的代码如下所示:
static { threadgroup tg = thread.currentthread().getthreadgroup(); for (threadgroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getparent()); thread handler = new referencehandler(tg, "reference handler"); /* if there were a special system-only priority greater than * max_priority, it would be used here */ handler.setpriority(thread.max_priority); handler.setdaemon(true); handler.start(); }
其优先级最高,可以理解为需要不断地处理引用对象.在通过jstack打印运行线程时,相应的reference handler即是指在这里初始化的线程,如下所示:
十一、jvm相关
在上述的各个处理点当中,都与jvm的回收过程相关.即认为gc流程会与相应的reference协同工作.如使用cms收集器,在上述的整个流程当中,涉及到preclean过程,也涉及到softreference的重新标记处理等,同时对reference对象的各种处理也需要与具体的类型相关进行协作.相应的jvm处理,采用c++代码,因此需要好好地再理一下.
十二、总结
与finalreference对象相同,整个reference和referencequeue都是一组协同工作的处理组,为保证不同的引用语义,通过与jvm gc相关的流程一起作用,最终实现不同场景,不同引用级别的处理.
另外,由于直接使用referencequeue,再加上开启线程去监控queue太过麻烦和复杂.可以参考由google guava实现的 finalizablereferencequeue 以及相应的finalizablereference对象.可以简化一点点处理过程.以上就是这篇文章的全部内容,希望对大家的学习或者工作带来一定的帮助。