JVM系列之垃圾收集简介

垃圾收集关注三个问题:哪些对象需要回收?什么时候回收?如何回收?


判断对象是否可被回收:

1.引用计数法

给每个对象添加一个引用计数器,当有新的引用就加1,当引用失效时则减1,当为0时,说明对象不会再被引用。

会出现bug的情况:当两个对象互相引用时,即使对象已经为null,仍然无法被回收


2.可达性分析算法

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

JVM系列之垃圾收集简介

JVM系列之垃圾收集简介

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

虚拟机栈(栈帧中的本地变量表)中引用的对象。当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。

方法区中类静态属性引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI(即一般说的Native方法)引用的对象。


总而言之,GC Roots的对象是必须活跃的引用。


引用的区分:

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

强引用就是指在程序代码之中普遍存在的,类似“Object obj = newObject()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用:如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

弱引用:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

虚引用:在任何时候都可能被垃圾回收。

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解 被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 


finalize

     在根搜索算法中,那些不可达的对象,比如上图中的Object5,6,7,并非是“真正死亡”的,主要看如下两次标记过程:

      a、当没有发现引用链时,进行第一次标记,此时进行第一次筛选,条件为此对象是否有必要执行finalize方法。当对象没有覆盖finalize方法,或者finalize方法已经被JVM调用过,此种情况下认为没有必要执行finalize方法。可知finalize方法只可被系统调用一次。

      b、如果有必要执行finalize方法,此时对象会被放置在一个F-Queue队列中,会有一个优先级比较低的FInalizer线程去执行触发finalize方法。finalize方法是对象真正判定死活的最后一次机会。此时,GC会对队列中的对象进行第二次标记,如果对象在finalize方法中完成了自救,即和GC Roots建立了通路,则在第二次标记时该对象将被移出回收的集合。否则,只能判定对象死了。


方法区中的回收

回收废弃常量与回收Java堆中的对象非常类似。不再被引用到的常量就会被清理。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

类需要同时满足下面3个条件才能算是“无用的类”:

1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

2.加载该类的ClassLoader已经被回收。

3.该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。



如何回收:

1.标记-清除算法

标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。

清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。

JVM系列之垃圾收集简介

缺点:效率不高;会造成内存空间的不连续(逻辑上的不连续?),对下次对象分配造成影响


2.复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

JVM系列之垃圾收集简介

最初的复制算法会把一次可用内存减小为原来的1/2,会牺牲大量空间。

hotspot虚拟机,将新生代(注意只是新生代)分为一个eden和两个survivor区,大小比例为8:1。只有10%的空间会被“浪费”。极端情况下,可以向“老年代”申请空间。


3. 标记-整理算法

根据老年代(对象存活率较高)的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记-整理”算法的示意图如图所示。

JVM系列之垃圾收集简介