您的位置:首页 > 其它

JVM学习之:GC的算法简述

2015-03-19 23:25 211 查看
由于垃圾收集算法在各个虚拟机以及不同的平台上会有不同的实现,所以开头先大概讲解一下几个基本的算法

1. 引用计数(Reference Counting)

为每一个对象添加一个计数器,计数器记录了对该对象的活跃引用的数量。如果计数器为0,则说明这个对象没有被任何变量所引用,即应该进行垃圾收集。

收集过程如下:

1. 减少被收集对象所引用的对象的计数器的值

2.将其放入延时收集队列之中

引用计数的方法需要编译器的配合。编译器需要为此对象生成额外的代码。如赋值函数将此对象赋值给一个引用时,需要增加此对象的引用计数。还有就是,当一个引用变量的生命周期结束时,需要更新此对象的引用计数器。

引用计数的方法由于存在显著的缺点,实际上并未被JVM所使用。下面的程序可以用来证明这点:

[java] view
plaincopyprint?

public class ReferenceCountingGC {

/**

* -verbose:gc -XX:+PrintGCDetails -Xms30M -Xmx30M -Xmn10M

*/

public static final int _1M = 1024 * 1024;

private ReferenceCountingGC ref = null;

private byte[] content = new byte [_1M * 1];

public void testGC() {

ReferenceCountingGC a = new ReferenceCountingGC();

ReferenceCountingGC b = new ReferenceCountingGC();

a.ref = a;

b.ref = b;

a = null;

b = null;

System.gc();

}

public static void main(String[] args) {

new ReferenceCountingGC().testGC();

}

}

下面为运行的结果:

[GC [PSYoungGen: 3411K->1248K(8960K)] 3411K->1296K(29440K), 0.0013584 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

[Full GC (System) [PSYoungGen: 1248K->0K(8960K)] [PSOldGen: 48K->1240K(20480K)] 1296K->1240K(29440K) [PSPermGen: 2993K->2993K(21248K)], 0.0064104 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

Heap

从程序中可以看出,即使a,b两个对象通过相互引用的方式来保持对象计数器的值不为零,但是任然会被垃圾回收器进行回收(

Java -verbose:gc 中参数-verbose:gc 表示输出虚拟机中GC的详细情况.
运行结果解读如下:

  箭头前后的数据1248K和0K分别表示垃圾收集GC前后所有存活对象使用的内存容量,说明有1248K-0K=1248K的对象容量被回收,括号内的数据8960K为堆内存的总容量,收集所需要的时间是0.0064104秒(这个时间在每次执行的时候会有所不同)

Note:GC会暂用CPU时间片,有可能造成应用程序在某个时刻极短的停顿.

)

2.标记-清除收集器(Mark-Swap Collectors)

收集过程分为2个阶段

1. 首先停止所有工作,从根集遍历所有被引用的节点,然后进行标记,最后恢复所有工作

2. 收集阶段会收集那些没有被标记的节点,然后返回空闲链表

标记-清除法的缺点在于

1.标记阶段暂停的时间可能很长,而整个堆在交换阶段又是可访问的,可能会导致被换页换出内存。

2.另外一个问题在于,不管你这个对象是不是可达的,即是不是垃圾,都要在清楚阶段被检查一遍,非常耗时.

3,标记清楚这两个动作会产生大量的内存碎片,于是当有大对象进行分配时,不需要触发一次垃圾回收动作

3.拷贝收集器(Copying Collectors)(适用于young generation:[b]PSYoungGen)

[/b]
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。

将内存分为两个区域(from space和to space)。所有的对象分配内存都分配到from space。在清理非活动对象阶段,把所有标志为活动的对象,copy到to space,之后清楚from space空间。然后互换from sapce和to space的身份。既原先的from space变成to sapce,原先的to space变成from space。每次清理,重复上述过程。

现在的商业虚拟机都用这种算法来回收新生代,因为新生代的大多数的生命周期都很短暂,所以前面提到的两块相互切换的区域并不需要按照1:1来进行分配。而是分配了一个Eden区,两个Survivor区。大部分对象默认的都是在 Eden区中生成。当垃圾回收时,Eden和其中的一个Survivor区的存活对象将被复制到另外一个Survivor区,当另外一个Survivor区也满了的时候,从Eden和第一个Survivor区复制过来的并且此时还存活的对象,将被复制到tenured generation。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。

young generation的gc称为minor gc。经过数次minor gc,依旧存活的对象,将被移出young generation,移到tenured generation

优点:copy算法不理会非活动对象,copy数量仅仅取决为活动对象的数量。并且在copy的同时,整理了heap空间,即,to space的空间使用始终是连续的,内存使用效率得到提高。

缺点:默认情况下Eden:Survivor=8:1, 所以总会有100-(80+10)%的新生代内存会被浪费掉。

4.标记-整理收集器(Mark-Compact Collectors)(适用于存放生命周期较长对象的tenured generation:PSOldGen)

标记整理收集器,通过融合了标记-清除收集器和拷贝收集器的优点,很好的解决了拷贝收集策略中,堆内存浪费严重的问题。

标记整理收集器分为2个阶段

1. 标记阶段, 这个阶段和标记-清除收集器的标记阶段相同

2. 整理阶段, 这个阶段将所有做了标记的活动对象整理到堆的底部(有点像是磁盘碎片整理,呵呵)

生命周期较长的对象,归入到tenured generation。一般是经过多次minor gc,还 依旧存活的对象,将移入到tenured generation。(当然,在minor gc中如果存活的对象的超过survivor的容量,放不下的对象会直接移入到tenured generation)tenured generation的gc称为major gc,就是通常说的full gc。由于tenured generaion区域比较大,而且通常对象生命周期都比较长,所以这部分的gc时间比较长。

minor gc可能引发full gc。当eden+from space的空间大于tenured generation区的剩余空间时,会引发full gc。这是悲观算法,要确保eden+from space的对象如果都存活,必须有足够的tenured generation空间存放这些对象。

最后补充一点,如果采用的是分代算法的话还会有一个permanent generation(PSPermGen)区域,该区域比较稳定,主要用于存放classloader信息,比如类信息和method信息。

对于spring hibernate这些需要动态类型支持的框架,这个区域需要足够的空间。(这部分空间应该存在于方法区而不是heap中)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: