JVM(二)--对象已死?和引用问题

    技术2022-07-11  92

    JVM(二)–对象已死?和引用问题

    写在前面:

    java内存运行时区域的各个部分,其中程序计数器、虚拟机栈和本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行者出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具有确定性,在这几个内存内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。

    而java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收是动态的,垃圾收集器所关注的是这部分内存。

    对象已死吗?

    在堆里面存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”

    1. 引用计数算法:

    判断一个对象是否存活的算法通常是这样的:

    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

    缺点:

    ①每次对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗

    ②很多主流的java虚拟机里面没有选用引用技术算法来管理内存,主要的原因就是: 它很难解决对象之间相互循环引用的问题

    如:A=B;B=A;A=null;B=null; 这样A和B的引用计数器都为1.

    2. 可达性分析算法:

    该算法的基本思路:

    通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象的GC Roots没有任何引用链相连时,则证明此对象是不可用的

    (也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可达的)对象就被判定为存活,没有被遍历到的就自然是不可用的。)

    在java语言中,可作为GC Roots的对象包括以下几种:

    虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中JNI(一般说的native方法)引用的对象

    可作为GC Roots的节点主要在全局性的引用(例如常量或者类静态属性)与执行上下文(例如栈帧中的本地变量表)中。

    但是,即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

    如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功成就自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那再第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

    ==>也就是对象可以在被GC时自我拯救。这种自救的机会只有一次,因为对象的finalize()方法最多只会被系统自动调用一次。

    public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, i am still alive:"); } protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscapeGC(); //对象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); //因为finalize方法优先级很低,所以暂停0.5秒以等待它 Thread.sleep(500); if(SAVE_HOOK != null) { SAVE_HOOK.isAlive(); }else { System.out.println("no, i am dead"); } //下面这段代码与上面的完全相同,但是这次自救却失败了 SAVE_HOOK = null; System.gc(); //因为finalize方法优先级很低,所以暂停0.5秒以等待它 Thread.sleep(500); if(SAVE_HOOK != null) { SAVE_HOOK.isAlive(); }else { System.out.println("no, i am dead"); } } }

    运行结果:

    finalize method executed! yes, i am still alive: no, i am dead

    代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次逃脱失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行

    再谈引用:

    Java中的四种引用:

    Java中有四种引用类型:强引用、软引用、弱引用、虚引用。

    Java为什么要设计这四种引用?

    Java的内存分配和内存回收,都不需要程序员负责,都是由伟大的JVM去负责,一个对象是否可以被回收,主要看是否有引用指向此对象,说的专业点,叫可达性分析。

    Java设计这四种引用的主要目的有两个:

    可以让程序员通过代码的方式来决定某个对象的生命周期;有利用垃圾回收。

    1,强引用:

    强引用是最普遍的一种引用,我们写的代码,99.9999%都是强引用:

    Object o = new Object();

    这种就是强引用了,是不是在代码中随处可见,最亲切。

    只要某个对象有强引用与之关联,这个对象永远不会被回收,即使内存不足,JVM宁愿抛出OOM,也不会去回收。(死都不回收),即使,该对象以后永远不会被用到,JVM也不会回收。因此,强引用也是造成内存泄露的主要原因

    那么什么时候才可以被回收呢?当强引用和对象之间的关联被中断了,就可以被回收了。

    我们可以手动把关联给中断了,方法也特别简单:

    o = 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这块资源可以进行垃圾回收了。

    2,软引用:

    软引用:是一种相对强引用弱化了一些的引用,需要使用java.lang.ref.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。

    3,弱引用:

    弱引用的使用和软引用类似,它比软引用的生存周期短,只是关键字变成了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@11d7fff null

    可以很清楚的看到明明内存还很充足,但是触发了GC,资源还是被回收了。弱引用在很多地方都有用到,比如ThreadLocal、WeakHashMap。

    软引用和弱引用的适用场景:

    如:有一个应用需求读取大量的本地图片:若每次读取图片都要从硬盘读取则会严重影响性能,若一次性全部加载到内存中又可能造成内存溢出。这时候就可以使用软/弱引用来解决这个问题。

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

    只要是WeakHashMap做缓存时,只要一GC,马上这个key/value就被移除。

    4,虚引用:

    虚引用又被称为幻影引用,我们来看看它的使用:

    ReferenceQueue queue = new ReferenceQueue(); PhantomReference<byte[]> reference = new PhantomReference<byte[]>(new byte[1], queue); System.out.println(reference.get());

    虚引用的使用和上面说的软引用、弱引用的区别还是挺大的,我们先不管ReferenceQueue 是个什么鬼,直接来运行:

    null

    竟然打印出了null,我们来看看get方法的源码:

    public T get() { return null; }

    这是几个意思,竟然直接返回了null。其意义在于:说明一个对象已经进入了finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

    这就是虚引用特点之一了:无法通过虚引用来获取对一个对象的真实引用。

    那虚引用存在的意义是什么呢?这就要回到我们上面的代码了:

    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中,就运用了虚引用管理堆外内存。

    还有,虚引用的主要作用是:跟踪对象被垃圾回收的状态。仅仅是提供例如一种确保对象被finalize以后,做某些事情的机制。

    一些解释:

    ①java提供了4种引用类型,在垃圾回收的时候,都有各自的特点 ②ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样是可以运行的 ③创建引用的时候,可以指定关联的队列,当gc释放对象内存的时候,会将引用加入到引用队列 ④若程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动,这相当于一种通知机制 ⑤当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收,通过这种方式,jvm允许我们在对象被销毁之后,做一些我们自己想做的事情。

    小总结:

    换句话说,设置虚引用关联的唯一目的:就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。 当发生GC,虚引用就会被回收,并且会把回收的通知放到ReferenceQueue中 虚引用的主要作用是:跟踪对象被垃圾回收的状态。仅仅是提供例如一种确保对象被finalize以后,做某些事情的机制

    附上一个链接:

    https://mp.weixin.qq.com/s/Sk-oBGJpm8mev_GNNabqmw

    感谢并参考: java知音 https://blog.csdn.net/xiaojie_570/article/details/80171284

    Processed: 0.009, SQL: 9