• Java引用类型原理


    1、前言
     
    Jvm垃圾收集中,非要判断对象是否可回收,无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。 
     
    强引用就是我们经常使用的Object a = new Object()这样的形式,在Java中并没有对应的Reference类。
    其实object是栈中分配的一个引用,而new Object()是在堆中分配的一个对象。而'='的作用是用来将引用指向堆中的对象的。就像你叫张三但张三是个名字而已并不是一个实际的人,他只是指向的你。
    软引用、弱引用、虚引用的实现,这三种引用类型都是继承于Reference这个类,主要逻辑也在Reference中。那么下述问题:
    1. 对于软引用的介绍是:在内存不足的时候才会被回收,那内存不足是怎么定义的?什么才叫内存不足?
    2. 虚引用的介绍是:形同虚设,虚引用并不会决定对象的生命周期。主要用来跟踪对象被垃圾回收器回收的活动。真的是这样吗?
    3. 虚引用在Jdk中有哪些场景下用到了呢?
     
    Q:Java为什么要设计这四种引用?
    Java的内存分配和内存回收,都不需要程序员负责,都是由JVM去负责,一个对象是否可以被回收,主要看是否有引用指向此对象。Java设计这四种引用的主要目的有两个:
    • 可以让程序员通过代码的方式来决定某个对象的生命周期;
    • 有利用垃圾回收。
    2、强引用
    强引用是最普遍的一种引用,我们写的代码,99.9999%都是强引用:
    Object obj = new Object();
    

    这种就是强引用了,是不是在代码中随处可见,最亲切。只要某个对象有强引用与之关联,这个对象永远不会被回收,即使内存不足,JVM宁愿抛出OOM,也不会去回收。

     那么什么时候才可以被回收呢?当强引用和对象之间的关联被中断了,就可以被回收了。我们可以手动把关联给中断了,方法也特别简单:
    obj = null;
    

    我们可以手动调用GC,看看如果强引用和对象之间的关联被中断了,资源会不会被回收,为了更方便、更清楚的观察到回收的情况,我们需要新写一个类,然后重写finalize方法,下面我们来进行这个实验:

    public class Student {   
      @Override
        protected void finalize() throws Throwable {
            System.out.println("Student 被回收了");
        }
    }
    public static void main(String[] args) {
        Student student = new Student();
        student = null;
        System.gc();
    }

    运行结果:

    Student 被回收了

    可以很清楚的看到资源被回收了。当然,在实际开发中,千万不要重写finalize方法。在实际的开发中,看到有一些对象被手动赋值为null,很大可能就是为了“特意提醒”JVM这块资源可以进行垃圾回收了。

     
    3、软引用SoftReference
     
    下面先来看看如何创建一个软引用:
    SoftReference<Student>  studentSoftReference = new SoftReference<Student>(new Student());
    

    软引用就是把对象用SoftReference包裹一下,当我们需要从软引用对象获得包裹的对象,只要get一下就可以了:

    SoftReference<Student>studentSoftReference=new SoftReference<Student>(new Student());
    Student student = studentSoftReference.get();
    System.out.println(student);
    

    软引用特点:当内存不足,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用的包裹的对象给干掉,也就是只有在内存不足,JVM才会回收该对象。

     
    还有:
    SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[1024*1024*10]);
    System.out.println(softReference.get());
    System.gc();
    System.out.println(softReference.get());        
    byte[] bytes = new byte[1024 * 1024 * 10];
    System.out.println(softReference.get());
    

    上述定义了一个软引用对象,里面包裹了byte[],byte[]占用了10M,然后又创建了10Mbyte[]。

     运行程序,需要带上一个参数:
    -Xmx20M //代表最大堆内存是20M。
    运行结果:
    [B@11d7fff
    [B@11d7fff
    null
    

    可以很清楚的看到手动完成GC后,软引用对象包裹的byte[]还活的好好的,但是当我们创建了一个10M的byte[]后,最大堆内存不够了,所以把软引用对象包裹的byte[]给干掉了,如果不干掉,就会抛出OOM。

    软引用作用:比较适合用作缓存,当内存足够,可以正常的拿到缓存,当内存不够,就会先干掉缓存,不至于马上抛出OOM。
     
    注意软引用只会在内存不足的时候才触发,不会像强引用那用容易内存溢出,可以用其实现高速缓存,一方面内存不足的时候可以回收,一方面也不会频繁回收。
    在高速本地缓存Caffeine中实现了软引用的缓存,当需要缓存淘汰的时候,如果是只有软引用指向那么久会被回收。
     
    4、弱引用WeakReference
     
    弱引用的使用和软引用类似,只是关键字变成了WeakReference:
    WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1024*1024*10]);
    System.out.println(weakReference.get());
    

    弱引用的特点是不管内存是否足够,只要发生GC,都会被回收:

    WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1]);
    System.out.println(weakReference.get());
    System.gc();
    System.out.println(weakReference.get());
    

    运行结果:

    [B@11d7fffnull
     
    可以很清楚的看到明明内存还很充足,但是触发了GC,资源还是被回收了。弱引用在很多地方都有用到,比如ThreadLocal、WeakHashMap。
     
    注意:ThreadLocalMap中使用的 key 为ThreaLocal的弱引用,而 value 是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。
     
    4.1、ThreadLocal与弱引用
    ThreadLocal是一个本地线程副本变量工具类。ThreadLocal和弱引用有什么关系呢?
     
    在我们的Thread类中有下面这个变量:
    • ThreadLocal.ThreadLocalMap   threadLocals
    ThreadLocalMap本质上也是个Map,其中Key是我们的ThreadLocal这个对象,Value就是我们在ThreadLocal中保存的值。也就是说我们的ThreadLocal保存和取对象都是通过Thread中的ThreadLocalMap来操作的,而key就是本身。在ThreadLocalMap中Entry有如下定义:
    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                 super(k);
                 value = v;
           }
     }
    

    可以看见Entry是WeakReference的子类,而这个弱引用所关联的对象正是我们的ThreadLocal这个对象。那么问题:

    "threadlocal的key是弱引用,那么在threadlocal.get()的时候,发生GC之后,key是否是null?"
     
    这个问题晃眼一看,弱引用嘛,还有垃圾回收那肯定是为null,这其实是不对的,因为题目说的是在做threadlocal.get()操作,证明其实还是有强引用存在的。所以key并不为null。如果我们的强引用不存在的话,那么Key就会被回收,也就是会出现我们value没被回收,key被回收,导致value永远存在,出现内存泄漏。这也是ThreadLocal经常会被很多书籍提醒到需要remove()的原因。
     
    你也许会问看到很多源码的ThreadLocal并没有写remove依然再用得很好呢?那其实是因为很多源码经常是作为静态变量存在的生命周期和Class是一样的,而remove需要在那些方法或者对象里面使用ThreadLocal,因为方法栈或者对象的销毁从而强引用丢失,导致内存泄漏。
     
    5、虚引用PhantomReference
     
    虚引用是最弱的引用,弱到什么地步呢?也就是你定义了虚引用根本无法通过虚引用获取到这个对象,更别谈影响这个对象的生命周期了。在虚引用中唯一的作用就是用队列接收对象即将死亡的通知。
    虚引用又被称为幻影引用,我们来看看它的使用:
    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference<byte[]> reference = new PhantomReference<byte[]>(new byte[1], queue);
    System.out.println(reference.get());
    

    虚引用的使用和上面说的软引用、弱引用的区别还是挺大的,直接来运行:

    null
     
    竟然打印出了null,我们来看看get方法的源码:
    public T get() {        
      return null;
    }
    

    直接返回了null。

     
    这就是虚引用特点之一了:无法通过虚引用来获取对一个对象的真实引用!!!!
     
    那虚引用存在的意义是什么呢?这就要回到我们上面的代码了
    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference<byte[]> reference = new PhantomReference<byte[]>(new byte[1], queue);
    System.out.println(reference.get());
    

    创建虚引用对象,我们除了把包裹的对象传了进去,还传了一个ReferenceQueue,从名字就可以看出它是一个队列。

     虚引用的特点之二就是 虚引用必须与ReferenceQueue一起使用,当GC准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的ReferenceQueue中。
     
    测试一下:
    ReferenceQueue queue = new ReferenceQueue();
    List<byte[]> bytes = new ArrayList<>();
    PhantomReference<Student> reference = new PhantomReference<Student>(new Student(),queue);
    new Thread(() -> {
        for (int i = 0; i < 100;i++ ) {
            bytes.add(new byte[1024 * 1024]);
        }
    }).start();
     
    new Thread(() -> {
        while (true) {
            Reference poll = queue.poll();
            if (poll != null) {
                System.out.println("虚引用被回收了:" + poll);
            }
        }
    }).start();
    Scanner scanner = new Scanner(System.in);
    scanner.hasNext();
    }
    运行结果:
    Student 被回收了
    虚引用被回收了:java.lang.ref.PhantomReference@1ade6f1
    

     

    我们简单的分析下代码:
    • 第一个线程往集合里面塞数据,随着数据越来越多,肯定会发生GC。
    • 第二个线程死循环,从queue里面拿数据,如果拿出来的数据不是null,就打印出来。
     
    从运行结果可以看到:当发生GC,虚引用就会被回收,并且会把回收的通知放到ReferenceQueue中。
     
    虚引用有什么用呢?在NIO中,就运用了虚引用管理堆外内存。
     
    6、Reference
     
    看下Reference.java中的几个字段
    public abstract class Reference<T> {
        //引用的对象
        private T referent;        
        //回收队列,由使用者在Reference的构造函数中指定
        volatile ReferenceQueue<? super T> queue;
        //当该引用被加入到queue中的时候,该字段被设置为queue中的下一个元素,以形成链表结构
        volatile Reference next;
        //在GC时,JVM底层会维护一个叫DiscoveredList的链表,存放的是Reference对象,discovered字段指向的就是链表中的下一个元素,由JVM设置
        transient private Reference<T> discovered;  
        //进行线程同步的锁对象
        static private class Lock { }
        private static Lock lock = new Lock();
        //等待加入queue的Reference对象,在GC时由JVM设置,会有一个java层的线程(ReferenceHandler)源源不断的从pending中提取元素加入到queue
        private static Reference<Object> pending = null;
    }
     
    

    针对文章开头提出的几个问题,看完分析,我们已经能给出回答:

    1.我们经常在网上看到软引用的介绍是:在内存不足的时候才会回收,那内存不足是怎么定义的?为什么才叫内存不足?
    • 软引用会在内存不足时被回收,内存不足的定义和该引用对象get的时间以及当前堆可用内存大小都有关系,计算公式在上文中也已经给出。
    2.网上对于虚引用的介绍是:形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。主要用来跟踪对象被垃圾回收器回收的活动。真的是这样吗?
    • 严格的说,虚引用是会影响对象生命周期的,如果不做任何处理,只要虚引用不被回收,那其引用的对象永远不会被回收。所以一般来说,从ReferenceQueue中获得PhantomReference对象后,如果PhantomReference对象不会被回收的话(比如被其他GC ROOT可达的对象引用),需要调用clear方法解除PhantomReference和其引用对象的引用关系。
    3.虚引用在Jdk中有哪些场景下用到了呢?
    • DirectByteBuffer中是用虚引用的子类Cleaner.java来实现堆外内存回收的。
     
     
     
     
     
     
  • 相关阅读:
    百度搜索技巧
    phpstorm知识点
    A-Z
    边框
    display
    布局
    盒模型
    浮动
    字体与图标
    pselect 问题
  • 原文地址:https://www.cnblogs.com/liujiarui/p/13905598.html
Copyright © 2020-2023  润新知