1 GC的算法
- 引用计数法
- 标记清除
- 标价压缩
- 复制算法
1.1引用计数法
引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
但是很难解决循环引用的问题(如下图),而且每个对象都维护者一个计数,带来性能上的开销,所以java没有采用这种算法
1.2 标记清除法
标记清除是现代垃圾回收的思想基础。分为俩个阶段标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
注意:白色:空闲空间,浅灰色:使用的空间,深色:垃圾空间(下面的图例同这)
1.3 标记压缩法
同标记清除法一样有俩个阶段:标记,压缩阶段。有别于1.2方法,压缩阶段它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端,在清除边界外的所有空间。这种适用于需要复制对象比较少,存活对象比较多的时候,eg:老年代
比较1.2和1.3俩种方式,1.3的主要优势是可以避免内存碎片。
1.4复制算法
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
相比于标记清除法,他是一个比较高效的方式,但是也比较浪费有限的内存空间,不适用于存活对象较多的场合 如老年代。
相比于标记清除法,他是一个比较高效的方式,但是也比较浪费有限的内存空间,不适用于存活对象较多的场合 如老年代
1.5JVM的GC初识
综上,整合上面的思想,JVM会依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代
据不同代的特点,选取合适的收集算法:少量对象存活,适合复制算法。大量对象存活,适合标记清理或者标记压缩。这也就分代回收的思想
以下是图例是方式。
新生代采用标记压缩的方式,将小数据复制到survivor的空闲空间,大对象转移到老年代。同时survivor将对象也将对象复制到survivor空闲空间和老年代(老年作为担保空间起作用的时候)。然后清除俩块区域,GC完后就是上图右边的图。
打印PrintGCDetails可以看到如下信息,
2 GC的对象
当一个对象不可达时候,对象就最终会被GC,那么问题来了,什么时候对象是可达的,什么时候是不可达的。我们来看看对象被回收前的经历的状态:可达,可复活,不可达
2.1 可达
从根节点可以触及到这个对象,(什么是根节点:栈中引用的对象,方法区中静态成员或者常量引用的对象(全局对象),JNI方法栈中引用对象)
2.2 可复活
一旦所有引用被释放,就是可复活状态,特殊状态:finalize()中可能复活该对象
2.3 不可达
在finalize()后,可能会进入不可触及状态
不可触及的对象不可能复活
可以回收
2.4 从finalize说起: 一个例子看清对象状态变迁
让我们来看个例子,体验对象从可达-》可复活-》可达-》不可达的过程
public class Student {
public static Student obj;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Student finalize called");
obj=this;
}
@Override
public String toString(){
return "I am Student";
}
public static void main(String[] args) throws
InterruptedException{
obj=new Student();
obj=null; //可复活
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj is null");
}else{
System.out.println("obj is avilable");
}
obj=null; //不可复活
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj is null");
}else{
System.out.println("obj is avilable");
}
}
}
public class Student {
public static Student obj;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Student finalize called");
obj=this;
}
@Override
public String toString(){
return "I am Student";
}
public static void main(String[] args) throws
InterruptedException{
obj=new Student();
obj=null; //可复活
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj is null");
}else{
System.out.println("obj is avilable");
}
obj=null; //不可复活
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj is null");
}else{
System.out.println("obj is avilable");
}
}
}
输出:
Student finalize called
obj is avilable
obj is null
当obj第一次引用设置成null 变成不可达,经过finalize()后变为可达对象,第二次设置成null由于finalize只执行一次,直接变为不可达对象。
注:避免使用finalize(),操作不慎可能导致错误。可以使用try-catch-finally来替代它。GC的时间不确定,如果略掉
Thread.sleep(1000)上面的结果是不可预测的。
3 GC种类
- 串行收集器
- 并行收集器
- CMS收集器
3.1Serial串行收集器
最早且最稳定的收集器,但是由于采用串行的方式可能产生较长的停顿。
新生代,老年带使用串行回收
使用方式:-XX:+UseSerialGC,新生代、老年代使用串行回收
使用的GC算法:新生代复制算法,老年代标记-压缩
[GC 0.844: [DefNew: 17472K->2176K(19648K), 0.0188339 secs] 17472K->2375K(63360K), 0.0189186 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
[Full GC 8.259: [Tenured: 43711K->40302K(43712K), 0.2960477 secs] 63350K->40302K(63360K), [Perm : 17836K->17836K(32768K)], 0.2961554 secs] [Times: user=0.28 sys=0.02, real=0.30 secs]
3.2并行收集器
3.2.1 ParNew并行收集器
使用方式:-XX:+UseParNewGC,新生代并行(Serial新生带的并行版本),老年代串行
使用GC算法:新生带使用的复制算法
注:多线程需要多个核心支持,-XX:ParallelGCThreads 限制线程合理的线程数量,否则会产生效果
[ParNew: 8192K->1024K(9216K), 0.0612519 secs] 8192K->2007K(19456K), 0.0612942 secs] [Times: user=0.03 sys=0.00, real=0.06 secs]
//todo
3.2.2 Parallel并行收集器
serial串行收集器在,新老年代的并行化,新生代使用复制算法,老年代使用的标记压缩算法,但是默认情况老年代是串行,类似ParNew。
这是关注吞吐量的一个并行收集器。
使用方式:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
-XX:+UseParallelOldGC 使用Parallel收集器+ 并行老年代
PSYoungGen total 9216K, used 8672K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) eden space 8192K, 93% used [0x00000000ff600000,0x00000000ffd78ff0,0x00000000ffe00000) from space 1024K, 99% used [0x00000000ffe00000,0x00000000ffeff1d8,0x00000000fff00000) to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) ParOldGen total 72192K, used 67580K [0x00000000f3800000, 0x00000000f7e80000, 0x00000000ff600000) object space 72192K, 93% used [0x00000000f3800000,0x00000000f79ff118,0x00000000f7e80000) Metaspace used 4705K, capacity 5068K, committed 5248K, reserved 1056768K class space used 529K, capacity 564K, committed 640K, reserved 1048576K
一些其他配置:
-XX:MaxGCPauseMills
最大停顿时间,单位毫秒,GC尽力保证回收时间不超过设定值
-XX:GCTimeRatio
垃圾收集时间占总时间的比
默认99,即最大允许1%时间做GC
这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优
3.3CMS(Concurrent Mark Sweep )收集器
并发标记清除收集器是和用户线程一起执行,一定程度上降低了应用线程的等待时间,但是在并发阶段会降低吞吐量。
使用方式:-XX:+UseConcMarkSweepGC,这是一个老年代的收集器
CMS的执行过程如下图:
CMS过程经历了四个过程,串行初始标记,并发标记,重新比较,并发清理四个阶段,为什么要四个阶段,
在经历初始比较后,由于在并发阶段可能导致对象的重新复活,所以需要使用重新标记。
[GC (Allocation Failure) [ParNew: 0K->0K(9216K), 0.0003332 secs][CMS: 193148K->80508K(194560K), 0.0065277 secs] 193148K->80508K(203776K), [Metaspace: 4697K->4697K(1056768K)], 0.0069152 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 90748K(194560K)] 90748K(203776K), 0.0001216 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (CMS Final Remark) [YG occupancy: 0 K (9216 K)][Rescan (parallel) , 0.0001196 secs][weak refs processing, 0.0000058 secs][class unloading, 0.0009811 secs][scrub symbol table, 0.0003560 secs][scrub string table, 0.0002675 secs][1 CMS-remark: 121468K(194560K)] 121468K(203776K), 0.0019507 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
par new generation total 9216K, used 168K [0x00000000f3800000, 0x00000000f4200000, 0x00000000f4200000) eden space 8192K, 2% used [0x00000000f3800000, 0x00000000f382a0a0, 0x00000000f4000000) from space 1024K, 0% used [0x00000000f4000000, 0x00000000f4000000, 0x00000000f4100000) to space 1024K, 0% used [0x00000000f4100000, 0x00000000f4100000, 0x00000000f4200000) concurrent mark-sweep generation total 194560K, used 80508K [0x00000000f4200000, 0x0000000100000000, 0x0000000100000000) Metaspace used 4704K, capacity 5068K, committed 5248K, reserved 1056768K class space used 529K, capacity 564K, committed 640K, reserved 1048576K
好了经历上面的介绍,总结下CMS的特点:
1 将标记时间进行了分阶段,尽可能的降低了停顿了,
2 由于采用并发方式频繁的线程切换,会影响系统整体的吞吐量和性能,同时清理也是不彻底的操作,会产生比较大的内存碎片
3 无法在空间快耗光时及时清理。虽然可以通过-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值,但内存预留空间不够,就会引起concurrent mode failure。
[GC (CMS Initial Mark) [1 CMS-initial-mark: 147067K(194560K)] 147067K(203776K), 0.0001267 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (CMS Final Remark) [YG occupancy: 0 K (9216 K)][Rescan (parallel) , 0.0001196 secs][weak refs processing, 0.0000061 secs][class unloading, 0.0010007 secs][scrub symbol table, 0.0003919 secs][scrub string table, 0.0001430 secs][1 CMS-remark: 188027K(194560K)] 188027K(203776K), 0.0018660 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-sweep-start]
[GC (Allocation Failure) [ParNew: 0K->0K(9216K), 0.0003316 secs][CMS[CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
(concurrent mode failure): 188027K->126587K(194560K), 0.0088382 secs] 188027K->126587K(203776K), [Metaspace: 4699K->4699K(1056768K)], 0.0092257 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]