zoukankan      html  css  js  c++  java
  • java基础:强引用、弱引用、软引用和虚引用 (转)

    出处文章:

      Java基础篇 - 强引用、弱引用、软引用和虚引用

      谈谈Java对象的强引用,软引用,弱引用,虚引用分别是什么


    整体结构

    java提供了4中引用类型,在垃圾回收的时候,都有自己的各自特点。

    为什么要区分这么多引用呢,其实这和Java的GC有密切关系。

    强引用(默认支持模式)

    • 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
    • 强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着
    • 当内存不足的时候,jvm开始垃圾回收,对于强引用的对象,就算出现OOM也不会回收该对象的。
      因此,强引用是造成java内存泄露的主要原因之一。
    • 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示的将引用赋值为null,GC就会回收这个对象了。

    案例

      public static void main(String[] args) {
            Object obj=new Object();//这样定义就是一个强引用
            Object obj2=obj;//也是一个强引用
            obj=null;
            System.gc();
            //不会被垃圾回收
            System.out.println(obj2);
        }

    软引用(SoftReference)

    • 软引用是一种相对强化引用弱化了一些引用,需要使用java.lang.SoftReference类来实现。
    • 对于只有软引用的对象来说,
      当系统内存充足时,不会被回收;
      当系统内存不足时,会被回收;

    案例

    /**
         * jvm配置配置小的内存,故意产生大的对象,导致OOM,
         * 验证软引用在内存足够的前后是否被回收。
         * 参数:-Xms:5M -Xmx:5M
         * @param args
         */
        public static void main(String[] args) {
            Object obj=new Object();//这样定义就是一个强引用
            //软引用需要使用java.lang.SoftReference来实现
            //现在sf就是一个软引用
            SoftReference sf=new SoftReference(obj);
    
            obj=null;
    
            System.out.println("内存足够软引用引用的对象"+sf.get());
    
            try {
                final byte[] bytes = new byte[8 * 1024 * 1024];
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                System.out.println("内存不够:软引用引用的对象:"+sf.get());
            }
    
        }

    结果:

    软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

    弱引用

    • 弱引用需要用java.lang.WeakReference类来实现,它比软引用的生存期更短。

    * 如果一个对象只是被弱引用引用者,那么只要发生GC,不管内存空间是否足够,都会回收该对象。

    • ThreadLocal静态内部类ThreadLocalMap中的Entiry中的key就是一个虚引用;

    案例

    public static void main(String[] args) {
            Object obj=new Object();
            WeakReference wrf=new WeakReference(obj);
            obj=null;
            System.out.println("未发生GC之前"+wrf.get());
            System.gc();
            System.out.println("内存充足,发生GC之后"+wrf.get());
        }

    结果:

    未发生GC之前java.lang.Object@2d363fb3
    内存充足,发生GC之后null

    你知道弱引用的话,能谈谈WeakHashMap吗?

    WeakHashMap的键是“弱键”,也就是键的引用是一个弱引用。

    public static void main(String[] args) {
            WeakHashMap<String,Integer> map=new WeakHashMap<>();
            String key = new String("wekHashMap");
            map.put(key,1);
            key=null;
            System.gc();
            System.out.println(map);
        }

    结果:map为空了。
    理论上我们只是把引用变量key变成null了,"wekHashMap"字符串应该被Map中key引用啊,不应该被GC回收啊,
    但是因为key是弱引用,GC回收的时候就忽略了这个引用,把对象当成垃圾收回了。

    虚引用

    • 虚引用需要 java. langref.PhantomReference类来实现。
    • 顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
      如果一个对象仅被虛引用持有,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
    • 它不能单独使用也不能通过它访问对象,虚引用必须和引用队列( Reference queue)联合使用。
    • 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize以后,做某些事情的机制。
    • PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。
      使用它的意义在于说明一个对象已经进入 finalization阶段,可以被回收,用来实现比 finalization机制更灵活的回收操作
      换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理;

    ReferenceQueue 引用队列

    • 对象在被回收之前要被引用队列保存一下。GC之前对象不放在队列中,GC之后才对象放入队列中。
    • 【通过开启线程监听该引用队列的变化情况】就可以在对象被回收时采取相应的动作。
      由于虚引用的唯一目的就是能在这个对象被垃圾收集器回收时能收到系统通知,因而创建虚引用时必须要关联一个引用队列,而软引用和弱引用则不是必须的。
      这里所谓的收到系统通知其实还是通过开启线程监听该引用队列的变化情况来实现的。
    • 这里还需要强调的是,
      对于软引用和弱引用,当执行第一次垃圾回收时,就会将软引用或弱引用对象添加到其关联的引用队列中,然后其finalize函数才会被执行(如果没复写则不会被执行);
      而对于虚引用,如果被引用对象没有复写finalize方法,则是在第一垃圾回收将该类销毁之后,才会将虚拟引用对象添加到引用队列,
      如果被引用对象复写了finalize方法,则是当执行完第二次垃圾回收之后,才会将虚引用对象添加到其关联的引用队列
    class User{
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("我要被GC干了!");
        }
    }
     public static void main(String[] args) throws  Exception {
            User user=new User();
            ReferenceQueue<User> queue=new ReferenceQueue();
            PhantomReference prf=new PhantomReference(user,queue);
    
            //启动一个线程监控引用队列的变化
            new Thread(()->{
                for(;;){
                    final Reference<? extends User> u = queue.poll();
                    if (u!=null){
                        System.out.println("有对象被加入到了引用队列了!"+u);
                    }
                }
            }).start();
    
            user=null;
            //GC之前引用队列为空
            System.out.println("GC之前"+queue.poll());
            
            System.gc();
            Thread.sleep(100);
            //GC之后引用队列才将对象放入
            System.out.println("第一次GC之后"+queue.poll());
    
            System.gc();
            Thread.sleep(100);
            System.out.println("第二次GC之后"+queue.poll());
    
        }

    结果:

    GC之前null
    我要被GC干了!
    第一次GC之后null
    有对象被加入到了引用队列了!java.lang.ref.PhantomReference@549763fd
    第二次GC之后java.lang.ref.PhantomReference@5aaa6d82 

    应用场景

    软引用:SoftReference的应用场景

    假如有一个应用需要读取大量的本地图片
    每次读取图片都从硬盘读取会影响性能。
    一次全部加载到内存中,又可能造成内存溢出。
    此时,可以使用软引用解决问题;
    使用一个HashMap保存图片的路径和响应图片对象关联的软引用之间的映射关系,
    内存不足时,jvm会自动回收这些缓存图片对象所占用的空间,可以避免OOM。

    Map<String,SoftReference<Bigmap>> imageCache=new HashMap<String,SoftReference<Bitmap>>();

    另一篇关于SoftReference的介绍

    1)为什么需要使用软引用?

      主要是为了更好的进行内存管理而设置的一套机制,粗俗的说就是不同的引用垃圾回收的力度不同。

      定义Java对象重要性和优先级,提高JVM执行效率。

      首先,我们看一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档 案信息。作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览WEB页面的时候也经常会使用“后退”按 钮)。这时我们通常会有两种程序实现方式:一种是把过去查看过的雇员信息保存在内存中,每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程 序始终;另一种是当用户开始查看其他雇员的档案信息的时候,把存储了当前所查看的雇员档案信息的Java对象结束引用,使得垃圾收集线程可以回收其所占用 的内存空间,当用户再次需要浏览该雇员的档案信息的时候,重新构建该雇员的信息。很显然,第一种实现方法将造成大量的内存浪费,而第二种实现的缺陷在于即 使垃圾收集线程还没有进行垃圾收集,包含雇员档案信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。我们知道,访问磁盘文件、访问网络资 源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的Java对象的引用,必将减少不必要的访问,大大提高程序的运 行速度。

    2)如何使用软引用?

      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
    }

    理解了ReferenceQueue的工作机制之后,我们就可以开始构造一个Java对象的高速缓存器了。

    4)通过软可及对象重获方法实现Java对象的高速缓存

      利用Java2平台垃圾收集机制的特性以及前述的垃圾对象重获方法,我们通过一个雇员信息查询系统的小例子来说明如何构建一种高速缓存器来避免重复构建同一个对象带来的性能损失。我们将一个雇员的档案信息定义为一个Employee类:

    public class Employee {
      private String id;// 雇员的标识号码
      private String name;// 雇员姓名
      private String department;// 该雇员所在部门
      private String Phone;// 该雇员联系电话
      private int salary;// 该雇员薪资
      private String origin;// 该雇员信息的来源
    
    // 构造方法
    
    public Employee(String id) { 
         this.id = id;
         getDataFromlnfoCenter();
    }
    
    // 到数据库中取得雇员信息
    
    private void getDataFromlnfoCenter() {
      // 和数据库建立连接井查询该雇员的信息,将查询结果赋值
      // 给name,department,plone,salary等变量
      // 同时将origin赋值为"From DataBase"
    }
    
    ……

      这个Employee类的构造方法中我们可以预见,如果每次需要查询一个雇员的信息。哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这是需要消耗很多时间的。下面是一个对Employee对象进行缓存的缓存器的定义:

    import java.lang.ref.ReferenceQueue;
    import java.lang.ref.SoftReference;
    import java.util.Hashtable;
    
    public class EmployeeCache {
       static private EmployeeCache cache;// 一个Cache实例
       private Hashtable< String,EmployeeRef> employeeRefs;// 用于Chche内容的存储
       private ReferenceQueue< Employee> q;// 垃圾Reference的队列
    
       // 继承SoftReference,使得每一个实例都具有可识别的标识。
       private class EmployeeRef extends SoftReference< Employee> {
         private String _key = "";
         public EmployeeRef(Employee em, ReferenceQueue< Employee> q) {
          super(em, q);
          _key = em.getID();
        }
       }
    
      // 构建一个缓存器实例
        private EmployeeCache() {
           employeeRefs = new Hashtable<String,EmployeeRef>();
           q = new ReferenceQueue<Employee>();
       }
    
        // 取得缓存器实例
    
    public static EmployeeCache getInstance() {
        if (cache == null) {
           cache = new EmployeeCache();
       }
        return cache;
    
    }
    
      // 以软引用的方式对一个Employee对象的实例进行引用并保存该引用
       private void cacheEmployee(Employee em) {
         cleanCache();// 清除垃圾引用
         EmployeeRef ref = new EmployeeRef(em, q);
         employeeRefs.put(em.getID(), ref);
       }
    
       // 依据所指定的ID号,重新获取相应Employee对象的实例
        public Employee getEmployee(String ID) {
           Employee em = null;
            // 缓存中是否有该Employee实例的软引用,如果有,从软引用中取得。
           if (employeeRefs.containsKey(ID)) {
              EmployeeRef ref = (EmployeeRef) employeeRefs.get(ID);
              em = (Employee) ref.get();
           }
    
    // 如果没有软引用,或者从软引用中得到的实例是null,重新构建一个实例,
    // 并保存对这个新建实例的软引用
    
        if (em == null) {
           em = new Employee(ID);
            System.out.println("Retrieve From EmployeeInfoCenter. ID=" + ID);
            this.cacheEmployee(em);
         }
         return em;
       }
    
    private void cleanCache() {
       EmployeeRef ref = null;
       while ((ref = (EmployeeRef) q.poll()) != null) {
        employeeRefs.remove(ref._key);
       }
       }
    
    // 清除Cache内的全部内容
    
    public void clearCache() {
       cleanCache();
        employeeRefs.clear();
        System.gc();
        System.runFinalization();
       }
    
    }

      注:原来ReferenceQueue起到一 个监听器的效果,当发现SoftReference.get()方法返回的是null值时,就会将SoftReference注册到自己里面队列里,当我 们调用ReferenceQueue的poll()方法时,返回并删除该SoftReference。

  • 相关阅读:
    家庭记账本APP开发准备(二)
    使用花生壳5做内网穿透
    课堂练习之可视化的强化版
    第五周总结
    课堂练习之疫情可视化
    第四周总结
    第三周总结
    第二周总结
    课堂练习之最大子数组
    软工第二学期开课博客
  • 原文地址:https://www.cnblogs.com/myseries/p/11823930.html
Copyright © 2011-2022 走看看