前言:
我们知道jvm中堆内存没有被引用的对象是垃圾对象,当堆中内存剩余过少时会触发gc对对象进行回收;如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现;jvm 发展这么多年肯定会有一些垃圾回收的策略供我们选择使用,那么jvm 中都有哪些垃圾回收策略?在项目中我们又怎样去选择一款合适的垃圾收集策略?
本文以JDK8为基础通过以下几点来探讨:
1 jvm 中垃圾回收器及其特点;
2 怎么选择一款合适的垃圾回收器;
1 jvm 中垃圾回收策略及其特点:
1.1 Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程。
特点:
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
1.2 ParNew ;
可以把这个收集器理解为Serial收集器的多线程版本。
特点:
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器;
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差(多线程单cpu会有线程的上下文切换影响效率)。
1.3Parallel Scavenge:
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量。
【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%】
参数:
-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,
尽可能的保证每次垃圾回收的时间控制在设置的该值时间内;并不是越小越好,越小,意味着每次收集到的垃圾越少,会增加gc 的频次;
-XX:GCTimeRatio直接设置吞吐量的大小。
吞吐量:取值0-100 的整数
如设置成19 则是100分钟的时间允许有5分钟的时间进行垃圾回收
XX:+UseAdaptiveSizePolicy
自适应策略,bool 类型,一旦打开,不需要再去设置新生代的内存大小,会跟随系统动态伸缩;
1.4 Serial Old :
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算法",运行过程和Serial收集器一样。
特点:
1.5 Parallel Old :
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收,也是更加关注系统的吞吐量。
Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合;
特点:
算法:标记-整理算法
适用范围:老年代
优点:多线程高吞吐量;
缺点:收集过程需要暂停所有线程
1.6 CMS:
官网 : https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#co
ncurrent_mark_sweep_cms_collector
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。
【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】
采用的是"标记-清除算法",整个过程分为4步:
(1)初始标记 CMS initial mark 标记GC Roots直接关联对象,不用Tracing,速度很快
(2)并发标记 CMS concurrent mark 进行GC Roots Tracing
(3)重新标记 CMS remark 修改并发标记因用户程序变动的内容以及扫描标记在第二阶段产生的新对象;
(4)并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾;
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。
总体上CMS是款优秀的收集器,但是它也有些缺点。
(1)cms堆cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。
(2)cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC
(3)由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了 -XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关【默认开启】,用于CMS顶不住要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)
(4)并发模式失败场景:
在并发标记和并发清理阶段,如果业务系统产生的对象,已经在old 区装不下了,这个时候会触发业务线程的暂停;
并发失败,backGroud 转换为ForeGroud;切换到其他垃圾收集器(Serial old),随之业务线程停止,并执行fullgc:
为了避免并发模式失败场景:就需要old 区在剩余一定比例的空间是就暂停业务;
第一个参数是设置阈值(默认值-1 不开启),第二个是辅助参数,当第一个参数设置后,需要将第二个参数设置为true(如果不设置,第一个参数设置的阈值只会执行一次) ;
默认值:
(5) 浮动垃圾:由于cms支持运行的时候用户线程也在运行,程序运行的时候会修改在并发阶段已经被扫描并标记为存活的对象的引用(如此时将应用线程将改对象的引用置空),虽然改对象已经是垃圾,但是本次不会清理需要等待下次gc。
扩展:
1 并发标记是和用户线程一起进行的,这个时候如果对象的引用发生改变;或者产生了新的对象怎么处理,最终清除的时候会被视为垃圾回收吗?
并发标记是和用户线程一起进行的,此时就可能存在有新的对象晋升到老年代,或者原有的老年代对象虽然之前是有引用的,但是此时没有被引用了;对于第二种此时没有被引用了,也就是此时的对象是无效的,因为之前已经进行过扫描标注了其为存活对象,那么只要等到下次gc去回收改对象就好了;
对于第一种情况,新晋的对象此时它肯定是没有被扫描到的,那么就必须有一套规则,来再一次对其进行扫描和标记,最终标记刚好就可以用来处理这件事情,但是有一个问题,堆中有如此多的gcroot引用链,肯定不能对所有的引用链进行一遍扫描,因为这样相当于变相否定了,第二个并发阶段做的工作,已停顿时间优先的cms肯定也不会采取此种方式;那么就必须要对gc root 引用链上的对象进行一些特殊的标记,这样就可以从此标记的对象处继续往下寻找到这些新晋的对象进行是否存活的标记;这个特殊的标记就是在进行对象的扫描时使用三色标记法对对象进行标记;
三色标记:
还没有被扫描到的对象是一种颜色(白色);已经被扫描的对象是一种颜色(黑色),正在扫描的对象是一种颜色(灰色);
只要一条链上存在未被扫描过的对象,那么就存在,正在扫描的颜色节点;
如果A新增加了一个引用,到C(C尚未被扫描的);cms 做法,增加A的引用,改变A的颜色,从已经扫描完成的黑色变为正在扫描的灰色;
过程:
2 并发清除时是和用户线程一起进行的,这个时候产生的新的对象会被当做垃圾回收吗?
因为并发清除时是和用户线程一起进行的,此时就有可能产生新的对象,这些对象晋升到老年代时,因为用了三色标记法标记队形,那么此时的对象一定是未被扫描到的(白色),但是这些对象很可能有存活的对象,所以肯定是不能进行回收的(虽然可能存在一些死亡的对象);所以cms 就需要为这些对象进行一些特殊的处理,来保证正在进行的并发清除不会印象到这些对象;
并发清理时,肯定会有一个白色对象集合,根据白色对象集合遍历清除对象所占用的空间。不妨想象一下,开始垃圾回收前,虚拟机创建三个集合白色对象集合、灰色对象集合、黑色对象集合。刚开始时老年代的对象都是白色,则都在白色对象集合中,随着标记过程开始,白色对象开始转移到灰色对象集合中,再由灰色集合转移到黑色对象集合中,最终只留下白色对象集合和黑色对象集合有对象,除了白色对象集合和黑色对象集合使用以外的内存就是空闲列表了。
并发清理时是根据白色对象集合去清理对象,此时用户线程新产生的老年代对象是分配的空闲列表中,不会影响白色对象集合的内容,清除时就保证了对新产生的对象不产生影响,这些新产生的对象将在下次垃圾回收时进行处理。
3 相关参数:
//开启CMS垃圾收集器
-XX:+UseConcMarkSweepGC
//默认开启,与-XX:CMSFullGCsBeforeCompaction配合使用
-XX:+UseCMSCompactAtFullCollection
//默认0 ,几次Full GC后开始整理,每次fullgc 都进行内存整理;
-XX:CMSFullGCsBeforeCompaction=0
//辅助CMSInitiatingOccupancyFraction的参数,不然CMSInitiatingOccupancyFraction只会使
用一次就恢复自动调整,也就是开启手动调整。
-XX:+UseCMSInitiatingOccupancyOnly
//取值0-100,按百分比回收
-XX:CMSInitiatingOccupancyFraction 默认-1
使用标记清除算法(background 常规模式)jdk 1.7 之前初始标记是一个线程,jdk1.8 之后初始标记就是多个线程的,会产生空间碎片;
Bool 类型,开启后初始标记是并行的;
4 老年代的引用链一部分在新生代的处理方式:
当gc root 的对象链中有一部分在新生代,一部分在老年代;cms 是老年代的算法,要想直到位于old 的那一部分对象是存活对象,就必须要去扫描新生代;这样做就相当于扫描了整个堆;
那么怎么做,才能让扫描新生代更高效,先在新生代进行一次young gc 回收大部分的对象;这样存活的对象就少,在扫描新生代效率自然就高;
新生代策略:
这两个参数,就是当新生代的空间剩余2M的时候启动,直到新生代剩余空间达到50%;
或者等待一定时间(默认5s) 之后不管是否进行了younggc 都进行重新标记;
1.7 G1(Garbage-First) :
官网 : https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#g
arbage_fifirst_garbage_collection
G1是一个面向服务端的JVM垃圾收集器,适用于多核处理器、大内存容量的服务端系统。 它满足短时间停顿的同时达到一个高的吞吐量。从JDK 9开始,G1成为默认的垃圾回收器
所谓garbarge first 垃圾优先,其实就是优先回收垃圾最多的Region区域 ;
特点:
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),G1默认region的最小大小为1M,最大大小为32M,且每个region的大小是2^n次,同时默认将堆划分为2048块,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合,并且构成新生代和老年代的区域可以动态改变。
1.7.1 Young GC
当Eden区的空间占满之后,会触发Young GC,G1将Eden和Survivor中存活的对象拷贝到Survivor,或者直接晋升到Old Region中。Young GC的执行是多线程的,期间会停顿所有的用户线程(STW);
1.7.2 Old GC
当堆空间的占用率达到一定阈值后会触发Old GC(阈值由命令参数-XX:InitiatingHeapOccupancyPercent设定,默认值45);
Old GC分为以下步骤完成:
(1)分代收集(仍然保留了分代的概念)
(2)空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消
耗在垃圾收集上的时间不得超过N毫秒)
1.7.2.1 并发标记过程中的引用改变怎么处理:
G1 关注的是引用消失,只要消失则进行推到栈中,不关心后续C是否又被引用;下一轮在进行gc 的时候,在通过gc root 扫描到栈中的这个特殊对象;
相关参数:
-XX: +UseG1GC 开启G1垃圾收集器
-XX: G1HeapReginSize 设置每个Region的大小,是2的幂次,1MB-32MB之间
-XX:MaxGCPauseMillis 最大停顿时间
-XX:ParallelGCThread 并行GC工作的线程数
-XX:ConcGCThreads 并发标记的线程数
-XX:InitiatingHeapOcccupancyPercent 默认45%,代表GC堆占用达到多少的时候开始垃圾收集
2.1 Jvm 垃圾收集器比较:
2.2 怎么选择圾收集器:
有两个参数标准可以参考:吞吐量和停顿时间 ;
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;
高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互
的任务。
如何选择垃圾收集器:
Serial 和Serial old 在工作的时候用户线程会被停止,是单线程的,适用于,单个cpu 或者设置的内存小于100M(没有停顿时间的要求);使用以下参数开启:
ParNew(新生代),parallel Scavenge(新生代),Parallel Old(老年代): 并行的垃圾收集器;多个线程进行垃圾回收,工作的时候,用户线程被停止,收集效率高;(吞吐量优先)如果对于停顿的时间没有过高的要求,允许停顿时间超过1s,可以考虑使用:(Parallel Scavenge Parallel Old)使用以下参数开启:
CMS(老年代),G1(新生代和老年代):并发类垃圾收集器,用户线程和垃圾回收线程可以一起工作;
如果对于停顿时间要求较高,停顿时间小于1s;使用以下参数开启:
CMS 和G1 如何选择:
1 )单核或双核使用G1和CMS 都不好(存在线程上下文的切换,不如Parallel);
2 )G1 要求的最小堆内存是:2G; 稳定允许需要8g 对硬件要求较高;至少需要4核8G;
如果服务器硬件足够(至少需要4核8G)但是版本是jdk7(或者5,6) 考虑使用CMS;
2.3 Jvm 垃圾收集器开启参数:
(1)串行
-XX:+UseSerialGC
-XX:+UseSerialOldGC
(2)并行(吞吐量优先):
-XX:+UseParallelGC
-XX:+UseParallelOldGC
(3)并发收集器(响应时间优先)
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
扩展:
1 并发和并行的补充:
1.1 程序的并发(Concurrent):
在操作系统中,是指一个时间段上有几个程序都处于已经启动运行到运行已经完毕之间,且这几个程序都是在同一个处理器_上运行。
并发不是真正意义上的”同时进行“,知识CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理的的当,就可以让用户感觉是多个应用程序同时在进行;
1.2 程序的并行(Parallel):
当系统有一个以上的CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(parallel)。
其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行;
并发的多个任务之间是互相抢占资源的;
并行的多个任务之间是互不抢占资源的;
只有在多CPU或一个CPU多核的情况中,才会发生并行,否则,看似同时发生的事情没事都是并发执行的;
1.3 垃圾回收器的并行:
指多条垃圾收集线程并行工作,但是此时用户线程仍处于等待状态;
1,.4 垃圾回收器的并发(Concurrent):
指用户线程与垃圾收集县城同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行;
1.5 垃圾回收器的串行(Serial):
相较于并行的概念,单线程执行如果内存不够,则程序暂停,启动JVM垃圾回收,回收完,再启动程序线程;
2 吞吐量和停顿时间的的关系:
程序运行完毕的时间包括用户线程运行总时间+垃圾回收线程总时间。
程序运行过程中,一般都不是只发生一次垃圾回收,如果考虑每次最短停顿时间的平均值,那么此时每次回收的垃圾就会减少,垃圾回收的频率就会增加,但是因为垃圾回收造成用户线程暂停,而用户线程在运行和暂停之间切换的成本是很高的,因此这反而会造成用户线程运行时间更短,这就造成吞吐量下降。