Java -verbose:gc 中参数-verbose:gc 表示输出虚拟机中GC的详细情况.
运行结果解读如下:箭头前后的数据1248K和0K分别表示垃圾收集GC前后所有存活对象使用的内存容量,说明有1248K-0K=1248K的对象容量被回收,括号内的数据8960K为堆内存的总容量,收集所需要的时间是0.0064104秒(这个时间在每次执行的时候会有所不同)
Note:GC会暂用CPU时间片,有可能造成应用程序在某个时刻极短的停顿.
由于垃圾收集算法在各个虚拟机以及不同的平台上会有不同的实现,所以开头先大概讲解一下几个基本的算法1. 引用计数(Reference Counting)为每一个对象添加一个计数器,计数器记录了对该对象的活跃引用的数量。如果计数器为0,则说明这个对象没有被任何变量所引用,即应该进行垃圾收集。收集过程如下:1. 减少被收集对象所引用的对象的计数器的值2.将其放入延时收集队列之中
由于垃圾收集算法在各个虚拟机以及不同的平台上会有不同的实现,所以开头先大概讲解一下几个基本的算法
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)
现在的商业虚拟机都用这种算法来回收新生代,因为新生代的大多数的生命周期都很短暂,所以前面提到的两块相互切换的区域并不需要按照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中)。
更多推荐
所有评论(0)