垃圾回收策略与算法

垃圾收集策略与算法

  • 垃圾回收只针对线程共享内存: 堆、方法区

  • 判断对象是否存活——不被任何对象或变量引用的是无效对象,需要被回收

    • 引用计数法——存在循环引用的问题
       对象头中维护一个计数器记录对象被引用的次数,当计数为零时对象无效
    • 可达性分析法(HotSpot)
      • 所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。
      • GC Roots: 不包括队中对象引用的对象,因此不存在循环引用问题
        • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
        • 本地方法栈中引用的对象
        • 方法区中常量引用的对象
        • 方法区中类静态属性引用的对象
  • 对象引用的种类——对应不同的可达性状态

    • 强引用
       类似 "Object obj = new Object()" 这类的引用,就是强引用,只要强引用存在,垃圾收集器永远不会回收被引用的对象。但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存泄漏

    • 软引用
       软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存

    • 弱引用
       弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

    • 虚引用
       虚引用也称幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。它仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制。

  • 回收堆中的无效对象——回收GC Roots不可达对象

    • 判定finalize()是否有必要执行
       JVM 会判断此对象是否有必要执行 finalize() 方法,如果对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么视为“没有必要执行”, 对象基本上就真的被回收了。
       如果对象被判定为有必要执行 finalize() 方法,那么对象会被放入一个 F-Queue 队列中,虚拟机会以较低的优先级执行这些 finalize()方法,但不会确保所有的 finalize() 方法都会执行结束。如果 finalize() 方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。

    • 对象重生或死亡
       如果在执行 finalize() 方法时,将 this 赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。任何一个对象的 finalize() 方法只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法不会被再次执行,想继续在 finalize() 中自救就失效了。

  • 回收方法区内存

    • 方法区主要清理两种垃圾:

      • 废弃常量
      • 无用的类
    • 判定废弃常量——常量不被任何变量或对象引用,就会被清除掉

    • 判定无用的类:

      • 该类所有对象都已被清除
      • 加载该类的ClassLoader已经被回收
      • 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

      一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除。

  • 垃圾收集算法

    • 标记-清楚算法

      • 标记: 遍历GC Roots将所有可达对象标记为存活
      • 清楚: 遍历堆中所有对象,清楚所有未被标记的无效对象,并且取消所有存活对象的标记方便下次清理
      • 缺点:
        • 效率不高
        • 会产生许多不连续内存碎片,导致后面的过程中GC更加频繁
    • 复制算法(新生代)

      • 划分两块相等的区域
         将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,就将存活者的对象复制到另一块上面,然后将第一块内存全部清除。这种算法有优有劣:

        • 不存在内存碎片问题
        • 内存缩小为原来的一半,浪费空间
      • 划分为三块区域( Eden、From Survivor、To Survivor -> 8:1:1)

        • 将内存分为三块: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor。回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。
        • 缺点
           无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够,需要依赖其他内存(指老年代)进行分配担保。
      • 分配担保(超过10%的存活对象放入老年代)

        • 为对象分配内存空间时,如果 Eden+Survivor 中空闲区域无法装下该对象,会触发 MinorGC 进行垃圾收集。但如果 Minor GC 过后依然有超过 10% 的对象存活,这样存活的对象直接通过分配担保机制进入老年代,然后再将新对象存入 Eden 区。
    • 标记-整理算法(老年代)

      • 标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历 GC Roots,然后将存活的对象标记。
      • 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。
    • 分代收集算法(综合上述几种方式)
       根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法:

      • 新生代: 复制算法
      • 老年代: 标记-整理算法
posted @ 2020-08-25 20:09  CodeSPA  阅读(154)  评论(0编辑  收藏  举报