《深入理解Java虚拟机》——垃圾收集器与内存分配策略

时间:2021-02-18 23:51:14

GC需要完成:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

如何确定对象不再使用

  • 引用计数算法
    给对象添加一个引用计数器,当有一个地方引用它时,计数器值进行加1操作;当引用失效时,计数器值进行减1操作;当计数器值为0,则说明对象不可能再被使用。但是它无法解决循环引用的问题。

    public class ReferenceCountingGC {
      
        public Object instance = null;
    
        public static void testGC(){
    
            ReferenceCountingGC objA = new ReferenceCountingGC ();
            ReferenceCountingGC objB = new ReferenceCountingGC ();
    
            // 对象之间相互循环引用,对象objA和objB之间的引用计数永远不可能为 0
            objB.instance = objA;
            objA.instance = objB;
    
            objA = null;
            objB = null;
    
            System.gc();
    }
    }

    上述代码最后面两句将objA和objB赋值为null,也就是说objA和objB指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为 0,那么垃圾收集器就永远不会回收它们。

  • 可达性分析算法
    通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如下图,对象Object5、Object6、Object7虽然互相有所关联,但是它们到GC Roots是不可达的,因此将它们判定为可回收的对象。
    《深入理解Java虚拟机》——垃圾收集器与内存分配策略
    在Java语言中,可作为GC Roots的对象包括:
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法去中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

关于引用

无论是通过引用计数算法还是可达性分析算法,判断对象是否存活都与“引用”有关。
宣告一个对象真正失效,至少要经历两次标记过程,如果对象在进行可达性分析后发现没有与 GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize() 方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
关于Java中的引用:

  • 强引用:程序代码中普遍存在,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用:用来描述一些还有用但并非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
  • 弱引用:用来描述非必需对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用挂链的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
  • 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。相较于废弃常量,判定一个类是否是“无用的类”的条件则相对苛刻很多,类需要同时满足下面三个条件:(不满足一定不回收,满足不一定回收,区别于对象无效就回收)
• 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
• 加载该类的ClassLoader已经被回收
• 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法


垃圾收集算法

  • 标记-清除(Mark-Sweep)算法
    首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象。标记过程就是使用引用计数法或可达性分析进行标记。
    不足:
    • 效率问题:标记和清除两个过程的效率都不高
    • 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。
      《深入理解Java虚拟机》——垃圾收集器与内存分配策略
  • 复制算法
    将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。
    《深入理解Java虚拟机》——垃圾收集器与内存分配策略

  • 标记-整理算法
    标记过程仍然与“标记-清除”算法一样,但后续步骤不再直接对可回收对象进行清理,二十让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
    《深入理解Java虚拟机》——垃圾收集器与内存分配策略

  • 分代收集算法
    根据对象存活周期的不同将内存划分为几块,一般把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那么就选用复制算法,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,必须使用“标记-清理”或者“标记-整理”算法来进行回收。