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

1. 引用计数(Reference Counting)

为每一个对象添加一个计数器,计数器记录了对该对象的活跃引用的数量。如果计数器为0,则说明这个对象没有被任何变量所引用,即应该进行垃圾收集。
收集过程如下:
1. 减少被收集对象所引用的对象的计数器的值
2.将其放入延时收集队列之中

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

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

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:PSYoungGen)

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。
    将内存分为两个区域(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中)。



Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐