JVM: 垃圾回收机制

垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。Java堆方法区的内存分配和回收是动态的,因而垃圾收集器所关注的也是这部分内存。

垃圾收集机制要考虑的两件事情:
(1)发现无用对象(如何判断对象是否还“存活”);
(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用(垃圾收集算法)。

对象“存活”?

垃圾回收的第一件事情就是要确定堆内存中有哪些对象还“存活”,哪些已“死去”。

引用计数法

引用计数(Reference Counting)是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。

当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1;但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

  • 优点
      引用计数算法实现简单,判定效率高。

  • 缺点
      Java虚拟机中没有选用该算法,因为无法检测出循环引用。(如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0)

可达性分析算法

可达性分析(Reachablility Analysis)算法的基本思想是:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(不可达),则证明此对象是不可用的。

Java中,可以作为GC Roots的对象包括:

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

枚举根节点会导致(Stop The World),也就是GC进行时必须停顿所有执行线程。

引用

JDK 1.2中对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,而不仅仅是只有被引用或没有被引用两种状态。

参见:https://rogerfang.github.io/2016/12/31/Java-%E5%87%A0%E7%A7%8D%E5%BC%95%E7%94%A8/

回收方法区

在方法区中进行垃圾收集的性价比一般比较低,垃圾收集的主要内容包括两部分:废弃的常量无用的类

  1. “废弃常量”的判定:没有其他地方引用这个字面量。
  2. “无用的类”的判定,条件较为苛刻,必须同时满足以下3个条件:
    • 该类所有的实例都被回收;
    • 加载该类的ClassLoader已经被回收;
    • 该类对应的java.lang.Class对象没有在其他地方被引用,无法在其他地方通过反射来访问该类。

      反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能。

垃圾收集算法

Mark-Sweep(标记清除)

此算法执行分为标记和清除两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。

不足:
(1)效率问题,标记和清除两个过程都不高;
(2)空间问题,标记清除后悔产生大量不连续的内存碎片,当无法找到足够的连续内存分配对象时会提前出发一次垃圾收集动作。

Copying(复制)

复制算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把存活的对象复制到另外一个区域中,然后再把已使用过的内存空间一次清理掉。

实现简单,运行高效,代价是需要两倍内存空间。

HotSpot虚拟机按照8:1的比例将新生代分为1个大内存的Eden区和2个小内存的Survivor区。每次使用Eden和一个Survivor区,当回收时,把存活的对象复制到剩下的一个Survivor区中。如果这个Survivor空间不够时,依赖老年代进行分配担保(Handle Promotion)。

Mark-Compact(标记整理)

标记整理算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

Generational Collection(分代收集)

一般把Java堆分为新生代老年代。根据各个年代的特点选择合适的收集算法,新生代每次垃圾收集时只有少量存活可以选用复制算法,而老年代对象存活率高、没有额外空间进行分配担保,必须使用“标记-清除”或“标记-整理”算法。

Region(分区算法)

分代算法是按照对象的生命周期长短划分成两个部分。

分区算法是将整个堆空间划分成连续的不同小区间,每个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。。