美文网首页我爱编程
《深入理解Java虚拟机》(二)--垃圾收集器与内存分配策略(1

《深入理解Java虚拟机》(二)--垃圾收集器与内存分配策略(1

作者: 蓝色_fea0 | 来源:发表于2018-05-26 22:50 被阅读102次

    在Java虚拟机中如何判断对象是否“存活”和“死去”?
    下面就开始讲述我今天学到的两种算法:

    1/1引用计数算法(Reference Counting):

    概述:

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

    优点:

    实现简单,判定效率很高,大部分情况下是一个不错的算法,也有一些比较著名的案例:例如微软公司的COM(Component Object Model)技术等等。

    缺点:

    Java虚拟机中并没有选择使用这种算法来管理内存,原因就是它很难解决对象之间相互循环引用的问题。

    举个栗子:

    public class TestReference {
        public Object object = null;
        
        public static void main(String[] args) {
            TestReference t1 = new TestReference();
            TestReference t2 = new TestReference();
            t1.object = t2;
            t2.object = t1;
            
            t1 = null;
            t2 = null;
            //假设在这里发生GC,那么t1和t2是否会被回收
                    System.gc();
        }
    }
    

    上述代码中,实际上t1和t2对象已经不可能再被访问,但是他们都互相引用着对方,所以引用计数器都不为0,于是引用计数器算法就无法回收他们。

    1/2可达性分析算法(Reachability Anaysis):

    概述:

    在主流的商用程序语言(Java,C#..)的主流实现中,都是称通过可达性分析来判断对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径叫做引用链(Reference Chain),当一个对象到GC Roots没有任何一个引用链相连时,就说明这个对象是不可用的,如图,虽然object5、6、7有所关联,但是没有到GC Roots的引用链,所以是不可以用的。这就很好的解决了对象的循环引用的问题。


    可达性分析算法判断对象是否可以回收

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

    1、虚拟机栈(栈帧中的本地变量表)所引用的对象(正在使用的某个方法或者类)。
    2、方法区中类静态属性引用的对象。
    3、方法区中常量引用的对象(不是很懂.....,可能就指的是一般的常量吧)。
    4、本地方法栈中JNI(即之前博客中所提到的Native方法)引用的对象。

    1/3引用

    概述:

    在JDK1.2以前,Java中的引用定义为:如果reference类型的数据中储存的数值代表的另外一块内存的起始地址,就称这块reference内存代表着一个引用(即C语言中的指针)。

    JDK1.2之后,针对那些“食之无味,弃之可惜”的对象Java对引用的概念进行了扩充,主要分为以下几种:

    1、强引用:
    表示代码中普遍存在的,例如 Student s = new Student(),只要强引用存在,垃圾回收器就永远不会回收被引用的对象。
    2、软引用:
    用来描述一些“有那么一点用,但不是很必须的对象”,当内存比较紧张时(比如即将发生内存溢出异常),会将这些对象列进回收的范围之中,进行二次回收。如果这次回收还没有空出足够的内存空间,才会抛出内存溢出异常。SoftReference类来实现软引用
    3、弱引用:
    与软引用差不多,不过它只能生存到下次垃圾回收之前。WeakReference类来实现。
    4、虚引用:
    也称为幽灵(幻影)引用。。。貌似还挺炫酷,它是最弱的一种引用关系,如果一个对象引用了虚引用的对象(例如 student = (虚引用),可能不太对,但应该差不多是这个意思),完全不会对该对象造成任何的影响,而且也无法通过虚引用来获取到一个对象的实例。PhantomReference类来实现。

    1/4对象自救的机会

    实际上,要真正的宣布一个对象死亡,至少要经过两次标记的过程。

    第一次标记:

    当一个对象在可达性分析后,被发现已经没有引用链了(上文提到的),那么它会被第一次标记,并且进行一个筛选,筛选的条件就是这个对象是否重写finalize()方法(我竟然不知道Object中竟然还有这么一个方法....)。当对象没有重写finalize()方法或者是finalize()方法已经被虚拟机调用过一次,那么这个对象就可以去死了。

    第二次标记

    如果对象在第一次标记后被判断能够执行finalize()方法,那么它会被放置在一个F-Queue队列中,并在稍后由一个虚拟机自己创建的低优先级线程Finalizer中去执行finalize()方法。但也不会老老实实的等finalize()方法运行完毕,就是说如果你的finalize()方法是一个死循环,人家这个线程不会让你一直执行下去的。

    成功的拯救了自己

    如果这个对象在finalize()方法中建立了有效的引用,那么它就可以复活。

    一个栗子:

    
    public class TestFinallize {
    
        public static TestFinallize SAVE_ME = null;
    
        public void isAlive() {
            System.out.println("我还活着!!!");
        }
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("我救了我自己!!!");
            TestFinallize.SAVE_ME = this;
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            // 对象第一次拯救了自己
            SAVE_ME = new TestFinallize();
            SAVE_ME = null;
            System.gc();
            // 因为 finalize()优先级很低,所以要等待一会
            Thread.sleep(500);
            if (SAVE_ME != null) {
                SAVE_ME.isAlive();
            } else {
                System.out.println("我已经死了");
            }
    
            // 第二次没能救回自己
            SAVE_ME = null;
            System.gc();
            Thread.sleep(500);
            if (SAVE_ME != null) {
                SAVE_ME.isAlive();
            } else {
                System.out.println("我已经死了");
            }
        }
    }
    
    

    运行结果:


    测试自救结果

    总结:

    从上面的代码中可以看出,第一执行垃圾回收时,finalize()方法确实实现了自救。但是第二次执行中没能自救成功,说明finalize()方法只能被执行一次。并且是低优先级的。不过话说回来,这个finalize()方法我直到看了这本书之前一直不知道它的存在。。。原来是因为这个方法是Java刚诞生时,为了使C/C++程序员更容易接受它所作出的一个妥协,它的代码运行代价高昂,不确定性大,无法保证每个对象的调用顺序,finalize()方法所能做的事情,使用try-finally或者其他方式可以更好更快的完成。所以该方法已经不建议使用0.0.....

    1/5回收方法区

    概述

    其实方法区(HotSpot虚拟机中的永久代)也是有垃圾收集的,在JVM规范中确实说过可以不要求虚拟机在方法区实现垃圾收集。不过方法区的垃圾回收的性价比也确实很低。
    永久代的垃圾收集主要有两部分内容
    1、放弃的常量
    举一个栗子:当一个常量“abc”在常量池中没有被任何变量引用比如String a =“abc”(这算有变量变量引用的) ,如果这个时候刚好发生垃圾回收的话,而且很必要的话(就是内存不太够),那么这个“abc”就会被提出常量池。
    2、无用的类
    判断“无用的类”的方法会比较严苛,必须同时满足以下三点:
    1)该被的所有实例均已被回收。
    2)该类的类加载器ClassLoader已被回收。
    3)该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    虚拟机可以对满足以上3个条件的无用类进行回收,但并不是说必然回收。
    在大量使用反射、动态代理、CGLib等ByteCode框架,动态生成JSP以及OSGI这类频繁自定ClassLoader的场景都需要虚拟机具备类卸载(就是回收无用的类)的功能,以保证永久代不会溢出。

    相关文章

      网友评论

        本文标题:《深入理解Java虚拟机》(二)--垃圾收集器与内存分配策略(1

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