美文网首页
《深入理解JVM虚拟机》读书笔记-对象引用&对象回收依据&回收方

《深入理解JVM虚拟机》读书笔记-对象引用&对象回收依据&回收方

作者: 乙腾 | 来源:发表于2021-10-27 17:50 被阅读0次

    引用

    引用

    定义

    • 传统定义

      • 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。
      • 狭隘性:
        • 这种定义并没有什么不对,只是现在看来有些过于狭隘了,一个对象在这种定义下只有<font color=red>“被引用”</font>或者<font color=red>“未被引用”</font>两种状态,对于描述一些“食之无味,弃之可惜”的对象就显得无能为力。
          • 比如:我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应用场景。
    • 在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为

      • 强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱

    四种引用介绍

    强引用:只要引用关系存在,永不回收

    • 介绍
      • 当在Java语言中使用new操作符创建一个新的对象, 并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。
    • 特点
      • <font color=red>无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象,所以强引用容易造成内存泄漏</font>。
    • 回收条件
      • 对于一一个普通的对象,如果没有其他的引用关系,只要<font color=red>超过了引用的作用域</font>或者<font color=red>显式地将相应(强)引用赋值为null</font>,就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。

    软引用:内存不足即回收

    • 介绍
      • 软引用是用来描述一些还有用,但非必须的对象。
    • 回收时机
      • 被软引用关联着的对象,在<font color=red>系统将要发生内存溢出异常前</font>,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
    • 用途
      • 缓存
        • 如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
    • 用途
      • 当内存足够时: 不会回收软引用的可达对象。
      • 当内存不够时: 会回收软引用的可达对象。

    使用范式

    Object obj = new object();/声明强引用
    SoftReference<0bject> sf = new SoftReference<0bject>(obj);
    obj = null;//销毁强引用
    

    例子

    /**
     * 软引用的测试:内存不足即回收
     * -Xms10m -Xmx10m -XX:+PrintGCDetails
     */
    public class SoftReferenceTest {
        public static class User {
            public User(int id, String name) {
                this.id = id;
                this.name = name;
            }
    
            public int id;
            public String name;
    
            @Override
            public String toString() {
                return "[id=" + id + ", name=" + name + "] ";
            }
        }
    
        public static void main(String[] args) {
            //创建对象,建立软引用
    //        SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));
            //上面的一行代码,等价于如下的三行代码
            User u1 = new User(1,"songhk");
            SoftReference<User> userSoftRef = new SoftReference<User>(u1);
            u1 = null;//取消强引用
    
    
            //从软引用中重新获得强引用对象
            System.out.println(userSoftRef.get());
    
            System.gc();
            System.out.println("After GC:");
    //        //垃圾回收之后获得软引用中的对象
            System.out.println(userSoftRef.get());//由于堆空间内存足够,所有不会回收软引用的可达对象。
    //
            try {
                //让系统认为内存资源紧张、不够
    //            byte[] b = new byte[1024 * 1024 * 7];
                byte[] b = new byte[1024 * 7168 - 399 * 1024];//恰好能放下数组又放不下u1的内存分配大小 不会报OOM
            } catch (Throwable e) {
                e.printStackTrace();
            } finally {
                //再次从软引用中获取数据
                System.out.println(userSoftRef.get());//在报OOM之前,垃圾回收器会回收软引用的可达对象。
            }
        }
    }
    
    

    弱引用:发现即回收

    • 介绍
      • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
    • 回收时机
      • 当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
      • 但是,由于<font color=red>垃圾回收器的线程通常优先级很低</font>,因此,并不一 定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
    • 用途
      • 软引用、弱引用都非常适合来保存那些可有可无的缓存数据。
      • 如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
    • 弱引用和软引用的区别
      • 当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。
      • 弱引用对象更容易、更快被GC回收。

    适用范式

    Object obj = new object(); //声明强引用
    WeakReference<0bject> sf = new WeakReference<0bject>(obj);
    obj = null; //销毁强引用
    

    例子

    public class WeakReferenceTest {
        public static class User {
            public User(int id, String name) {
                this.id = id;
                this.name = name;
            }
    
            public int id;
            public String name;
    
            @Override
            public String toString() {
                return "[id=" + id + ", name=" + name + "] ";
            }
        }
    
        public static void main(String[] args) {
            //构造了弱引用
            WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk"));
            //从弱引用中重新获取对象
            System.out.println(userWeakRef.get());
    
            System.gc();
            // 不管当前内存空间足够与否,都会回收它的内存
            System.out.println("After GC:");
            //重新尝试从弱引用中获取对象
            System.out.println(userWeakRef.get());
        }
    }
    
    

    虚引用:对象回收跟踪

    • 介绍
      • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。
      • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
    • 特点
      • 它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。
    • 用途
      • 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
      • 由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虛引用中执行和记录。
    • 使用注意
      • 虚引用必须和引用队列一起使用。
        • 虚引用在创建时必须提供一个引用队列作为参数。
        • 当垃圾回收器准备回收一个对象时,如果发现它还有虛引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

    使用范式

    #在JDK 1. 2版之后提供了PhantomReference类来实现虚引用
    object obj = new object();
    ReferenceQueuephantomQueue = new ReferenceQueue();
    PhantomReference<object> pf = new PhantomReference<object>(obj, phantomQueue); 
    obj = null;
    

    例子

    public class PhantomReferenceTest {
        public static PhantomReferenceTest obj;//当前类对象的声明
        static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用队列
    
        public static class CheckRefQueue extends Thread {
            @Override
            public void run() {
                while (true) {
                    if (phantomQueue != null) {
                        PhantomReference<PhantomReferenceTest> objt = null;
                        try {
                            objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if (objt != null) {
                            System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");
                        }
                    }
                }
            }
        }
    
        @Override
        protected void finalize() throws Throwable { //finalize()方法只能被调用一次!
            super.finalize();
            System.out.println("调用当前类的finalize()方法");
            obj = this;
        }
    
        public static void main(String[] args) {
            Thread t = new CheckRefQueue();
            t.setDaemon(true);//设置为守护线程:当程序中没有非守护线程时,守护线程也就执行结束。
            t.start();
    
            phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
            obj = new PhantomReferenceTest();
            //构造了 PhantomReferenceTest 对象的虚引用,并指定了引用队列
            PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
    
            try {
                //不可获取虚引用中的对象
                System.out.println(phantomRef.get());
    
                //将强引用去除
                obj = null;
                //第一次进行GC,由于对象可复活,GC无法回收该对象
                System.gc();
                Thread.sleep(1000);
                if (obj == null) {
                    System.out.println("obj 是 null");
                } else {
                    System.out.println("obj 可用");
                }
                System.out.println("第 2 次 gc");
                obj = null;
                System.gc(); //一旦将obj对象回收,就会将此虚引用存放到引用队列中。
                Thread.sleep(1000);
                if (obj == null) {
                    System.out.println("obj 是 null");
                } else {
                    System.out.println("obj 可用");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    输出

    null
    调用当前类的finalize()方法
    obj 可用
    第 2 次 gc
    追踪垃圾回收过程:PhantomReferenceTest实例被GC了
    obj 是 null
    

    判定对象可以回收的依据

    对象的finalization机制

    • 介绍
      • Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
    • 特点
      • 垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
      • finalize()方法允许在子类中被重写,用于<font color=red>在对象被回收时进行资源释放</font>。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
      • 从功能上来说,finalize()方法与C++ 中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()方法在本质,上不同于C++ 中的析构函数。
    • 注意事项
      • java的使用者永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。
      • 理由包括下面三点:
        • 在finalize() 时可能会导致对象复活。
        • finalize()方法的执行时间是没有保障的,它完全由Gc线程决定,极端情况下,若不发生GC,则finalize() 方法将没有执行机会。
        • 一个糟糕的finalize ()会严重影响GC的性能。

    对象在虚拟机中的三种状态

    • 不是所有不可达对象都必须被回收
      • 如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。
    • 由于finalize ()方法的存在,虚拟机中的对象一般处于三种可能的状态
      • 一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
        • 可触及的:从根节点开始,可以到达这个对象。
        • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
        • 不可触及的:对象的finalize()被调用过,并且没有复活,那么就会进入不可触及状态。<font color=red>不可触及的对象不可能被复活</font>,因为<font color=red>finalize() 只会被调用一一次</font>。

    判定是否可以回收具体过程

    判定一个对象objA是否可回收,至少要经历两次标记过程:

    • 如果对象objA到GC Roots没有引用链,则进行第一 次标记。
    • 进行筛选,判断此对象是否有必要执行finalize()方法
      • 如果对 象objA没有重写finalize()方法,或者finalize ()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
      • 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F一Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
      • <font color=red>finalize()方法是对象逃脱死亡的最后机会</font>,稍后Gc会对F一Queue队列中的对象进行第二次标记。
        • 如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。
        • 之后,对象会再次出现没有引用存在的情况,在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,<font color=red>一个对象的finalize方法只会被调用一次</font>。

    对象自我拯救的例子

    /**
     * 测试Object类中finalize()方法,即对象的finalization机制。
     *
     */
    public class CanReliveObj {
        public static CanReliveObj obj;//类变量,属于 GC Root
    
    
        //此方法只能被调用一次
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("调用当前类重写的finalize()方法");
            obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
        }
    
    
        public static void main(String[] args) {
            try {
                obj = new CanReliveObj();
                // 对象第一次成功拯救自己
                obj = null;
                System.gc();//调用垃圾回收器
                System.out.println("第1次 gc");
                // 因为Finalizer线程优先级很低,暂停2秒,以等待它
                Thread.sleep(2000);
                if (obj == null) {
                    System.out.println("obj is dead");
                } else {
                    System.out.println("obj is still alive");
                }
                System.out.println("第2次 gc");
                // 下面这段代码与上面的完全相同,但是这次自救却失败了
                obj = null;
                System.gc();
                // 因为Finalizer线程优先级很低,暂停2秒,以等待它
                Thread.sleep(2000);
                if (obj == null) {
                    System.out.println("obj is dead");
                } else {
                    System.out.println("obj is still alive");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    输出

    第1次 gc
    调用当前类重写的finalize()方法
    obj is still alive
    第2次 gc
    obj is dead
    
    

    回收方法区

    • 介绍
      • 有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,《Java虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载)
    • 特点:方法区垃圾收集的“性价比”通常也是比较低的
      • 在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收因为苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
    • 主要回收的两部分:
      • 废弃的常量
        • 回收废弃常量与回收Java堆中的对象非常类似。
        • 举个常量池中字面量回收的例子
          • 假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。
      • 不再使用的类型
        • 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
          • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
          • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
          • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
        • Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。
        • 关于是否要对类型进行回收虚拟机提供参数控制
          • HotSpot虚拟机提供了-Xnoclassgc参数进行控制
          • 还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息
            • 其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。
    • 回收方法区的必要性
      • 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

    相关文章

      网友评论

          本文标题:《深入理解JVM虚拟机》读书笔记-对象引用&对象回收依据&回收方

          本文链接:https://www.haomeiwen.com/subject/akupaltx.html