Hello,今天记录下 Java虚拟机中的其中一个重点知识 --> 垃圾回收算法及分代垃圾收集器。
一起学习,一起进步。继续沉淀,慢慢强大。希望这文章对您有帮助。若有写的不好的地方,欢迎评论给建议哈!
初写博客不久,我是杨展浩。这是我的第十四篇博客。加油!!!
直接进入主题,先介绍下垃圾收集器的分类。
1、次收集器 Scavenge GC
指发生在新生代的 GC,因为新生代的 Java 对象大多都是朝生夕死,所以Scavenge GC 非常频繁,一般回收速度也比较快。当 Eden 空间不足以为对象分配内存时,会触发 Scavenge GC。一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC,对 Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC 会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。当年轻代堆空间紧张时会被触发相对于全收集而言,收集间隔较短
2、全收集器 Full GC
指发生在老年代的 GC,出现了 Full GC 一般会伴随着至少一次的 Minor GC(老年代的对象大部分是 Scavenge GC 过程中从新生代进入老年代),比如:分配担保失败。 FullGC 的速度一般会比 Scavenge GC 慢 10 倍以上。当老年代内存不足或者显式调用 System.gc()方法时,会触发 Full GC。当老年代或者持久代堆空间满了,会触发全收集操作可以使用 System.gc()方法来显式的启动全收集全收集一般根据堆大小的不同,需要的时间不尽相同,但一般会比较长。
3、分代垃圾回收器种类的常规匹配
4、分代垃圾收集器 (对上面第三点的一一讲解)
4.1、串行收集器(Serial)
Serial 收集器是 Hotspot 运行在 Client 模式下的默认新生代收集器, 它的特点是: 只用一个 CPU(计算核心)/一条收集线程去完成 GC 工作,且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称 STW)。可以使用-XX:+UseSerialGC 打开。
虽然是单线程收集,但它却简单而高效,在 VM 管理内存不大的情况下(收集几十 M~一两百 M 的新生代),停顿时间完全可以控制在几十毫秒~一百多毫秒内。
4.2、并行收集器(ParNew)
ParNew 收集器其实是前面 Serial 的多线程版本, 除使用多条线程进行 GC 外, 包括 Serial可用的所有控制参数、收集算法、 STW、对象分配规则、回收策略等都与 Serial 完全一样(也是 VM 启用 CMS 收集器-XX: +UseConcMarkSweepGC 的默认新生代收集器)。由于存在线程切换的开销,ParNew 在单 CPU 的环境中比不上 Serial,且在通过超线程技术实现的两个 CPU 的环境中也不能 100%保证能超越 Serial. 但随着可用的 CPU 数量的增加,收集效率肯定也会大大增加(ParNew 收集线程数与 CPU 的数量相同, 因此在 CPU 数量过大的
环境中,可用-XX:ParallelGCThreads=<N>参数控制 GC 线程数)。
4.3、Parallel Scavenge 收集器
与 ParNew 类似,Parallel Scavenge 也是使用复制算法, 也是并行多线程收集器。但与其他收集器关注尽可能缩短垃圾收集时间不同,Parallel Scavenge 更关注系统吞吐量:系统吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)停顿时间越短就越适用于用户交互的程序 - 良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务 - 可以最高效率地利用CPU时间,尽快地完成程序的运算任务。Parallel Scavenge 提供了如下参数设置系统吞吐量。
4.4、Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法
4.5、Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法,吞吐量优先,主要与 Parallel Scavenge 配合在注重吞吐量及 CPU 资源敏感系统内使用;
4.6、CMS 收集器(Concurrent Mark Sweep)
CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器,一款真正意义上的并发收集器,虽然现在已经有了理论意义上表现更好的 G1 收集器,但现在主流互联网企业线上选用的仍是 CMS(如 Taobao、微店)。CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器),基于”标记-清除”算法实现, 整个 GC 过程分为以下 4 个步骤:
1. 初始标记(CMS initial mark)
2. 并发标记(CMS concurrent mark:GC Roots Tracing 过程)
3. 重新标记(CMS remark)
4. 并发清除(CMS concurrent sweep:已死对象将会就地释放,注意:此处没有压缩)
其中 1, 3 两个步骤(初始标记、重新标记)仍需 STW。但初始标记仅只标记一下 GC Roots能直接关联到的对象,速度很快;而重新标记则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,虽然一般比初始标记阶段稍长,但要远小于并发标记时间。
CMS 特点:
1、CMS 默认启动的回收线程数=(CPU 数目+3) / 4,当 CPU 数>4 时,GC 线程一般占用不超过 25%的 CPU 资源,但是当 CPU 数<=4 时,GC 线程可能就会过多的占用用户 CPU 资源,从而导致应用程序变慢,总吞吐量降低。
2、无法处理浮动垃圾, 可能出现 Promotion Failure、 Concurrent Mode Failure 而导致另一次 Full GC 的产生:浮动垃圾是指在 CMS 并发清理阶段用户线程运行而产生的新垃圾。由于在 GC 阶段用户线程还需运行,因此还需要预留足够的内存空间给用户线程使用,导致 CMS不 能 像 其 他收 集 器那 样 等到老年代几乎填满了再进行收集。因此 CMS 提 供 了 -XX:CMSInitiatingOccupancyFraction 参 数 来 设 置 GC 的 触 发 百 分 比 ( 以 及
-XX:+UseCMSInitiatingOccupancyOnly 来启用该触发百分比),当老年代的使用空间超过该比例后 CMS 就会被触发(JDK 1.6 之后默认 92%)。但当 CMS 运行期间预留的内存无法满足程序需要,就会出现上述 Promotion Failure 等失败,这时 VM 将启动后备预案:临时启用 Serial Old 收集器来重新执行Full GC(CMS通常配合大内存使用,一旦大内存转入串行的Serial GC,那停顿的时间就是大家都不愿看到的了)。
3、最后,由于 CMS 采用”标记-清除”算法实现,可能会产生大量内存碎片。内存碎片过多可能会导致无法分配大对象而提前触发 Full GC。因此 CMS 提供了-XX:+UseCMSCompactAtFullCollection 开关参数,用于在 Full GC 后再执行一个碎片整理过程。但内存整理是无法并发的,内存碎片问题虽然没有了,但停顿时间也因此变长了,因此 CMS还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction 用于设置在执行 N 次不进行内存整理的 Full GC 后, 跟着来一次带整理的(默认为 0: 每次进入 Full GC 时都进行碎片整理)
4.7、分区收集- G1 收集器
G1(Garbage-First)是一款面向服务端应用的收集器,主要目标用于配备多颗 CPU 的服务器治理大内存。
- G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector(CMS)。
-XX:+UseG1GC 启用 G1 收集器。
与其他基于分代的收集器不同,G1 将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。如:
每块区域既有可能属于 O 区、也有可能是 Y 区,因此不需要一次就对整个老年代/新生代回收。而是当线程并发寻找可回收的对象时,有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时 G1 仍然需要暂停应用线程,但可以用相对较少的时间优先回收垃圾较多的 Region。这种方式保证了 G1 可以在有限的时间内获取尽可能高的收集效率。
G1的新生代收集跟ParNew类似:存活的对象被转移到一个/多个Survivor Regions。如果存活时间达到阀值,这部分对象就会被提升到老年代。
G1 年轻代 GC 特点如下:
一整块堆内存被分为多个 Regions。
存活对象被拷贝到新的 Survivor 区或老年代。
年轻代内存由一组不连续的 heap 区组成, 这种方法使得可以动态调整各代区域尺寸。
Young GC 会有 STW 事件, 进行时所有应用程序线程都会被暂停。
多线程并发 GC。
G1 老年代 GC 特点如下:
并发标记阶段
1 在与应用程序并发执行的过程中会计算活跃度信息。
2 这些活跃度信息标识出那些 regions 最适合在 STW 期间回收(which regions will be best to reclaim during an evacuation pause)。
3 不像 CMS 有清理阶段。
再次标记阶段
1 使用 Snapshot-at-the-Beginning(SATB)算法比 CMS 快得多。
2 空 region 直接被回收。
拷贝/清理阶段(Copying/Cleanup Phase)
1 年轻代与老年代同时回收。
2 老年代内存回收会基于他的活跃度信息。
5、常见垃圾回收算法
5.1、引用计数(Reference Counting)
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用的问题。
5.2、复制(Copying)
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小, 同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。 简图如下:
5.3、标记-清除(Mark-Sweep)
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。 简图如下:
5.4、标记-整理(Mark-Compact)
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。 简图如下: