当前位置: 首页 > 科技观察

令人痛心的Java幻象引用!

时间:2023-03-13 17:37:13 科技观察

本文转载自微信公众号“小姐姐的味道”,作者小姐姐养的狗。转载本文请联系味觉小姐公众号。在Java世界里,对象有不同层次的存在,类与类之间充满了调侃。strong、weak、weak的各种引用,熟悉Java的同学一定不会陌生。随着级别的降低,它们变得越来越不存在。大多数常用对象都是强引用的;而软引用和弱引用在一些堆缓存框架中经常使用。幻影引用呢?难道传说中的鬼参就像它的名字一样,没有用?三种类型的引用首先,让我们回顾一下其他三种引用的类型和用途。强引用当内存空间不足,系统无法支持时,JVM会抛出OutOfMemoryError错误。即使程序异常终止,也不会收集此类对象。这种引用是最常见也是最强的一种存在,只有和GCRoots断绝关系才会被淘汰。这种参考,你每天编码都用到。例如:new一个普通对象。Objectobj=newObject()可能有问题。如果您的系统被大量用户(User)访问,您需要记录该用户访问的时间。不幸的是,User对象中没有这样的字段,所以我们决定开辟一个额外的空间来存储这些信息。软引用软引用用于维护一些可有可无的对象。当内存足够时,软引用对象不会被回收。只有当内存不足时,系统才会回收软引用对象。如果软引用对象回收后内存仍然不够,则会抛出内存溢出异常。可以看出这个特性非常适合缓存技术。比如网页缓存,图片缓存等。Guava的CacheBuilder提供了设置软引用和弱引用的方式。在这种情况下,软引用比强引用安全得多。软引用可以与引用队列(ReferenceQueue)结合使用。如果软引用引用的对象被垃圾回收,Java虚拟机会将软引用添加到与其关联的引用队列中。弱引用弱引用对象比软引用更无用,生命周期更短。JVM在进行垃圾回收时,无论内存是否足够,弱引用关联的对象都会被回收。弱引用的生命周期较短,在Java中由java.lang.ref.WeakReference类表示。奇怪的幻影引用上面的引用层次很好理解,但是幻影引用是个例外。可以使用以下代码定义虚引用:Objectobject=newObject();ReferenceQueuequeue=newReferenceQueue();//幻引用必须关联一个引用队列PhantomReferencepr=newPhantomReference(object,queue);但是当你想取出值(get)的时候,却总是getnull。//JDK源码/***返回thisreferenceobject的referent。因为引用的fa*phantomreference总是不可访问的,所以这个方法总是returns*{@codenull}.**@return{@codenull}*/publicTget(){returnnull;}Phantomreferences主要用来跟踪被引用的对象垃圾收集活动。当垃圾回收器要回收一个对象时,如果发现它还有一个虚引用,就会在回收这个对象之前把这个虚引用添加到与之关联的引用队列中。如果程序发现引用队列中加入了虚引用,则可以在被引用对象的内存被回收之前采取必要的动作。在桃花源的深处,在hotspot的jvm中,有一个叫做cleaner的类,其实就是幻引用的典型应用。可以看出,Cleaner直接简单的继承了PhantomReference,所以本质上是一个幻引用,只是增加了一些更方便的操作。那么这个类用在什么地方呢?每个人手里应该都有jdk的源码。跟踪后发现,最终是DirectByteBuffer使用的。直接记忆一直是一个非常崇高的名词。它基本上与高性能挂钩,但也容易出现内存泄漏。由于直接内存属于堆外内存,当垃圾被回收时,JVM的一套垃圾回收算法并不能将其清理干净。事实上,由于DirectByteBuffer可能会被长期使用,所以在年轻代经过各种回收后会进入老年代。这时候就比较麻烦了。这些引用对象只能在下一轮OldGC或FullGC中被触发。如果你的老年代空间很大,那么触发回收操作需要很长时间。问题是,在这期间,堆外内存虽然不再使用,但仍然占据着很大的物理空间,最终造成严重的浪费,甚至崩溃。对堆外内存不是很了解的同学可以看我之前做的一张图。或者只是阅读这篇文章。直接内存使用上限可以通过-XX:MaxDirectMemorySize来限制。《一图解千愁,jvm内存从来没有这么简单过!》那么这些堆外内存是怎么回收的呢?这就是清洁工的作用。Cleaner通过next和prev构建了一个典型的链表,但是它没有任何逻辑,因为它的清洗逻辑在thunk方法中。cleaner=Cleaner.create(this,newDeallocator(base,size,cap));publicvoidclean(){if(remove(this)){try{this.thunk.run();即Deallocator=Deallocator。其中,传入的base是unsafe类申请的堆外内存地址引用(只是一个地址)。有了reference和capacity,我们在回收的时候其实就可以定位到真正的堆外内存块了。就像Deallocator一样。publicvoidrun(){if(address==0){//Paranoiareturn;}unsafe.freeMemory(address);address=0;Bits.unreserveMemory(size,capacity);}机制没有问题,关键看关于它们是如何连接起来的。这种问题,当然要交给其他线程来完成,这里就是ReferenceHandler。很熟悉的名字,每次使用jstack命令导出堆栈时都会看到。Threadhandler=newReferenceHandler(tg,"ReferenceHandler");/*如果有一个特殊的system-onlypriority大于*MAX_PRIORITY,它将在这里使用*/handler.setPriority(Thread.MAX_PRIORITY);handler.setDaemon(true);handler.start();真正工作的方式,就是tryHandlePending,这里调用Cleaner的clean方法,然后调用真正的cleaning方法释放堆外内存。它会从幻象引用注册的队列中取出一个新的对象,然后判断是否是Cleaner类型,如果是则进行清理。End这是一个虚引用。它存在的唯一目的就是在回收时被感知,从而进行更深层次的清洁。在commons-io包的FileCleaningTracker类中,还有一个继承虚引用的Tracker类,用于跟踪后续文件的一些清理工作。这个没有任何存在感的小虚拟引用默默承担着最后一道防线,是系统正常运行的有效保障。不要小看它,它无处不在。因为您的每个JVM进程都运行一个名为ReferenceHandler的线程。作者简介:品味小姐姐(xjjdog),一个不允许程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。我的个人微信xjjdog0,欢迎加好友进一步交流。