参考文献
极客时间JAVA训练营
GC介绍
随着电脑硬件的升级,电脑的内存和内核数的增加,开发人员不在需要谨小慎微的使用内存和线程,GC便也随着JDK的升级而充分发挥了系统的资源。
无论是串行GC到并行GC的升级,还是CMS到G1的升级,都在以分而治之的思想为核心来做到降低系统的延迟性。
除了分而治之的思想以外,CMS和并行GC区别在于,对于GC的更细粒度的划分,就像我们平时开发时候划水,本来一个小时的忙碌,七个小时的休息,被我们划分为一天的摸鱼。
而G1则是有着一个不同于CMS和并行GC的升级,那就是我们不在拘泥于对年轻代和老年代的泾渭分明的划分,我想原因是随着内存的增大,我们不得不对内存大小和模块划分更多的阶级,就如同你原来只有一个一百平米的地,你需要盖一个房子,这个房子两层,一层住爸妈,一层住你。而随着系统资源的增加,我们拥有了更多的土地,所以我们便不会在局限于一个房子,或者两层的房子。随着房子的增多和层数的增加,我们需要街道,门牌号等等对于不同的房子的划分。这也对应着我们的内存,对象,GC的不同处理方式。
why?
为什么CMS使用标记清除算法?
为了提升系统延迟性,摒弃了整理阶段,而使用当碎片到达一定数量时才进行整理,使用空间换时间。
MinorGC、Major GC、Full GC、Mixed GC 的区别?
- MinorGC对于年轻的GC,伴随着STW.
- Major GC对于老年代GC,伴随着STW。
- Full GC是清理整个堆空间—包括年轻代和永久代,MinorGC会造成Full GC,Full GC的同时又包含MinorGC和Major GC。伴随着STW.
- Mixed GC 混合模式是在于MinorGC的基础上,对于一些容易清理,不耗费大量时间的老年代清理,伴随着STW。
GC策略
串行(Serial GC)
UseSerialGC
-XX:+UseSerialGC开启
年轻代使用标记复制算法(mark copy)
串行
收集器,老年代使用标记清除整理算法(mark sweep compact)串行
收集器GC时会STW(stop the word)
适用于单核机器,以及堆内存只有几百mb时使用。
优点
1.在堆内存较小时,虽是单线程,但是内存较小,无论是年轻代的标记复制算法(mark copy),还是老年代使用的标记清除整理算法(mark sweep compact),GC的STW(stop the work)时间,相较于并行、CMS、G1的时间,在没有内存溢出的前提下,性能相当。
2.相较于CMS和G1,吞吐量更高。
3.GC周期之间不消耗系统资源
缺点
1.多核机器时,由于GC属于单线程,无法对多个线程资源进行有效利用,STW(stop the word)时间较长。
2.堆内存较大时,单线程对于年轻代的标记复制(mark copy)和老年代的标记清除整理(mark sweep compact)时间较长。
3.GC时无论是年轻代的标记复制算法(mark copy),还是老年代使用的标记清除整理算法(mark sweep compact),都需要STW(stop the word)。
并行(Parallel GC)
UseParallelGC(单并行)
-XX:+UseParallelGC开启
年轻代使用标记复制算法(mark copy)
并行
收集器,老年代使用标记清除整理算法(mark sweep compact)串行
模式GC时会STW(stop the word)
适用于多核机器,以及对于系统延迟没有那么高要求的,比如ERP、OMS等,不利于银行,金融对于GC暂停时间较长有要求的系统。
UseParallelOldGC(双并行)
-XX:+UseParallelOldGC开启
年轻代使用标记复制算法(mark copy)
并行
收集器,老年代使用标记清除整理算法(mark sweep compact)并行
收集器GC时会STW(stop the word)
适用于多核机器,以及对于系统延迟没有那么高要求的,比如ERP、OMS等,不利于银行,金融对于GC暂停时间较长有要求的系统。
优点
1.相较于串行GC,多线程的GC会缩短STW的时间,两次GC之间不消耗系统资源。
2.相较于CMS和G1,吞吐量更高。
3.GC周期之间不消耗系统资源
缺点
1.内存较小时,线程数不多的系统中无法对性能进行提升。
2.当堆内存高达几百G甚至上T时,STW(stop the word)的时间依然较长。
3.GC时无论是年轻代的标记复制算法(mark copy),还是老年代使用的标记清除整理算法(mark sweep compact),都需要STW(stop the word)。
相关参数
-XX:ParallelGCThreads=N 来指定 GC 线程数, 其默认值为 CPU 核心数。
CMS(ConcMarkSweep )
并发标记清除 (UseConcMarkSweepGC)
-XX:+UseConcMarkSweepGC开启
年轻代使用标记复制算法(mark copy)
并行
收集器,老年代使用标记清除算法(mark sweep)并发
收集器年轻代GC时会STW(stop then word),老年代GC时会和应用线程并行进行,某些节点才会STW(stop then word)
适用于多核系统,对于系统延迟要求较高的系统,比如银行,金融等
ConcMarkSweep
- Initial Mark(初始标记)
- Concurrent Mark(并发标记)
- Concurrent Preclean(并发预清理)
- Final Remark(最终标记)
- Concurrent Sweep(并发清除)
- Concurrent Reset(并发重置)
初始标记
- 伴随着STW(stop then word)暂停。
- 初始标记的目标是标记所有的根对象,包括根对象直接引用的对象,以及被年轻代中所有存活对象所引用的对象(老年代单独回收)。
并发标记
- 遍历老年代,标记所有的存活对象,从初始标记 找到的根对象开始算起。
- 应用程序同时运行,不用STW(stop then word)暂停。
并发预清理
- 应用线程并发执行的,不需要STW(stop then word)。
- 因为并发标记与程序并发运行,可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化,JVM会通过Card(卡片)的方式将发生了改变的区域标记为
脏区
,这就是所谓的卡片标记
(Card Marking)。
最终标记
- 最后一次STW(stop then word)暂停。
- 完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发执行的,有可能GC线程跟不上应用程序的修改速度。所以需要一次 STW暂停来处理各种复杂的情况。
- 通常 会尝试在年轻代尽可能空的情况下执行,以免连续触发多次STW(stop then word)事件。
并发清除
- 与应用程序并发执行,不需要STW(stop then word)停顿。
- JVM在此阶段删除不再使用的对象,回收他们占用的内存空间。
并发重置
- 重置CMS算法相关的内部数据,为下一次GC循环做准备
优点
1.相较于并行,系统延迟更低。
2.堆内存较高时,仍可以保持较低的STW(stop then work)的时间
缺点
1.老年代GC伴随应用程序同时进行,由于占用系统资源,会导致吞吐量降低。
2.老年代内存碎片问题(因为不压缩),在某些情况下 GC 会造成不可预测的暂停时间,特别是堆内存较大的情况下。
垃圾优先(Garbage First G1)
-XX:+UseG1GC开启
G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。
G1 GC 最主要的设计目标是:将 STW 停顿的时间和分布,变成可预期且可配置的。
G1 GC 是一款软实时垃圾收集器,可以为其设置某项特定的性能指标。为了达成可预期停顿时间的指标,G1 GC 有一些独特的实现。
内存划分
堆不再分成年轻代和老年代,而是划分为多个(通常是2048个)可以存放对象的小块堆区域(smaller heap regions)。
每个小块,可能一会被定义成 Eden 区,一会被指定为 Survivor或者Old 区。
在逻辑上,所有的 Eden 区和 Survivor 区合起来就是年轻代,所有的 Old 区拼在一起那就是老年代。
每次 GC 暂停都会收集所有年轻代的内存块,但一般只包含部分老年代的内存块。
在并发阶段估算每个小堆块存活对象的总数。构建回收集的原则是: 垃圾最多的小块会被优先收集。
G1 GC 的处理步骤
1.年轻代模式转移暂停(Evacuation Pause)
G1 GC 会通过前面一段时间的运行情况来不断的调整自己的回收策略和行为,以此来比较稳定地控制暂停时间。在应用程序刚启动时,G1 还没有采集到什么足够的信息,这时候就处于初始的 fully-young 模式。当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存活区。如果还没有存活区,则任意选择一部分空闲的内存块作为存活区。拷贝的过程称为转移(Evacuation),这和前面介绍的其他年轻代收集器是一样的工作原理。
2.并发标记(Concurrent Marking)
Initial Mark(初始标记)
,此阶段标记所有从 GC 根对象直接可达的对象。Root Region Scan(Root区扫描)
,此阶段标记所有从 “根区域” 可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。Concurrent Mark(并发标记)
此阶段和 CMS 的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。Remark(再次标记)
和 CMS 类似,这是一次 STW 停顿(因为不是并发的阶段),以完成标记过程。 G1 收集器会短暂地停止应用线程,停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。Cleanup(清理)
最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升GC的效率,维护并发标记的内部状态。 所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的:例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的 STW 暂停。
3.转移暂停: 混合模式(Evacuation Pause (mixed))
执行一次混合收集(mixed collection),不只清理年轻代,还将一部分老年代区域也加入到回收集中。
混合模式的转移暂停不一定紧跟并发标记阶段。有很多规则和历史数据会影响混合模式的启动时机。比如,假若在老年代中可以并发地腾出很多的小堆块,就没有必要启动混合模式。因此,在并发标记与混合转移暂停之间,很可能会存在多次 young 模式的转移暂停。具体添加到回收集的老年代小堆块的大小及顺序,也是基于许多规则来判定的。其中包括指定的软实时性能指标,存活性,以及在并发标记期间收集的 GC 效率等数据,外加一些可配置的 JVM 选项。混合收集的过程,很大程度上和前面的 fully-young gc 是一样的。
G1 GC 的注意事项
特别需要注意的是,某些情况下 G1 触发了 Full GC,这时 G1 会退化使用 Serial 收集器来完成垃圾的清理工作,它仅仅使用单
线程来完成 GC 工作,GC 暂停时间将达到秒级别的。
并发模式失败
G1 启动标记周期,但在 Mix GC 之前,老年代就被填满,这时候 G1 会放弃标记周期。
**解决办法
:**增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads 等)。
晋升失败
没有足够的内存供存活对象或晋升对象使用,由此触发了 Full GC(to-space exhausted/to-space overflow)。
**解决办法
:**增加 –XX:G1ReservePercent 选项的值(并相应增加总的堆大小)增加预留内存量。通过减少 –XX:InitiatingHeapOccupancyPercent 提前启动标记周期。也可以通过增加 –XX:ConcGCThreads 选项的值来增加并行标记线程的数目。
巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间。
**解决办法
:**增加内存或者增大 -XX:G1HeapRegionSize
相关参数
-
-XX:G1NewSizePercent:初始年轻代占整个 Java Heap 的大小,默认值为 5%;
-
-XX:G1MaxNewSizePercent:最大年轻代占整个 Java Heap 的大小,默认值为 60%;
-
-XX:G1HeapRegionSize:设置每个 Region 的大小,单位 MB,需要为 1、2、4、8、16、32 中的某个值,默认是堆内存的1/2000。如果这个值设置比较大,那么大对象就可以进入 Region 了;
-
-XX:ConcGCThreads:与 Java 应用一起执行的 GC 线程数量,默认是 Java 线程的 1/4,减少这个参数的数值可能会提升并行回收的效率,提高系统内部吞吐量。如果这个数值过低,参与回收垃圾的线程不足,也会导致并行回收机制耗时加长;
-
-XX:+InitiatingHeapOccupancyPercent(简称 IHOP):G1 内部并行回收循环启动的阈值,默认为 Java Heap的 45%。这个可
以理解为老年代使用大于等于 45% 的时候,JVM 会启动垃圾回收。这个值非常重要,它决定了在什么时间启动老年代的并行回收;
-
-XX:G1HeapWastePercent:G1停止回收的最小内存大小,默认是堆大小的 5%。GC 会收集所有的 Region 中的对象,但是如果下降到了 5%,就会停下来不再收集了。就是说,不必每次回收就把所有的垃圾都处理完,可以遗留少量的下次处理,这样也降低了单次消耗的时间;
-
-XX:G1MixedGCCountTarget:设置并行循环之后需要有多少个混合 GC 启动,默认值是 8 个。老年代 Regions的回收时间通常比年轻代的收集时间要长一些。所以如果混合收集器比较多,可以允许 G1 延长老年代的收集时间。
-
-XX:+G1PrintRegionLivenessInfo:这个参数需要和 -XX:+UnlockDiagnosticVMOptions 配合启动,打印 JVM 的调试信息,每个Region 里的对象存活信息。
-
-XX:G1ReservePercent:G1 为了保留一些空间用于年代之间的提升,默认值是堆空间的 10%。因为大量执行回收的地方在年轻代(存活时间较短),所以如果你的应用里面有比较大的堆内存空间、比较多的大对象存活,这里需要保留一些内存。
-
-XX:+G1SummarizeRSetStats:这也是一个 VM 的调试信息。如果启用,会在 VM 退出的时候打印出 Rsets 的详细总结信息。如果启用 -XX:G1SummaryRSetStatsPeriod 参数,就会阶段性地打印 Rsets 信息。
-
-XX:+G1TraceConcRefinement:这个也是一个 VM 的调试信息,如果启用,并行回收阶段的日志就会被详细打印出来。
-
-XX:+GCTimeRatio:这个参数就是计算花在 Java 应用线程上和花在 GC 线程上的时间比率,默认是 9,跟新生代内存的分配比例一致。这个参数主要的目的是让用户可以控制花在应用上的时间,G1 的计算公式是 100/(1+GCTimeRatio)。这样如果参数设置为9,则最多 10% 的时间会花在 GC 工作上面。Parallel GC 的默认值是 99,表示 1% 的时间被用在 GC 上面,这是因为 Parallel GC 贯穿整个 GC,而 G1 则根据 Region 来进行划分,不需要全局性扫描整个内存堆。
-
-XX:+UseStringDeduplication:手动开启 Java String 对象的去重工作,这个是 JDK8u20 版本之后新增的参数,主要用于相同String 避免重复申请内存,节约 Region 的使用。
-
-XX:MaxGCPauseMills:预期 G1 每次执行 GC 操作的暂停时间,单位是毫秒,默认值是 200 毫秒,G1 会尽量保证控制在这个范围内。
-
-XX:MaxGCPauseMillis=50