垃圾收集器与内存分配策略(三)
垃圾收集器
收集算法是内存回收的方法论,而垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器如何实现没有硬性规定。因此各大厂商、不同JDK版本提供的垃圾收集器都会有很大差别。
如图所示,共有7种作用于不同分代的收集器,如果收集器之间存在连线,代表可以配合使用
1、Serial收集器
是最基本,也是历史最久的收集器,在JDK1.3之前是新生代垃圾收集的唯一选择。
是个单线程的收集器,只使用一个收集线程去完成工作,并且在收集时,暂停其他所有工作线程,直到收集线程结束工作。所谓的“stop the world”,对绝大数应用来说是不可接受的,好比是计算机每运行一小时就得暂停响应5分钟,下图是Serial收集器的运行过程
虽然如此,但实际上依旧是虚拟机运行在客户端程序上的默认新生代收集器,简单而高效,在桌面应用场景中,分配给新生代内存不会很大,收集几十兆的内存,停顿时间大约一百毫秒以内,只要不是频繁发生,这都是可以接受的,所以,客户端场景下,Serial收集器是一个很好的选择。
2、ParNew收集器
实际就是Serial收集器的多线程版本。两者的收集算法、控制参数、回收策略都相同。下图是ParNew收集器的工作过程
是运行在服务端模式下的虚拟机首选的新生代收集器,是除了Serial收集器外,目前唯一一个能与CMS收集器配合的。ParNew收集器可以使用参数-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定。可以使用参数-XX:ParallelGCThreads来限制垃圾收集的线程数。
3、Parallel Scavenge收集器
该收集和ParNew收集器一样,是个新生代的收集器,采用复制算法,并行的多线程收集器。Parallel Scavenge收集器的特点是关注点和其他收集器不同,CMS等收集器的目的是尽可能地缩短垃圾收集时用户线程暂停时间,而Parallel Scavenge收集器是为了达到可控的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),如虚拟机总运行了100分钟,垃圾收集花了1分钟,吞吐量就是99%
Parallel Scavenge收集器提供了两个参数用于控制吞吐量
- -XX:MaxGCPauseMillis:最大垃圾收集停顿时间。是一个大于0的毫秒数,收集器尽量保证内存回收时间不超过这个值,但并不是越小越好。GC停顿缩短时间是牺牲吞吐量和新生代空间换取的,也将导致垃圾收集会发生的更加频繁,比如原来10秒收集一次,暂停100毫秒,现在5秒收集一次,每次70毫秒,停顿时间在下降,吞吐量也在下降。
- -XX:GCTimeRatio:设置吞吐量大小。是一个大于0小于100的整数。假设值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。比如设置为9,则垃圾收集时间为1/1+9=10%,最大垃圾收集时间占总时间的10%,默认为99,即垃圾收集时间占总时间的1%
还有一个参数-XX:+UseAdaptiveSizePolicy,是GC自适应的调节策略,当这个参数打开后,虚拟机会根据当前系统的运行情况会自动调提供最合适的停顿时间或最大的吞吐量。这个策略也是和ParNew收集器的一个重要区别。
4、Serial Old收集器
是Serial老年代的版本,同样是单线程,使用“标记-整理”算法。也是用于客户端模式下的虚拟机。下图是工作过程示意图
5、Parallel Old收集器
是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。开始于JDK1.6。Parallel Old收集器的出现,“吞吐量”收集器名副其实,其工作过程如下
6、CMS收集器(Concurrent Mark Sweep)
JDK1.5时诞生,具有划时代的意义,第一款并发收集器,实现了让垃圾收集线程和用户线程(基本上)同时工作。以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法,运行过程分为四种
- 初始标记:仍需要“Stop The World”,该过程仅仅是标记一个GC Roots能直接关联到的对象,速度快。
- 并发标记:就是进行GC Roots追踪搜索的过程。
- 重新标记:仍需要“Stop The World”,为了修正并发标记期间,因程序继续运行而导致标记改动的对象的标记记录,该阶段停顿时间比初始标记时间稍长,比并发标记要短。
- 并发清除:并发标记和清除是整个过程中耗时最长的,并且可以与用户线程一起工作,所以总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器运行步骤如下
虽然CMS收集器有着并发垃圾收集、低停顿的特点,但是也不是完美的,也有以下三个缺点
- 基于“标记-清除”算法,会导致大量内存碎片产生。当大对象分配内存时,就可能会提前多做一次Full GC(老年代的垃圾回收)操作。CMS为了解决这个问题,提供了-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),作用就是在多做Full GC时,对内存碎片进行整理,但缺点是停顿时间变长了。还有一个参数是-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,下一次进行压缩整理(默认0,代表每次GC时,都进行压缩)
- 对CPU资源敏感。并发时,虽然不会导致用户线程停顿,但是会占用一部分线程,导致程序变慢。CMS默认的回收线程数是(CPU数量+3)/4
- 无法处理浮动垃圾。所谓浮动垃圾就是,因为CMS支持并发,所以当内存清理时用户线程还在运行,这时可能存在新的垃圾产生,但是无法在本次清理过程中处理,只好下一次GC时再清理。也正是这个原因,所以CMS不能像别的老年代收集器一样,等内存填满再清理,需要预留空间。JDK1.5是使用了68%就会激活内存清理,1.6时提升到了92%,可以调整参数-XX:CMSInitiatingOccupancyFraction来提高百分比。当预留内存不够时就会出现“Concurrent Mode Failure”异常,虚拟机会临时启用Serial Old收集器重新进行老年代的垃圾收集,这样会导致停顿时间变长。
7、G1收集器
JDK1.7发布,是一款面向服务端应用的垃圾收集器。HotSpot虚拟机期望它未来可以替代JDK1.5发布的CMS收集器。与其他收集器相比,有以下特点
- 并行和并发:充分利用多CPU、多核环境来缩短停顿时间,G1收集器可以通过并发的方式不暂停用户线程的执行
- 分代收集:可以单独管理整个GC堆,不需要和别的收集器配合使用,可以使用不同方式处理新创建的对象和熬过多次GC的旧对象,获得更好的收集效果
- 空间整合:整体是基于“标记-整理”算法,局部是基于“复制”算法,这两种算法都不会产生内存碎片,收集后可提供规整的可用内存。此特性有利于程序长时间运行,不会因为分配大对象时造成内存不够用而提前触发GC的场景
- 可预测的停顿:能够让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。其原因在于它可以有计划地避免在整个Java堆进行全区域的垃圾收集。根据每个区域垃圾堆积价值大小建立列表,后根据时间优先收集价值最大的区域,此做法保证了G1可以在有限的时间内达到更高的收集效率
在G1之前的收集器,管理范围都是新生代或老年代,而G1是管理整个Java堆,将整个Java堆划分为多个大小相等的区域,虽然新生代和老年代的概念还保留着,但不像之前那样隔离开来。
在G1收集器中,区域之间对象的引用以及其他收集器新生代和老年代之间的对象引用,虚拟机都是通过Remembered Set来避免全堆扫描的,G1中的每个区域都有一个与之对应的Remembered Set,如果发现引用对象处于不同的区域(新老年代中),通过CardTable将相关引用信息记录到被引用对象的区域的Remembered Set中。当回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全堆范围进行扫描也不会出现遗漏的情况。
在不计算维护Remembered Set操作情况下,G1收集器的运作步骤大致可分为:
- 初始标记:与CMS收集器相似,仅仅标记GC Roots能直接关联到的对象,并修改TAMS的值,让下一阶段用户程序并发运行时,能在正确的区域创建新对象,该阶段需要停顿用户线程,但耗时极短。
- 并发标记:与CMS收集器相似,从GC Root开始对堆中对象进行可达性分析,找出存活对象,该阶段耗时较长,但不需要停顿用户线程,可并发执行
- 最终标记:与CMS收集器相似,为了修正并发标记期间因用户线程继续运行而导致标记发生改动的那一部分标记记录,虚拟机将其变化,记录在线程Remembered Set Logs中,该阶段需要将Remembered Set Logs中的数据合并到Remembered Set中,需要停顿线程,但可并行执行。
- 筛选回收:该阶段首先对每个区域的回收价值进行排序,根据用户期望的GC停顿时间制定回收计划,也可与用户线程并发执行。
下图是G1收集器运行示意图
8、理解GC日志
[GC (System.gc()) [PSYoungGen: 8061K->560K(76288K)] 8061K->568K(251392K), 0.0017111 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 560K->0K(76288K)] [ParOldGen: 8K->409K(175104K)] 568K->409K(251392K), [Metaspace: 3161K->3161K(1056768K)], 0.0040151 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
- GC日志开头的“[GC”和“[Full GC”说明了垃圾收集的停顿类型,并非区别新生代还是老年代的GC,如果有Full,说明这次GC是发生了Stop-The-World的,如果调用了System.gc(),就是显示为Full GC (System.gc()。
- “[PSYoungGen”“[ParOldGen”指的是GC发生的区域,显示的区域名和使用的GC收集器密切相关,如果是ParNew收集器,新生代为“[ParNew”,如果是Parallel Scavenge收集器,新生代为“[PSYoungGen”,老年代和永久代同理,名称也由收集器决定的。
- 方括号内部的8061K->560K(76288K),指的是“GC前该内存区域已使用容器->GC后该内存以使用容量(该内存区域总容量)”。如果方括号外也有这样的,就代表“GC前Java堆已使用容量->GC后Java堆已使用容量(Java总容量)”。
- 0.0040151 secs指的是该内存区域GC所占的时间,单位是秒。[Times: user=0.00 sys=0.00, real=0.00 secs] 是更具体的时间数据,分别代表用户态消耗的CPU时间、内核态消耗的CPU时间和操作从开始到结束所经过的墙钟时间。
垃圾收集器参数总结
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收 |
UseConcMarkSweepGC | 打开此开关后,使用ParNew+ CMS + Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old (PS Mark Sweep)的收集器组合进行内存回收 |
UserParallelOldGC | 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收 |
SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Survivor = 8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代 |
UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC时间占总时间的比率,默认值是99, 即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效 |
MaxGCPauseMillis | 设置GC的最大停顿时间。仅在使用Parallel Scavenge收集器时生效 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代时间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效 |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用CMS收集器时生效 |