美文网首页面试题
深入JVM内核11 JVM垃圾回收

深入JVM内核11 JVM垃圾回收

作者: 香沙小熊 | 来源:发表于2020-02-27 17:15 被阅读0次

    1. 如何判断对象可以被回收

    堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。

    1.1 引用计数法

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

    优点:实现简单,效率高。

    缺点:很难解决对象之间的相互循环引用。A引用B,B引用A ——循环引用 (引用计数算法)由于A、B彼此引用对方,导致引用计数都不为0,所以GC无法回收它们

     1 public class MyObject {
     2     public Object ref = null;
     3     public static void main(String[] args) {
     4         MyObject myObject1 = new MyObject();
     5         MyObject myObject2 = new MyObject();
     6         myObject1 = myObject2;
     7         myObject2 = myObject1;
     8         myObject1 = null;
     9         myObject2 = null;
    10     }
    11 }
    

    以myObject1对象为例:
    1、代码执行到line7,myObject1、myObject2的引用计数均为2。

    2、此时将myObject1和myObject2分别置为null,以前一个对象为例,它的引用计数将减1。

    3、当myObject1=0,垃圾回收器才能进行垃圾回收,由于 myObject2 = myObject1;因为myObject2持有myObject1的引用,而要清除掉这个引用的前提条件是myObject2引用的对象被回收。而myObject2的引用又被myObject1持有,也就进入一种死循环的状态。

    4、最终myObject1 和myObject2引用计数均为1,导致不能进行垃圾回收。

    1.2 可达性分析算法

    这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
    GC Roots根节点:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等等


    image.png
    1.3 finalize()方法最终判定对象是否存活

    即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
    标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

    1. 第一次标记并进行一次筛选。
      筛选的条件是此对象是否有必要执行finalize()方法。
      当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。
    2. 第二次标记
      如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
      finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
      见示例程序:
    @Data
    public class User {
        private int id;
        private String name;
        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }
    
        @Override
        protected void finalize() throws Throwable {
            System.out.println("关闭资源! user"+ id + "即将被回收");
        }
    }
    
    public class FinalizeTest {
        public static void main(String[] args) {
            List<User> list = new ArrayList<User>();
    
            int i = 0;
            int j = 0;
            while (true) {
                list.add(new User(i++, UUID.randomUUID().toString()));
                new User(j--, UUID.randomUUID().toString());
            }
        }
    }
    

    输出

    ...
    关闭资源! user-31945即将被回收
    关闭资源! user-31946即将被回收
    关闭资源! user-31947即将被回收
    关闭资源! user-31948即将被回收
    关闭资源! user-31949即将被回收
    关闭资源! user-31951即将被回收
    关闭资源! user-31952即将被回收
    关闭资源! user-31953即将被回收
    ...
    

    一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize() 方法,并且在下一次垃圾回收动作发生时,才真正回收对象占用的内存。

    注意:finalize只有垃圾回收GC的时候才会去调用,但程序不一定会GC。
    finalize未执行
    public class FinalizeTest {
        public static void main(String[] args) {
            FinalizeTest ft = new FinalizeTest();
        }
        @Override
        protected void finalize() throws Throwable {
            System.out.println("it is finalized!");
        }
    }
    

    finalize只有垃圾回收的时候才会去调用。操作系统结束一个进程,会把那个进程申请的内存都清了,所以用不到gc

    finalize执行
    public class FinalizeTest {
        public static void main(String[] args) throws Exception {
            FinalizeTest ft = new FinalizeTest();
            //表示ttf已经不可能有任何线程会使用它了
            ft = null;
            //运行垃圾回收器
            System.gc();
    
        }
    
        @Override
        protected void finalize() throws Throwable {
            System.out.println("it is finalized!");
        }
    }
    
    jvm在什么情况下会执行GC?
    • 对象没有引用
    • 作用域发生未捕获异常
    • 程序在作用域正常执行完毕
    • 程序执行了System.exit()
    • 程序发生意外终止(被杀进程等)

    1.4 如何判断一个常量是废弃常量

    运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
    假如在常量池中存在字符串 "abc",如果当前没有任何String对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。

    1.5 如何判断一个类是无用的类

    方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
    判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是 “无用的类” :
    该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    加载该类的 ClassLoader 已经被回收。
    该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

    2.垃圾收集算法

    2.1 标记-清除算法

    算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:
    效率问题
    空间问题(标记清除后会产生大量不连续的碎片)


    image.png
    2.2 复制算法

    为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。


    image.png
    2.3 标记-整理算法

    根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。


    image.png
    2.4 分代收集算法

    当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
    比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

    相关文章

      网友评论

        本文标题:深入JVM内核11 JVM垃圾回收

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