垃圾回收机制,我们已经知道什么样的对象会成为垃圾。对象回收经历了什么——垃圾回收算法。那么谁来负责回收垃圾呢?
-XX:+UseSerialGC -XX:+UseSerialOldGC
单线程执⾏垃圾收集,收集过程中会有较⻓的STW(stop the world),在GC时⼯作线程不能⼯作。虽然STW较⻓,但简单、直接。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
由于Serial垃圾回收器是单线程的,因此它的优点是简单且占用资源较少;它适用于小型应用程序,例如移动应用程序和桌面应用程序。
-XX:+UseParallelGC,-XX:+UseParallelOldGC
使⽤多线程并行进⾏GC,会充分利⽤cpu,但是依然会有stw,这是jdk8默认使⽤的新⽣代和⽼年代的垃圾收集器。充分利⽤CPU资源,吞吐量⾼。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
Parallel垃圾回收器适用于中等大小的应用程序,特别是那些需要高吞吐量的应用程序,例如: Web应用程序和大规模企业应用程
序。
-XX:+UseParNewGC
⼯作原理和Parallel收集器⼀样,都是使⽤多线程进⾏GC,但是区别在于ParNew收集器可以和CMS收集器配合⼯作。主流的方案:
ParNew收集器负责收集新生代,CMS负责收集老年代。
-XX:+UseConcMarkSweepGC
⽬标:尽量减少stw的时间,提升⽤户的体验。真正做到gc线程和⽤户线程⼏乎同时⼯作。CMS采⽤标记-清除算法。
此处标记的是有用的对象。
初始标记:暂停所有的其他线程(STW),并记录gc roots直接能引⽤的对象。
例:线程栈帧的局部变量表中有个引用指向堆空间对象A,堆空间变量又引用了另一个对象B,则只记录A,不算B。
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较⻓但是不需要STW,可以与应用线程⼀起并发运⾏。这个过程中,⽤户线程和GC线程并发,可能会有导致已经标记过的对象状态发⽣改变,所以下一步需要重新标记
最后一句话是说会造成标记的遗漏:
重新标记:为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远比并发标记阶段时间短。主要⽤到三⾊标记⾥的算法做重新标记。
并发清理:开启⽤户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。(最终被标记为白色的都是垃圾)
并发重置:重置本次GC过程中的标记数据。
总结:
用于大对象的回收,且jdk8版本对G1不是很完整
再上述cms收集器中,采用到的算法就是三色标记法。
三色标记法会将内存中的对象分为白、灰、黑三种颜色,具体标记过程如下:
根据可达性分析算法,从 GC Roots 开始进行遍历访问。初始状态,所有的对象都是白色的,只有 GC Roots 是黑色的。
初始标记阶段,GC Roots 标记直接关联对象置为灰色。
并发标记阶段,扫描整个引用链。
扫描完成,此时黑色对象就是存活的对象,白色对象就是已消亡可回收的对象。
即(A、D、E、F、G)可达也就是存活对象,(B、C、H)不可达可回收的对象。
三色标记算法,由于在并发标记阶段的时候,因为用户线程与 GC 线程同时运行,有可能会产生多标或者漏标。
假设已经遍历到 E(变为灰色了),此时应用执行了 objD.fieldE = null
(D → E 的引用断开)。
D → E 的引用断开之后,E、F、G 三个对象不可达,应该要被回收的。然而因为 E 已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮 GC 不会回收这部分内存。
这部分本应该回收但是没有回收到的内存,被称之为浮动垃圾
。浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。
假设 GC 线程已经遍历到 E(变为灰色了),此时应用线程先执行了:
var G = objE.fieldG; objE.fieldG = null; // 灰色E 断开引用 白色G objD.fieldG = G; // 黑色D 引用 白色G
此时切回到 GC 线程,因为 E 已经没有对 G 的引用了,所以不会将 G 置为灰色;尽管因为 D 重新引用了 G,但因为 D 已经是黑色了,不会再重新做遍历处理。
最终导致的结果是:G 会一直是白色,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。
漏标只有同时满足以下两个条件时才会发生:
也有方法可以解决:
需要在上面三个步骤中任意一个中,将对象 G 记录起来,然后作为灰色对象再进行遍历即可。比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再重新标记阶段对该集合的对象遍历即可(重新标记)。
var G = objE.fieldG; // 1.读objE.fieldG = null; // 2.写objD.fieldG = G; // 3.写