背景知识2——java 引用总结

277 阅读6分钟

引用

强引用

父类reference

T referent

ReferenceQueue<? super T> queue; 引用队列

Reference next;

  • 1、如果引用对象在引用队列里面了,则next表示引用队列的下一个引用对象,如果是引用队列的最后一个元素则next=this
  • 2、和queue共同编码了引用对象的状态

Reference discovered;

  • pending 链表的下一个引用对象

static Reference pending
  • 是一个static 字段,是pending 链表的头结点

ReferenceHandler

  • 在Reference的static 语句里会启动这么一个线程

  • 用system线程组作为Reference Handler的线程组

  • Reference Handler设置最高的线程优先级

  • 是一个守护线程

  • 功能

    • 不断将pending 链表的引用对象拿出,如果pending链表为空,进入等待,等待vm将应用对象放入pending链表后,唤醒ReferenceHandler
    • 拿出之后,如果引用对象是Cleaner的实例则调用clean函数,否则将应用对象放入引用队列里面

状态

  • active

    • 状态,如果对象已经不可达,那么如果引用没有和一个引用队列关联,则ref的状态会改为Inactive,否则会改为Pending,然后将这个ref放到pending list中
  • pending

    • 是pending list中的一个元素,等待Reference-handler thread去将它入队列,如果引用没有和一个引用队列关联则不会到这个状态
  • Enqueued

    • 该引用是与之相关联的引用队列中的一个元素,当从引用队列中移除的时候,则变成Inactive
  • Inactive

    • Inactive状态,不会再改变了
  • 四种状态是由queue和next这两个字段共同编码的

    • active

      • queue=ReferenceQueue.NULL或者等于一个引用队列
      • next=null
    • pending

      • queue等于与之关联的引用队列
      • next=this
    • Enqueued

      • queue=ReferenceQueue.ENQUEUED
      • next等于它在引用队列中的下一个引用,如果它在引用队列的尾部,则next=this
    • Inactive

      • queue = ReferenceQueue.NULL
      • next = this

软引用

用于做缓存

弱引用

ThreadLocal中用于防止内存泄露

WeakHashMap

  • 其中的Entry继承自弱引用,指向key对象,当key对象只有弱可达时,该key会被回收,这个时候,vm会将弱引用放到pending 链表中,同时通知reference handler线程处理,reference handler会将该弱引用从pending 链表中拿出来,放到引用队列里面来。当expungeStaleEntries被调用的时候,会从引用队列中取出相应的Entry对象,将它从hashmap中删掉

虚引用

不能通过get获取到它关联的对象

必须关联一个引用队列

虚引用所引用的对象,永远不会被回收,除非指向这个对象的所有虚引用都调用了clear函数,或者所有这些虚引用都不可达

  • java9则不会影响对象的生命周期

java8及以前,虚引用指向的对象至少要经过2次gc才能回收

用法

  • 用于释放对外内存

  • mysql 中用虚引用去做连接的保底释放

    • 每次建立链接都会创建虚引用ConnectionFinalizerPhantomReference

    • Set connectionFinalizerPhantomRefs = ConcurrentHashMap.newKeySet();一个static的集合,保证所有的虚引用对象强可达

    • AbandonedConnectionCleanupThread 不断从引用队列里面把ConnectionFinalizerPhantomReference拿出来进行处理,

      • 1、关闭socket
      • 2、将ConnectionFinalizerPhantomReference里面的引用清空,保证MysqlConnection对象也能被回收
      • 3、将ConnectionFinalizerPhantomReference从connectionFinalizerPhantomRefs删掉,保证虚引用本身也可以被gc

cleaner

继承自虚引用

构造器私有,调用工厂方法,则创建一个cleaner,并且将这个cleaner加入全局的cleaner链表,头指针是一个static字段,因此创建的cleaner是强可达的

必须的字段,引用、runnable, 引用队列

clean方法

  • 1、将cleaner从链表中删除,保证该cleaner和cleaner指向的对象,都不可达,都可以在下次gc中被回收
  • 2、调用runnable方法

用于释放直接内存

  • 创建DirectByteBuffer对象时会创建一个Cleaner对象,Cleaner对象持有了DirectByteBuffer对象的引用。当JVM在GC时,如果发现DirectByteBuffer被地方法没被引用啦,JVM会将其对应的Cleaner加入到pending-reference链表中,同时通知ReferenceHandler线程处理,ReferenceHandler收到通知后,会调用Cleaner#clean方法,而对于DirectByteBuffer创建的Cleaner对象其clean方法内部会调用unsafe.freeMemory释放堆外内存。最终达到了DirectByteBuffer对象被GC回收其对应的堆外内存也被回收的目的

为什么要用cleaner,因为cleaner的处理在ReferenceHandler线程中,ReferenceHandler优先级很高

FinalReference

引出

  • Object#finalize如果在执行的时候当前对象又被重新赋值,那下次GC就不会再执行finalize方法了,这是为什么啊

相关主件

  • FinalReference

    • 一定要和一个引用队列进行关联
  • Finalizer

    • Finalizer继承FinalReference
  • FinalizerThread

    • 守护线程,优先级不是很高
    • 线程名称为Finalizer
  • 三个主件都是包可见性,因此程序员没有办法对之编程

特性

  • HotSpot实现上在创建一对象时,如果该类重写了Object#finalize方法且方法内容不为空,则会调Finalizer#register方法

    • Finalizer#register方法的调用契机

      • new一个对象的过程

        • 分配空间
        • 调用构造函数
      • Finalizer#register的调用可以在分配空间之后,或者构造函数返回之前

      • -XX:-RegisterFinalizersAtInit默认为true,为true则在构造函数返回前调用Finalizer#register,为false则在分配空间之后调用

        • 在构造函数返回前调用Finalizer#register的实现?

          • hotspot的实现是在Object这个类在做初始化的时候将构造函数里的return指令替换为_return_register_finalizer指令,处理该指令的时候调用Finalizer.register
  • Finalizer的register方法会调用构造方法,构建方法里面会创建Finalizer对象,同时将新创建的Finalizer加入链表的表头

  • gc的时候,当一个对象只有Finalizer指向它的时候,这个是有Finalizer会被放入pending 链表里面,之后会通知reference handler线程进行处理。这次gc,这个对象不会被gc掉,因为Finalizer有到这个对象的强引用,并且这个Finalizer还在链表中,因此这个对象强可达。reference handler会将Finalizer放到所有Finalizer共有的引用队列里面,FinalizerThread 会源源不断的从这个引用队列里面取出Finalizer,来进行处理runFinalizer

    • 把该Finalizer从Finalizer链表中删掉
    • 把next和prev都设置为this,表示该对象的finalize方法已经调用过了
    • 调用对象的finalize方法
    • 将Finalizer存储的引用清空clear

问题

  • 为什么finalize方法只执行一次

    • 两个原因

      • 1、如果FinalizerThread已经对Finalizer进行处理了,那么这个时候Finalizer存储的引用清空clear了,Finalizer和对应没有关系了
      • 2、Finalizer记录了finalize方法是否被调用过
  • 为什么建议不要重写Object#finalize方法

    • 因为重写了之后,那么这个对象什么时候回收的呢?经历一次gc,然后Finalizer执行了runFinalizer之后的下一次gc,才能被回收掉。因为Finalizer的优先级比较低,因此等到runFinalizer执行完,可能中间执行了多次gc了,因此重写Object#finalize方法的对象至少要经历两次gc才能回收对象,因此造成了内存泄露