目录
并发标记问题
三色算法问题
浮动垃圾问题
漏标问题
cms的解决方式
g1的解决方式
跨代(区)引用
CMS垃圾回收日志
G1垃圾回收日志
垃圾回收过程其实都包含两步:标记+回收。
标记算法:
回收算法:
综合来看,复制算法适合在垃圾回收执行时存活对象比较少的场景;整理算法适合在垃圾回收时存活对象较多但是不能有内存碎片的场景;而清理算法因为会产品内存碎片,实际的场景其实并不多,CMS使用该算法,但也诟病比较多。
对于一个java进程中的对象,经过统计发现,大部分对象生命期都是比较短的,活不过一次垃圾回收,而少部分生命期比较长,多次gc后依然存在,甚至和java进程生命期相同。针对这种情况,就对java的内存进行了分区,不同分区使用不同的回收算法,来达到最大的收集效果。
所以在垃圾回收器的发展历史中,出现了两个分区方式(根据分区的粒度不同)
这是从内存布局上和回收算法上看gc发展的历史,主要就是分这么两个阶段。
从并行的角度看垃圾回收的发展。


所以到了现在的垃圾回收器,就是多种回收算法的组合、多gc线程、以及gc过程中某些阶段gc线程和业务线程可并行运行的有机结合,来提高gc的性能的。比如CMS、G1,以及后来的ZGC。
jdk支持的垃圾回收器(连线表示可组合使用):

当业务线程和gc线程并行运行的时候,就让标记变得非常复杂了,比如gc线程标记到某个对象不是垃圾、但是业务线程下一秒就断开了到该对象的引用、那么这个对象本次gc就不会回收;另外gc线程没有标记认为是垃圾、但是在回收前业务线程又有引用指向了这个对象,这种情况就会更严重,将不是垃圾的对象给回收了,程序就会出现问题。
为了解决这种并发标记的问题,发明了三色着色算法来解决这个问题,其核心思想就是在标记的过程中,给对象的标记情况进行着色区分:
ps:当一个对象被标记成灰对象,所谓的后面标记,是指因为操作系统调度,gc线程被暂停了,重新获得cpu时间片后,接着继续标记。这里千万不要和CMS、G1收集器的remark阶段混淆了,这些收集器设计remark阶段就是为了解决着色算法的问题的。所以这里讲的着色算法时的垃圾回收就两个阶段:标记+回收,标记不分阶段哈,不要和remark混为一谈,否则比较不好理解。
当gc线程执行标记时,到了如下情况时,gc线程的cpu时间片耗尽,业务线程开始执行。

业务线程将B指向C的引用给解除了:

当gc线程再次获得cpu时间片的时候,再去标记B的字段的时候,已经通过B字段找不到C了,即C已经成为垃圾,但本轮gc就有可能回收不到C。不过下轮gc的时候,C就会被回收了,这种因为业务线程和gc线程并行执行过程中产生的、而本轮gc又不能回收的垃圾称之为浮动垃圾。
浮动垃圾的影响只是会多占用一段时间的内存,并不会导致错误。但是对于不是垃圾而又没有被标记到的,被清理了就会出现问题
为了解决浮动垃圾占用内存的问题,gc的触发时机就不会等到分区(分代)内存全部用光了,才会触发gc,而是对应分区(分代)内存占用达到一定比例的时候,就会处罚gc。
gc线程执行到如下情况,cpu时间片消耗尽了:
但是业务线程运行的时候,让A持有了C的引用,但是断开了B对C的引用。这个时候gc线程获得cpu时间片继续运行时,因为A已经是黑色对象,所以gc线程不会再去遍历其字段引用了哪些对象了、而B是灰色对象,但是通过B的字段遍历,已经标记不到C了,那么就会误认为C是一个垃圾,如果本轮gc将C给回收了,那么A对象去访问C的时候,就会出问题。

综上:三色标记法漏标问题的产生有两个充要条件:
1. 业务线程使得一个黑色对象引用指向了白色对象
2. 原本指向这个白色对象引用的灰色对象,被业务线程删除了
cms解决三色标记漏标的问题,就是破坏第一个条件:业务线程使得一个黑色对象引用指向了白色对象。具体的做大就是当黑对象引用别的对象发生变化的时候,就将黑对象变成灰对象(写屏障),那么gc线程再次运行的时候,就会继续标记了。
但是对于并行的gc标记来说,还是会有问题,比如当gc线程开始标记A对象,所以A对象是灰对象

当gc线程正在通过A对象的A.b字段去标记B对象的时候,这个时候cpu时间片耗尽,业务线程开始运行,这个时候业务线程将B对象对C的引用断开,但是让A对象A.c引用指向了C对象。在写屏障中会将A变成灰对象(本身已经是灰对象了):

当gc线程再次获得cpu时间片的时候,会接着上次没有标记完的地方开始继续标记。当标记完成后,C其实是没有被标记到的,认为是垃圾,就会被回收,但实际上A却是有引用指向了C的,所以C是不能回收的。

在remark阶段,暂停业务线程,来重新扫描所有灰对象(其实是从gc root开始)的所有字段。这样就会将C给标记上了,从而避免问题。所以我们看CMS垃圾回收stw的时候,有的时候remark的时间是比较长的。
当然CMS还有其他的问题,比如CMS是不具内存整理能力的,当老年代内存使用率达到指定的比例的时候,就开始一次老年代gc,而这次gc只是回收了垃圾对象占用内存,但是并不会整理内存,当有对象需要分配在老年代(不管是晋升还是大对象直接分配),没有足够的连续内存分配的时候,就会触发一次内存整理,这个内存整理过程会SWT(和serial old过程一样),那这次gc的STW就会比较旧。
为什么清除算法有碎片问题,但是CMS还是使用了清除算法呢?
就是为了回收阶段不暂停业务线程。因为对于整理算法和复制算法,在回收的时候,对象的内存地址其实是要变化的,所以在回收过程也是需要暂停业务线程的。但是清除算法因为不会改变对象的内存地址,就可以做到业务线程和gc线程线程并行运行。
总结起来CMS量大问题:
1. 解决漏标问题效率太低了,即使remark的时候重新扫描
2. 因为清除算法会有碎片的问题,可能导致回退到serial old收集器来gc,所以可能导致某次耗时特别的长。这就会导致系统的不稳定
参考这个博客R大的回复:
https://hllvm-group.iteye.com/group/topic/44381?page=2
G1的解决思路就是破坏第二个条件:原本指向这个白色对象引用的灰色对象,被业务线程删除了。基本思路就是在灰色指向其他对象引用断开时,记录下断开的引用。然后在重新标记的时候,来看是否还有对象指向了断开引用指向的对象。

如上图,G1会记录下B-->C这个断开的引用,在重新标记的时候,就会去看是否还有对应应用了C,如果有,就会标记C。这样就会不会把C漏标了。
G1是如果知道B-->C的引用断开了呢?答案就是SATB,简单理解就是在gc开始的时候记录下了引用的快照,和快照相比,发现断开了B-->C的引用,于是将B-->C的这个引用会放到gc线程的运行栈中去,然后gc再次运行的时候,就会去看是否还有其他对象引用C,如果有,就会将C标记,避免漏标。
所以下一个问题就是SATB这个快照是如何创建的?难道真的是将当前堆里所有对象的引用关系都copy了一份?答案肯定不是的。这里其实可以引申一下,mysql事务开启的时候,也会创建一个快照视图(它肯定是不可能copy整个数据库的数据的,否则会疯的);ES的scroll扫描大量数据的时候,也是在初始化scroll的时候创建了一个快照(它肯定也不是copy整个索引的)。背后的基本思想个人总结就是:记录的其实是一个id水位。比如mysql,事务开启的时候,其快照记录的其实是:当前有哪些正在执行中的事务,且事务id是递增的,所以小于这个集合中最小id的事务一定是已经提交的、大于这个集合最大的id的事务一定是当前事务启动后创建的,所以利用这个水位关系来实现了快照;ES中的道理也打通小异,再回到这里的SATB,其实也是利用了类似的思想来记录快照的。
所以总结起来看:
除此之外,G1和CMS在最后回收阶段有个不同:
这里主要的问题就是:不回收的那个分区引用了当前回收的区域中的对象的时候,会有问题。比如yong gc的时候回收年轻代,但是有老年代的对象引用了年轻代的对象,年轻代的这个对象就不能被回收。

反过来,只是回收老年代的时候,有年轻代对象引用了老年代对象的情况,也是一样的。
解决这方式:全量扫描一遍非收集区,比如yong gc的时候,全量扫描一遍老年代,看看有没有老年代对象引用新生代对象。
对于old区的回收,这么搞是ok的,因为这个时候可以先触发一次yong gc,然后年轻代存活对象也不多了,所以这么扫描不会耗时太久;但是反过来,yong gc的时候,其目的就是只回收年轻代,这个时候去全量扫描老年代,就失去了分区gc的意义了。
对于CMS来说,解决yong gc过程,有old区对象引用年轻代对象的情况,在年轻代中引入了卡表(card table),卡表中记录了有哪些老年代对象引用了年轻代的对象,在引用变更的时候,写屏障中会来更新卡表。
这样在yong gc的时候,不需要全量扫描老年代的对象,看是否引用了新生代对应,只需要扫描年轻代的卡表就可以了。而对于回收old区的时候,会先触发一次yong gc,然后回收old区的时候,扫描yong区就好了(其实这也是cms垃圾回收的一个耗时点)
而对于G1来说,分代在G1中,分代只是一个逻辑概念了,其真实的内存布局已经变成了分区(Region)的,gc回收的时候,也是按照Region来回收的,所以这个问题就转换成了跨Region引用的问题了。
G1的解决方式就是每个Region都维护了一个Rset(Remember Set)来记录了其他Region引用了当前Region的对象,在回收对应Region的时候,扫描Rset就可以了。
打印gc日志 -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCDetails 打印gc的详细日志 -XX:+PrintGCCause 打印产生gc的原因 // 如下是指定gc日志输出的方式 -Xloggc:/Users/george/gclog/gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M
另外,-Xmx10M,配置成10M,更容易观察到full gc。
parNew和CMS的组合:
-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=30 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=1
parNew收集年轻代日志:

GC Allocation Failure就是造成本次gc的原因。Allocation Failure表示的就是新建对象分配内存失败导致的gc。常见的gc cause参考美团的技术博客:Java中9种常见的CMS GC问题分析与解决
cms收集老年代老年代:

CMS-concurrent-sweep(并发清除)
CMS-concurrent-reset(并发重置)
-XX:+UseG1GC
G1相关的参数
| -XX:G1HeapRegionSize=n | Region的大小。但是这不是最终值,Region大小会根据实际情况自动调整的 |
| -XX:MaxGCPauseMillis | 一次gc回收期望的STW时间,默认为200ms。G1会尽量在指定的这个时间内完成gc。 |
| -XX:G1NewSizePercent | 新生代最小值,默认值5% |
| -XX:G1MaxNewSizePercent | 新生代最大值,默认值60% |
| -XX:ParallelGCThreads | STW期间,并行GC线程数 |
| -XX:ConcGCThreads=n | 并发标记阶段,并行执行的线程数 |
| -XX:InitiatingHeapOccupancyPercent | 设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包old+humongous |
ps:查看java的参数:
下一篇:NNDL 作业12:第七章课后题