深入JVM内核(三)对象存活判定算法与垃圾收集算法

对象存活判定算法

引用计数法(Reference Counting)

思路很简单,给每个对象中添加一个引用计数器,每当一个地方引用它时,计数器值加一;当引用失效时,计数器值减一。任何时候,当计数器值为0就不可能再被使用了。

引用计数法实现简单,判断效率也高。但是主流的Java虚拟机里面没有使用引用计数法来管理内存。主要的原因在于它很难解决对象之间相互循环引用的问题。

可达性分析算法(Reachability Analysis)

算法思路:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始往下开始搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots之间没有任何引用链相连,则证明此对象时不可用的。

可作为GC Roots对象包括以下几种:

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

再谈引用

如果一个对象分为被引用或者没有被引用两种状态有些狭隘。
我们希望描述这样一类对象:当内存空间还足够时,则能保留再内存之中;如果内存空间再进行垃圾收集之后还是非常紧张,则可以抛弃这些对象。
在JDK1.2后,对引用进行了扩充,将引用分为:强引用、软引用、弱引用、虚引用。

  • 强引用:只要强引用还在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用:描述一些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。
  • 弱引用:描述一些非必需的对象。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存周期产生影响,也无法通过虚引用来获取一个对象实例。它的唯一作用是这个对象在被垃圾收集器回收时收到一个系统通知。

“缓刑”与finalize()方法

即使在可达性分析算法中不可达的对象,也并不是“非死不可”,可能会处于“缓刑”阶段。
要宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后没有与GC Roots相关联的引用链,那它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当没有对象覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,这两种情况被视为没有必要执行finalize()方法。

注意:任何一个对象的finalize()方法都只能被系统自动调用一次。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class CanReliveObj {
public static CanReliveObj obj;

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("CanReliveObj finalize called");
obj=this;
}

@Override
public String toString(){
return "I am CanReliveObj";
}

public static void main(String[] args) throws InterruptedException{
obj = new CanReliveObj();
obj = null; //可复活
System.gc();
Thread.sleep(1000);
if(obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
System.out.println("第二次gc");
obj = null; //不可复活
System.gc();
Thread.sleep(1000);
if(obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
}
}

输出:

1
2
3
4
CanReliveObj finalize called
obj 可用
第二次gc
obj 是 null

可触及性

可触及的:
从根节点可以触及到这个对象

可复活的:
一旦所有引用被释放,就是可复活状态
因为在finalize()中可能复活该对象

不可触及的:
在finalize()后,可能会进入不可触及状态
不可触及的对象不可能复活
可以回收

垃圾收集(Garbage Collection)算法

标记-清除算法(Mark-Sweep)

算法分为“标记”和“清除”两个阶段。首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。 标记过程就是判断对象生死的过程。

标记清除算法

缺点:

  • 标记和清除两个过程的效率都不高
  • 空间问题,清除后会产生大量不连续的内存碎片

复制算法(Copying)

将可用的内存容量划分为大小相等的两块,每次只使用其中的一块。
当这块的内存使用完了,就将还存活的对象复制到另一块内存上,然后使用过的内存空间一次性清理。

复制算法

缺点:
空间浪费,每次只能使用一半的内存空间。
如果对象的存活率较高,就会有太多的复制操作,效率会降低。所以老年代不采用这种算法。

改进的复制算法:
将内存空间分为一块较大的Eden空间和两块较小的Survivor空间(幸存区)。
每次使用Eden和其中一个Survivor。回收时,将Eden和Survivor中还存活着的对象一次性的复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

复制算法2

标记整理算法(Mark-Compact)

首先标记所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

标记整理算法

分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用的一种方法。
思想是:根据对象存活周期的不同将内存划分为几块。一般是将Java堆分为新生代和老年代。

在新生代中,每次垃圾回收时都会发现大批对象死去,只有少量存活,所以使用复制算法,只需要复制少量的存活对象就可以完成收集。

在老年代中,对象存活率较高,使用标记清理或者标记整理算法较好。


参考:

  • 周志明. 深入理解JVM虚拟机
  • 炼数成金