第二十章 引用是个什么鬼

引用是个什么鬼

一切都是对象

一切都是对象,但操作对象的都是指针或引用。在C++中,我们通过指针操作对象。而在Java中,我们把指针称为"引用",通过引用来操作对象。Java中的垃圾回收机制也是根据"引用"来判断是否需要回收。所以,理解"引用"对理解Java语言很重要。

在Java中,引用主要有两种。对于基本类型变量( 字符类型char,布尔类型boolean以及数值类型byte、short、int、long、float、double),赋值运算直接修改为新值,原来的数据被覆盖。对于引用类型变量,赋值运算只会改变变量中所保存的对象的地址,原来的对象通过垃圾回收机制进行回收。有一种特殊的引用类型null,它既可以转化为任意类型,但又不属于任意类型。引用类型有:类类型(class types),接口类型(interface types),数组类型(array types),类型变量类型(type variables)(实际上泛型类型,泛型擦除后运行时的实际类型可能是类类型,接口类型或者数组类型)。

基于强弱区分引用

JDK1.2之后,Java扩充了引用的概念。提供了四种引用类型,分别是强引用、软引用、弱引用和虚引用。这些类都在java.lang.ref包下。
在这里插入图片描述

强引用

Java中的Object obj = new Object(),obj就是一个强引用。只要是强引用,表示该对象在使用,在内部不足时也不会被回收。当JVM需要新分配对象时,堆内存不足会直接抛出OutOfMemoryError(OOM)。如果要中断强引用与对象之间的联系,可以将栈上的强引用赋值为null。这样JVM就可以对该对象进行回收了。

软引用

软引用表示引用一些可能有用的对象。在内存充足时,软引用指向的对象不会被回收;只有在堆使用率临近阈值时,系统会回收软引用对象(一般FullGC会将软引用对象放入了old区,这里是将老年区的软引用回收)。此时JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的Reference最近使用情况和创建时间来综合决定是否回收该Reference。如果回收了软引用对象之后仍然没有足够的内存,才会抛出OOM。所以,SoftReference的特点是,它的一个实例保存了一个Java对象的软引用,如果该对象没被回收,通过get()方法返回实例对象的强引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。这个特性决定了软引用很适合做缓存。在本节最后有个实例会用软引用做缓存。

弱引用

弱引用WeakReference比软引用更弱的引用,无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都会被回收。 弱引用主要用于监控对象是否被垃圾回收器标记为即将回收,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。对于只被弱引用指向的对象来说,其只能存活到下一次JVM执行垃圾回收动作之前。也就是说,JVM的每一次垃圾回收动作都会回收那些只被弱引用指向的对象。

其中,弱引用的一个构造方法WeakReference(T referent, ReferenceQueue<? super T> q),其中ReferenceQueue,在对象被回收后,会把弱引用对象,也就是WeakReference对象或者其子类的对象,放入队列ReferenceQueue中,这里并不是被弱引用的对象,被弱引用的对象已经被回收了。

如下图所示,带实线的箭头表示强引用,带虚线的箭头表示弱引用。此时"hello"对象被str强引用,并且被wf弱引用。因此"hello"不会被回收。

 String str = new String("hello"); //--1--
 ReferenceQueue<String> rq = new ReferenceQueue<String>(); //--2--
 WeakReference<String> wf = new WeakReference<String>(str, rq); //--3--
 str = null; //--4--取消"hello"对象的强引用
 String str1 = wf.get(); //--5--假如"hello"对象没有被回收,str1引用"hello"对象
//假如"hello"对象没有被回收,rq.poll()返回null
 Reference<? extends String> ref = rq.poll();

在这里插入图片描述

虚引用

虚引用PhantomReference随时可能会被回收,虚引用不能通过get()获取目标对象的强引用从而使用目标对象。 可以和引用队列一起使用检测对象是否从内存中删除,所以,虚引用可以用于对对象回收进行跟踪。

与软引用、弱引用不同,虚引用必须和引用队列一起使用。虚引用所指向的对象在被系统内存回收前,虚引用自身会被放入ReferenceQueue对象中从而跟踪对象垃圾回收。 并且虚引用不会根据内存情况自动回收目标对象。

引用队列

引用队列ReferenceQueue可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

另外值得注意的是,其实SoftReference,WeakReference 以及 PhantomReference 的构造函数都可以接收一个 ReferenceQueue 对象。当 SoftReference 以及 WeakReference 被清空的同时,也就是 Java 垃圾回收器准备对它们所指向的对象进行回收时,调用对象的 finalize() 方法之前,它们自身会被加入到这个ReferenceQueue对象中,此时可以通过 ReferenceQueue的poll()方法取到它们。而PhantomReference只有当Java垃圾回收器对其所指向的对象真正进行回收时,会将其加入到这个ReferenceQueue 对象中,这样就可以追综对象的销毁情况。

下面表格展示了不同引用类型的应用场景。

引用类型回收时间应用场景
强引用一直存活,除非GC Roots不可达基本对象,自定义对象通过new关键字建立强引用
软引用内存不足时会被回收用在对内存非常敏感的资源上,用作缓存的场景比较多,如图片缓存等。
弱引用只能存活到下一次GC前生命周期很短的对象,例如ThreadLocal中的Key
虚引用随时会被回收, 创建了可能很快就会被回收用来跟踪JVM的垃圾回收活动

值传递和引用传递

局部变量/方法参数

对于方法中传递的参数和方法内的局部变量在编译时会计算分配的内存大小,该大小存在于class文件中的方法code属性的max_locals变量中,在运行时根据编译时确认的大小在栈上开辟空间,这部分空间称为局部变量表。局部变量表中的单位为"slot"。每个slot存放基本类型boolean、type、char、short、int、 float、double、long,引用类型reference,及返回值类型returnAddress类型的数据。slot的索引从0开始,如果执行的是非static方法,默认第一个slot存储为该实例对象的指针,其后依次为方法入参、方法内使用的局部变量。除了double、long两个64位长度的数值类型,每个slot占用32位大小。如果是64位的数据类型,则连续读取n,n+1两个slot的数据。连续读取两个slot并不需要考虑是否是原子操作问题,因为局部变量表是建立在线程的堆栈上的,是线程私有的,可以理解为ThreadLocal中的ThreadLocalMap中的变量。当在方法内部声明一个int变量或Object obj = null时,此时仅仅在当前线程的堆栈中分配空间,不影响虚拟机堆空间。当声明Object obj = new Object()时,将会在虚拟机堆中分配一段内存空间并初始化Object对象。但对8种基本类型对应的包装器类访问是基于引用。那么为什么设计这8种基本类型呢?这个主要是基于基本类型占用内存少和性能高。 下面实例是基本类型的传递。通过输出结果我们知道,基本变量的传递为值传递。

public class TestReference {
    public static void testOne(int i){
        i = 2;
        System.out.println("test函数中的变量值:" + i);
    }
    public static void main(String[] args) {
        int a = 1;
        TestReference.testOne(a);
        System.out.println("main函数中的变量值:" + a);
    }
}

数组类型引用和对象

当在方法中声明数组时,int[] arr=new int[2];数组也是对象,arr在栈上占用32位大小的存储空间,而在堆中开辟相应大小空间进行存储,然后arr变量指向它。当声明一个二维数组时,如:int[][] arr2=new int [2][4],arr2同样在栈中占用32位,在堆内存中开辟长度为2,类型为int[]的数组对象,然后arr2指向这个数组。这个数组内部有两个引用类型(大小为4个字节),分别指向两个长度为4类型为int的数组。内存分布如图:
在这里插入图片描述

String类型数据

当定义String类型参数时,在堆中维护了该String的内容,开始位置和长度。最终调用System.arraycopy来返回对象。

 Arrays.copyOfRange(value, offset, offset+count);

根据上面的内容,当方法中参数为数组等对象时,出现“按引用传递”的效果。 当参数为基本数据类型时,参数的传递为按值的拷贝传递。
a. 基本数据类型参数传值为值传递,对传入调用方法的入参变量修改不会影响原始值。
b. 包装器类型及String类型参数值值为引用传递,但由于包装器类与String类的不变性,编译器对其进行了特殊处理,并不会改变原始值,所以也可以理解为值传递。
c. 引用传递,对传入调用方法的入参变量和原始变量指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象。

引用类与垃圾回收

Reference

Reference属性

    private T referent;         /* 保存对象的引用,根据不同的Reference会被GC特别对待 */
    ReferenceQueue<? super T> queue;/*如果需要通知机制,则需要初始化该队列**/
    Reference next;/*这个用于实现一个单向循环链表,保存ReferenceHandler线程处理的引用*/
    transient private Reference<T> discovered;  /* 被虚拟机使用 */
    private static Lock lock = new Lock();//用于同步队列pending的出队入队。
    /*等待排队的引用列表,垃圾收集器将引用添加到这个列表,ReferenceHandlerThread负责删除它们,此列表受上面的lock保护
     */
    private static Reference pending = null;

reference:表示对象的引用。对象即将被回收的定义:此对象除了被reference引用之外没有其它引用了,此时GCRoot将不可达该对象。如果一旦被回收,则会直接置为null,而外部程序可通过引用对象本身( 通过reference#get() )了解到回收行为的产生( PhntomReference除外 )。

next:存储当前引用节点下一个即将被处理的节点。在执行enqueue()方法时,将next设置为下一个要处理的Reference对象。为了描述相应的状态值,在放到队列当中后,其queue就不会再引用这个队列了。而是引用一个特殊的ENQUEUED。因为已经放到队列当中,并且不会再次放到队列当中。

discovered:表示被虚拟机找到的要回收的下一个对象,下一个对象也是一个链表结构。当ReferenceQueue处于active状态时,此时discovered的下一个元素是由GC操纵的( 如果是最后一个则为this );当处于pending状态:discovered为pending集合中的下一个元素( 如果是最后一个了则为null );其他状态:discovered为null。

lock:在垃圾收集中用于同步的对象。收集器必须在每次收集周期开始时获取该锁,任何持有该锁的代码应该尽快完成,不分配新对象,并且避免调用用户代码。

pending:用来保存等待回收的引用列表。收集器会添加引用到这个列表,直到Reference-handler线程删除该引用列表。这个列表被上面的lock对象保护。这个列表使用discovered字段来连接它自己的元素( 即pending的下一个元素就是discovered对象 )。可以理解JVM在GC时会将要清理的对象赋值给这个静态属性。

queue:即将被回收的引用队列。当对象即被回收时,整个reference对象( 而不是被回收的对象 )会被放到queue里面,然后外部程序即可通过监控这个queue拿到相应的数据了。它的存储是依赖于内部节点之间的关系来表达。可以理解为queue为一个链表的容器,其自己仅存储当前的head节点,而后面的节点由每个reference节点自己通过next来保持即可。

Reference构造函数

构造函数有两个。一个带queue,一个不带queue。其中queue的意义在于,我们可以在外部对这个queue进行监控。即如果有对象即将被回收,那么相应的reference对象就会被放到这个queue里。而如果不带的话,就只有不断地轮询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;
}

ReferenceHandler线程

用静态代码块启动高级别的后台线程来将等待回收的引用追加到pending中。当一个Reference的referent被回收时,垃圾回收器会把reference添加到pending这个链表里,然后Reference-handler thread不断的读取pending中的reference,把它加入到对应的ReferenceQueue中,如果pending为null,线程进入wait状态。为什么需要对pending加锁?因为如果采用CMS并发收集器时,GC线程和ReferenceHandler线程可能并发执行。

由此可见,pending是由jvm来赋值的,当Reference内部的referent对象的可达状态改变时,jvm会将Reference对象放入pending链表。并且这里enqueue的队列是我们在初始化( 构造函数 )Reference对象时传进来的queue,如果传入了null( 实际使用的是ReferenceQueue.NULL ),则ReferenceHandler则不进行enqueue操作,所以只有非RefernceQueue.NULL的queue才会将Reference进行enqueue。

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();
    }

static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {//如果pending链表不为空,从pending中获取下一个元素,如果后继为空,则next指向本身
                    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;
                } else {//如果pending链表为空,则当前线程等待
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    if (waitForNotify) {
                        lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;//唤醒后继续循环
        } catch (InterruptedException x) {
            // retry
            return true;
        }

        // Fast path for cleaners
        if (c != null) {
            c.clean();//如果是虚引用,则直接清除,清除后继续循环
            return true;
        }

        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);//如果该Reference注册了对应的Queue,则加入到Queue中。
        return true;
    }                    

ReferenceQueue

ReferenceQueue是作为JVM GC与上层Reference对象管理之间的一个消息传递方式,它使得我们可以对所监听的对象引用可达发生变化时做一些处理。而ReferenceHandler线程负责将已注册的引用对象添加到该队列中。ReferenceQueue的属性有以下几个。

head :始终保存队列中最新要处理的节点,可以认为queue为一个后进先出的队列。

static ReferenceQueue NULL = new Null();//用于标识没有注册Queue
static ReferenceQueue ENQUEUED = new Null();//用于标识已经处于对应的Queue中

static private class Lock { };
//互斥锁,用在enqueue和用户线程操作的remove和poll出队操作中
private Lock lock = new Lock();
private volatile Reference<? extends T> head = null;
private long queueLength = 0;//队列中的元素个数

JVM并不需要定义状态值来判断相应引用的状态处于哪个状态,只需要通过计算next和queue即可进行判断。通过这个组合,收集器只需要检测next属性为了决定是否一个Reference实例需要特殊的处理:如果next==null,则实例是active;如果next!=null,为了确保并发收集器能够发现active的Reference对象,而不会影响可能将enqueue()方法应用于这些对象的应用程序线程,收集器应通过discovered字段链接发现的对象。discovered字段也用于链接pending列表中的引用对象。

  1. 活动状态active
    GC会特殊对待此状态的引用,一旦被引用的对象的可达性发生变化(如失去强引用,只剩弱引用,可以被回收),GC会将实例的状态变为挂起状态或者不活跃状态。依赖于实例在创建时是否向引用队列中注册。如果已经注册,则GC会将该对象的引用放入pending队列并将其状态改为pending状态。否则新建实例的状态为活动状态active。这个状态的对象,不需要注册到引用队列ReferenceQueue中,所以,Reference中的next字段表示对象垃圾回收状态为next == null。而引用队列queue为ReferenceQueue.NULL。
  2. 挂起状态pending
    位于静态字段pending队列中,等待ReferenceHandler线程将引用入队queue。显然,未注册的实例不可能为pending状态。此状态下,next == this,引用队列queue为 ReferenceQueue.NULL。
  3. 排队状态enqueue
    当实例创建时用于注册实例引用到队列, 此状态中next 为该queue中的下一个引用,如果是该队列中的最后一个,那么为this,queue = ReferenceQueue.ENQUEUED 。当实例从ReferenceQueue移除时,它将变成非活动状态。
  4. 不活跃状态inactive
    引用从queue出队后的最终状态,该状态不可变。 此时next == this,queue == ReferenceQueue.NULL。即意味着此引用对象可以被回收,并且对内部封装的对象也可以被回收掉了。但实际的回收运行取决于clear()方法是否被调用(clear()方法会设置this.referent = null)

enqueue()方法。将待处理引用放入当前队列中。

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
        synchronized (r) {
            if (r.queue == ENQUEUED) return false;//判断是否已经入队
            synchronized (lock) {
                r.queue = ENQUEUED;
                //单向链表
                r.next = (head == null) ? r : head;
                head = r;
                queueLength++;
                if (r instanceof FinalReference) {
                    sun.misc.VM.addFinalRefCount(1);
                }
                lock.notifyAll();//通知阻塞在当前队列上的情况
                return true;
            }
        }
    }

外部从queue中获取Reference

  • WeakReference对象进入到queue之后,相应的referent为null。
  • SoftReference对象,如果对象在内存足够时,不会进入到queue,自然相应的referent不会为null。如果需要被处理( 内存不够或其它策略 ),则置相应的referent为null,然后进入到queue。通过debug发现,SoftReference是pending状态时,referent就已经是null了,说明此事referent已经被GC回收了。
  • FinalReference对象,因为需要调用其finalize对象,因此其reference即使入queue,其referent也不会为null,即不会clear掉。
  • PhantomReference对象,因为本身get实现为返回null。因此clear的作用不是很大。因为不管enqueue还是没有,都不会清除掉。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UNp8fX58-1606584688742)(E:\muke\images\finalizer-state.png)]

active ——> pending :Reference#tryHandlePending
pending ——> enqueue :ReferenceQueue#enqueue
enqueue ——> inactive :Reference#clear

Finalizer

Finalizer类的功能包含使用FinalizerThread来执行对象的finalize方法()。垃圾收集器调用finalizer()的时间取决于JVM的实现和系统条件,这个是我们无法控制的,但是我们可以利用System.gc方法或者RunTime.getRunTime().gc()来指定收集内容。《Effective Java》的作者要求我们尽量避免覆盖finalize()方法。覆盖了finalize方法的对象至少需要两次GC才可能被回收。第一次GC把覆盖了finalize方法的对象对应的Finalizer reference加入referenceQueue等待FinalizerThread来执行finalize方法。第二次GC才有可能释放finalizee对象本身,前提是FinalizerThread已经执行完finalize方法了,并把Finalizer reference从Finalizer静态unfinalized链表中剔除,因为这个链表和Finalizer reference对finalizee构成的是一个强引用。

Finalizer类中有个很重要的属性unfinalized,该属性维护了一个未执行finalize方法的reference链表。维护静态字段unfinalized的目的是为了一直保持对未执行finalize方法的reference的强引用,防止被gc回收掉。第一次GC时,会把该finalizee对应的reference放到Finalizer的refereneQueue中;接着,FinalizerThread来执行finalizee的finalize方法,并把当前Finalizer从unfinalized中剔除。当下一次GC发生时,由于unfinalized已经不再持有该对象的referent,故该对象被直接回收掉。

执行过程
  1. JVM在对象初始化时(默认为构造函数返回之前调用)调用register方法,register方法会把本对象封装为Finalizer放入静态unfinalized链表中。

    JVM通过VM参数RegisterFinalizersAtInit的值来确定何时调用register,RegisterFinalizersAtInit默认为true,则会在构造函数返回之前调用call_register_finalizer方法。如果通过-XX:-RegisterFinalizersAtInit 设为false,则会在对象空间分配好之后就调用call_register_finalizer。

  2. JVM在执行GC时,实现了finalize()的对象没有被其他对象引用时(FinalizeReference除外),会把该对象finalizee对应的reference放到Finalizer的静态链表refereneQueue中,等待FinalizerThread来执行finalizee的finalize方法。finalize方法就是轮到该线程执行时就弹出里面的对象,执行它们的finalize(),对应的FinalizerReference对象在下次执行GC时就会被清理掉。一个堆的FinalizerReference会组成一条双向链表,垃圾回收器应该会持有链表头(链表头在FinalizerReference中为一个静态成员),如下图所示。
    在这里插入图片描述

  3. 下次GC时,finalizee对象才能被GC回收。

final class Finalizer extends FinalReference<Object> { 
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private static Finalizer unfinalized = null;
    private static final Object lock = new Object();

    /* Invoked by VM */
    //对象初始化时调用该方法
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        add();//放入到unfinalized中
    }
    private void add() {
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }
private static class FinalizerThread extends Thread {
        private volatile boolean running;
        FinalizerThread(ThreadGroup g) {
            super(g, "Finalizer");
        }
        public void run() {
            if (running)
                return;
            // Finalizer thread starts before System.initializeSystemClass
            // is called.  Wait until JavaLangAccess is available
            while (!VM.isBooted()) {//必须等虚拟机启动才运行该线程
                // delay until VM completes initialization
                    VM.awaitBooted();
            }
            //用于获取系统初始化后的对象
            final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
            running = true;
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue  //finalize方法抛异常时不处理
                }
            }
        }
    }
private void runFinalizer(JavaLangAccess jla) {
    synchronized (this) {
        if (hasBeenFinalized()) return;
        remove();
    }
    try {
        Object finalizee = this.get();
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
            //用于执行参数对象的finalize()方法
            jla.invokeFinalize(finalizee);
            /* Clear stack slot containing this variable, to decrease
               the chances of false retention with a conservative GC */
            finalizee = null;
        }
    } catch (Throwable x) { }
    super.clear();
}

从源码中可以看出,Java.lang.System中,静态初始化registerNatives()方法时,会调用initializeSystemClass方法,进而执行setJavaLangAccess方法;setJavaLangAccess方法会对sun.misc.SharedSecrets.javaLangAccess赋对象,对象中invokeFinalize(Object o)的实现就是执行o.finalize()方法。Finalizer线程在System.initializeSystemClass方法执行前被调用,等到JavaLangAccess可用为止。表示System.initializeSystemClass初始后sun.misc.VM#booted=true,才往下执行,否则等待。

public final class System {

    /* register the natives via the static initializer.
     *
     * VM will invoke the initializeSystemClass method to complete
     * the initialization for this class separated from clinit.
     * Note that to use properties set by the VM, see the constraints
     * described in the initializeSystemClass method.
     */
    private static native void registerNatives();
    static {
        registerNatives();
    }
   private static void initializeSystemClass() {
        // register shared secrets
        setJavaLangAccess();
          ...
        // Subsystems that are invoked during initialization can invoke
        // sun.misc.VM.isBooted() in order to avoid doing things that should
        // wait until the application class loader has been set up.
        // IMPORTANT: Ensure that this remains the last initialization action!
        sun.misc.VM.booted();
    }

java.lang.System#setJavaLangAccess

private static void setJavaLangAccess() {
        // Allow privileged classes outside of java.lang
        sun.misc.SharedSecrets.setJavaLangAccess(new sun.misc.JavaLangAccess(){
            public sun.reflect.ConstantPool getConstantPool(Class<?> klass) {
                return klass.getConstantPool();
            }
              ......
            public void invokeFinalize(Object o) throws Throwable {
                o.finalize();//回收对象内存
            }
        });
    }

引用在缓存中的使用实例

SoftReference的特点是它的一个实例保存一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之后,get()方法将返回null。 看下面代码:

MyObject aRef = **new**
MyObject();
SoftReference aSoftRef=**new** SoftReference(aRef); 

此时,对于这个MyObject对象,有两个引用路径,一个是来自SoftReference对象的软引用,一个来自变量aReference的强引用,所以这个MyObject对象是强可及对象。
随即,我们可以结束aReference对这个MyObject实例的强引用:

aRef = null;

此后,这个MyObject对象成为了软可及对象。如果垃圾收集线程进行内存垃圾收集,并不会因为有一个SoftReference对该对象的引用而始终保留该对象。Java虚拟机的垃圾收集线程对软可及对象和其他一般Java对象进行了区别对待:软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可反对象会被虚拟机尽可能保留。在回收这些对象之前,我们可以通过:

MyObject anotherRef = (MyObject)aSoftRef.get(); 

重新获得对该实例的强引用。而回收之后,调用get()方法就只能得到null了。

3 使用ReferenceQueue清除失去了软引用对象的SoftReference
作为一个Java对象,SoftReference对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。所以,当软可及对象被回收之后,虽然这个SoftReference对象的get()方法返回null,但这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。在java.lang.ref包里还提供了ReferenceQueue。如果在创建SoftReference对象的时候,使用了一个ReferenceQueue对象作为参数提供给SoftReference的构造方法,如:

ReferenceQueue queue = new ReferenceQueue();
SoftReference ref=new SoftReference(aMyObject, queue); 

那么当这个SoftReference所软引用的aMyOhject被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。另外从ReferenceQueue这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。
在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。常用的方式为:

SoftReference ref = null;
while ((ref = (EmployeeRef) q.poll()) != null) {
// 清除ref
}

https://www.cnblogs.com/aspirant/p/11734918.html

总结

GC线程回收对象 -> 将相关指向这个对象的引用加入到其引用队列(如果有)-> 更新引用入队状态(isEnqueued 方法返回 true)-> 在 Java 代码中可以得到引用队列中的已经入队的引用(即得到要回收对象的对应引用对象,作为对象回收的一个通知)。

如果PhantomReference对象不管enqueue还是没有,都不会清除掉reference对象,那么怎么办?这个reference对象不就一直存在这了??而且JVM是会直接通过字段操作清除相应引用的,那么是不是JVM已经释放了系统底层资源,但java代码中该引用还未置null??
A:不会的,虽然PhantomReference有时候不会调用clear,如Cleaner对象 。但Cleaner的clean()方法只调用了remove(this),这样当clean()执行完后,Cleaner就是一个无引用指向的对象了,也就是可被GC回收的对象。

Java为什么还会存在内存泄漏?
直接原因就是守护线程优先级比较低(Thread.MAX_PRIORITY- 2),运行的时间比较少。如果较短时间内创建较多的原对象,就会因为守护线程来不及弹出原对象而使FinalizerReference和原对象都得不到回收。无论怎样调用GC都没有用的,因为只要原对象没有被守护线程弹出执行其finalize()方法,FinalizerReference对象就不会被GC回收。

finalizer.setPriority(Thread.MAX_PRIORITY- 2);//为什么守护线程优先级比较低?
每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。
并非线程优先级越高的就一定先执行,哪个线程的先运行取决于CPU的调度;
默认情况下main线程具有普通的优先级,而它创建的线程也具有普通优先级。
Thread对象的setPriority(int x)和getPriority()来设置和获得优先级。
MAX_PRIORITY :值是10
MIN_PRIORITY :值是1
为什么还会存在内存泄漏?
直接原因就是守护线程优先级比较低(Thread.MAX_PRIORITY- 2),运行的时间比较少。如果较短时间内创建较多的原对象,就会因为守护线程来不及弹出原对象而使FinalizerReference和原对象都得不到回收。无论怎样调用GC都没有用的,因为只要原对象没有被守护线程弹出执行其finalize()方法,FinalizerReference对象就不会被GC回收。

finalizer.setPriority(Thread.MAX_PRIORITY- 2);//为什么守护线程优先级比较低?
每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。
并非线程优先级越高的就一定先执行,哪个线程的先运行取决于CPU的调度;
默认情况下main线程具有普通的优先级,而它创建的线程也具有普通优先级。
Thread对象的setPriority(int x)和getPriority()来设置和获得优先级。
MAX_PRIORITY :值是10
MIN_PRIORITY :值是1
NORM_PRIORITY :值是5(主方法默认优先级)

https://www.cnblogs.com/aspirant/p/11734918.html
https://www.javatpoint.com/Garbage-Collection
https://blog.csdn.net/weixin_35632331/article/details/112730275

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值