对象内存回收
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
GC算法
- 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。
public class Test {
Object instance = null;
public static void main(String[] args) {
Test objA = new Test();
Test objB = new Test();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
- 可达性分析算法:将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
垃圾收集算法
-
分代收集理论
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。 -
标记-复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
-
标记-清除算法
算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。它是最基础的收集算法,比较简单,但是会带来两个明显的问题:- 效率问题 (如果需要标记的对象太多,效率不高)
- 空间问题(标记清除后会产生大量不连续的碎片)
-
标记-整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
垃圾收集器
如果说垃圾收集算法是理论,那么垃圾收集器就是对这个理论的实现,让我们一起来看看有哪些垃圾收集器吧!
Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)
Serial顾名思义是串行的垃圾收集器,是历史最悠久的垃圾收集器了,它是单线程的,意味着在进行垃圾回收的时候只会有一个线程去处理垃圾收集的工作,他在进行垃圾收集的时候必须暂停其他所有的线程,也就是传说中的STW,STW会持续到垃圾收集完成。
Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))
Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾回收之外,其他的和Serial垃圾收集器类似,默认的线程数是cpu的核数,也可以使用参数(-XX:ParallelGCThreads)设置线程数,但是一般不建议修改
Parallel垃圾收集器新生代采用复制算法,老年代采用标记-整理算法。
ParNew收集器(-XX:+UseParNewGC)
ParNew收集器和Parallel收集器类似,区别就是ParNew可以配合CMS垃圾收集器使用
ParNew收集器新生代采用复制算法,老年代采用标记-整理算法。
CMS收集器(-XX:+UseConcMarkSweepGC(old))
CMS(Concurrent Mark Sweep)收集器是一个以获取最短回收停顿时间为目标的垃圾收集器,他非常符合注重在用户体验上的程序上使用,他也是真正意义上的第一款并发垃圾收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
从它的名字可以看出来Mark Sweep,它是用标记-清除算法实现的,它的运行过程相对前面几款来说是比较复杂一些,它分为以下几步骤:
- 初始标记:会STW,并记录下gc roots直接能引用的对象,速度很快。
- 并发标记:不会STW,并发标记会从GC Roots的直接关联对象开始遍历整个对象, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。但是因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
- 重新标记:会STW,重新标记就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。
- 并发清理:不会SWT,GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
- 并发重置:重置本次GC过程中的标记数据。
CMS垃圾收集器的优点:
- STW时间短而且可以控制,
- 并发收集
CMS垃圾收集器的缺点:
- 对CPU资源敏感(会和服务抢资源);
- 无法处理浮动垃圾(在并发标记和并发清理时产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生(可以通过参数设置在执行完标记清除后再做整理)
- 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入STW,用serial old垃圾收集器来回收
CMS的相关核心参数:
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
- -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
- -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
- -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段
- -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
由于中秋节提前下班赶火车了,所以垃圾收集器先写到这里,等明天到家再补上,哈哈哈哈哈,在这里提前祝大家中秋节快乐!!