1.如何确定对象存活

1.1 引用计数法

  • 描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
  • 优点:实现简单,判定效率高
  • 缺点:互相引用对方,导致引用计数器都不为0,于是引用计数器无法通知GC收集器回收他们;标记和清楚两个过程的效率不高;标记清楚之后会产生大量不连续的内存碎片。

1.2 可达性分析算法

在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。


在Java语言中,可作为GC Roots的对象包括下面几种:

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

2. 引用

  • JDK 1.2以前关于引用的老定义(现在已经过时):如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

这样的定义太狭隘,更好的定义应该能描述这样的对象:内存空间还足够时,可以保留在内存之中,如果GC之后内存还十分紧张,则抛弃这些对象。即这些“食之无味,弃之可惜”的对象。JDK1.2之后对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次减弱。

2.1 强引用

和JDK1.2中关于引用的定义类似,如Object obj = new Object()这样的语句(没有其他引用修饰符修饰),引用类型数据存储另外一块内存的起始地址。

2.2 软引用

SoftReference类可以实现软引用。软引用用来描述一些有用但非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存才会抛出内存溢出异常。

2.3 弱引用

WeakReference类来实现弱引用。描述非必须对象,但是强度比软引用更弱一些,被弱引用关联的对象只能生成到下一次垃圾收集发生之前。当垃圾收集器工作的时候,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

2.4 虚引用

最弱的引用关系,用PhantomReference类来实现引用。一个对象是否有须引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象呗收集器回收的时候收到一个系统通知。

3.回收前的两次标记,与死亡逃脱

可达性分析算法中不可达的对象,是先处于缓刑阶段,经历真正死亡需要经历两次标记过程。可达性分析发现没有和GC Roots互相连接的引用链,则第一次标记。然后判断是否有必要执行finalize()方法。finalize()没有被覆盖或者没有被虚拟机调用过,则认为没有必要执行。有必要执行回收的对象会被放到一个F-Queue队列之中,然后由虚拟机自动建立的Finalizer线程去触发finalize()。在finalize()过程中如果重新与引用链上的任何一个对象建立关联,就可以在第二次标记前被移出即将回收的集合。

4.方法区和永久代的回收

一般来说堆中的新生代的回收就可以回收70%-95%的空间,方法区和永久代的垃圾回收性价比十分低。方法区一般不进行垃圾回收。永久代主要回收废弃常量和无用的类。没有其他地方引用这个字面量,则就要被回收了,常量池的其他类接口、方法、字段的符号引用也类似。判断一个类是否是无用的类条件会复杂的多,需要同时满足以下条件:

  1. 该类所有的实例都已经被回收。JAVA堆中不存在该类的任何实例
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方呗引用,无法在任何地方通过反射访问该类的方法。

5. 垃圾回收算法

5.1 标记-清楚算法

这是最基础的算法,前面也已经分析过其优缺点。后面的算法都是针对其不足进行改进。

5.2 复制算法

思想是专门在内存中开辟一处内存区域,把存活的对象复制过来,这样可以对之前连续的区域一起回收,避免产生大量不连续的内存四篇。商用JVM都采用这种算法来回收新生代。将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收的时候,Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次只有10%的内存不会被回收。98%的对象可回收是一般的使用场景,如果Survivor不够用了,就靠老年代来进行分配了。

5.3 标记-整理算法

复制算法在对象存活率较高的时候进行较多复制,效率会较低。还需要额外的空间(从老年代)进行分配担保,例如100%的对象都存活的极端情况。因此在老年代不采用这种复制算法。标记整理也很好理解,标记好了,先让标记的存活对象往一端移动,然后对其余空间进行回收。

5.4 总结

可见在实际过程中采用的是分代收集的办法,在新生代使用复制算法,在老年代使用标记整理算法。