【Java】深入理解Java虚拟机2——判断对象是否存活和引用

为什么我们还要去了解GC和内存分配呢?答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。

其他文章

【Java】深入理解Java虚拟机1——内存区域以及OOM类型:https://www.jianshu.com/p/65c91ba4006e
【Java】深入理解Java虚拟机2——判断对象是否存活和引用:https://www.jianshu.com/p/67c24aa93c03
【Java】深入理解Java虚拟机3——垃圾收集算法:https://www.jianshu.com/p/362407886236
【Java】深入理解Java虚拟机4——内存分配与回收策略:https://www.jianshu.com/p/e21f5d5c4f42
【Java】深入理解Java虚拟机5——类的加载过程:https://www.jianshu.com/p/931ef115d48e
【Java】深入理解Java虚拟机6——类的加载器及双亲委派:https://www.jianshu.com/p/2f33eca93a4f

垃圾回收针对区域

Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个 栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化,但在本章基于概念模型的讨论中,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆和方法区则不一 样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存,本文后续讨论中的“内存”分配与回收也仅指这一部分内存。

判断对象是否存活

1.引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。 下面举个例子:
对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

    /**
     * testGC()方法执行后,objA和objB会不会被GC呢? *@author zzm
     */
    public class ReferenceCountingGC {
        public Object instance = null;private static final int_1MB=1024*1024;
        /**
         * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
         */
        private byte[] bigSize = new byte[2 * _1MB];

        public static void testGC() {
            ReferenceCountingGC objA = new ReferenceCountingGC();
            ReferenceCountingGC objB = new ReferenceCountingGC();objA.instance = objB;
            objB.instance = objA;objA = null;
            objB = null; //假设在这行发生GC,objA和objB是否能被回收? System.gc(); } }
        }
    }

运行结果:
【Java】深入理解Java虚拟机2——判断对象是否存活和引用_第1张图片
image.png

从运行结果中可以清楚看到,GC日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数算法来判 断对象是否存活的。

2.可达性分析算法

在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图:,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
【Java】深入理解Java虚拟机2——判断对象是否存活和引用_第2张图片
可达性分析算法.png

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

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

再谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。 我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(PhantomReference)4种,这4种引用强度依次逐渐减弱。
强引用就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。比如你创建一个很长的数组Object[] objArr = new Object[10000];当运行至这句时,如果内存不足,JVM会抛出OOM错误也不会回收数组中的object对象。不过要注意的是,当方法运行完之后,数组和数组中的对象都已经不存在了,所以它们指向的对象都会被JVM回收。
软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。一般可用来实现缓存,在使用之前需要判空,从而判断当前时候已经被回收了。
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,通过java.lang.ref.WeakReference或java.util.WeakHashMap类实现,eg : WeakReference p = new WeakReference(new Person("Rain"));不管内存是否足够,系统垃圾回收时必定会回收。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。不能单独使用,主要是用于追踪对象被垃圾回收的状态。通过java.lang.ref.PhantomReference类和引用队列ReferenceQueue类联合使用实现。

finalize()

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处 于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达 性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选, 筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

/**
     * 此代码演示了两点: *1.对象可以在被GC时自我拯救。 *2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 *@author zzm
     */
    public class FinalizeEscapeGC {
        public static FinalizeEscapeGC SAVE_HOOK = null;

        public void isAlive() {
            System.out.println("yes,i am still alive:)");
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("finalize mehtod executed!");
            FinalizeEscapeGC.SAVE_HOOK = this;
        }

        public static void main(String[] args) throws Throwable {
            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 mehtod executed!
    yes,i am still alive:)
    no,i am dead:(

一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次, 如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。
笔者建议大家尽量避免使用它,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。有些教材中描述它适合做“关闭外部资源”之类的工作,这完全是对这个方法用途的一种自我安慰。 finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时。

错误不足之处或相关建议欢迎大家评论指出,谢谢!如果觉得内容可以的话麻烦喜欢(♥)一下

你可能感兴趣的:(【Java】深入理解Java虚拟机2——判断对象是否存活和引用)