收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现 JDK1.7的收集器如图,问号是G1
如果两个收集器存在连线,就说明可以搭配使用,虚拟机所处的区域,则表示属于新生代还是老年代收集器,接下来逐一了解这些收集器 不过我们要明确一个观点,我们比较收集器的目的是选择最适合场景的收集器,而不是比较出一个最好的收集器
Serial收集器
可以搭配的老年代收集器,CMS和Serial Old Serial收集器是最基本,历史最悠久的收集器,在Jdk1.3.1之前是虚拟机新生代收集的唯一选择,是单线程的收集器,它进行收集时,必须STW,直到它收集结束,即JVM在后台,在用户不可见的情况下,把用户正常的线程全部停掉,这对很多应用来说都是难以接受的 如图,这时Serial+Serial Old的收集流程
但是到现在为止,他依然是虚拟机运行在client模式下的默认的新生代收集器,优点是简单,高效(与其他收集器的单线程比),在client的桌面级场景中,收集几十或者几百MB的新生代,停顿时间在100ms之内,是可以接受的,所以Serial收集器在client模式下是一个不错的选择
补充一下JVM的server与client模式 JVM在client模式默认-Xms是1M,-Xmx是64M;JVM在Server模式默认-Xms是128M,-Xmx是1024M; server:启动慢,编译更完全,编译器是自适应编译器,效率高,针对服务端应用优化,在服务器环境中最大化程序执行速度而设计。 client:快速启动,内存占用少,编译快,针对桌面应用程序优化,为在客户端环境中减少启动时间而优化;
ParNew 收集器
可以搭配的老年代收集器,CMS与Serial OLd 因为实现与Serial大体相似,所以搭配也相同 ParNew是Serial的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括所有Serial可用的控制参数,收集算法,STW,对象分配规则,回收策略与Serial完全一样,在实现上,这两种收集器也公用了相当多的代码 如图,这时ParNew搭配Serial Old的流程
虽然ParNew除了多线程以外,其他与Serial的实现大体相似,但是却是许多运行在server模式下的首选的新生代收集器,++其中一个与性能无关,但很重要的原因就是,CMS只能和Serial与ParNew一起工作++
ParNew收集器是使用 -XX:UseConcMarkSweepGC后,即开启老年代CMS后,新生代首选的收集器,也可以使用 -XX:UseParNewGC来强制选择它
随着可以使用的CPU的核心数的增加,ParNew对于资源的利用更好,默认开启的线程数,与CPU数量相同,可以使用 -XX:ParallelGCThreads 参数来限制GC的线程数
并发和并行 并行(Parallel) 指多条垃圾收集线程并行工作,但此时用户线程处于等待 并发(Concurrent) 用户线程与垃圾收集器同事执行,但不一定是并行的,可能会交替执行,可能用户程序在继续运行,而垃圾收集程序在另一个CPU上运行
Parallel Scavenge 吞吐量优先收集器(PSYoungGen)
可以搭配的老年代Serial Old(万物搭配) 和Parallel Old Parallel Scavenge是一个新生代的收集器,它也是使用复制算法的收集器,又是并行多线程的收集器,看起来与ParNew一样,那它的特别之处是什么? 特点是它的关注点与其他的收集器不同,++PS收集器目标是达到一个可以控制的吞吐量++,即吞吐量=运行用户代码时间/(运行用户代码+GC时间),如JVM总运行100min,垃圾收集1min,吞吐量为99%
停顿时间越短,越适合需要与用户交互的程序,良好的响应速度可以提升用户的体验,而高吞吐量可以更高效的利用CPU时间,尽快的完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
PS收集器提供了两个参数来精确的控制吞吐量,分别是控制最大垃圾停顿时间的 -XX:MaxGCPauseMills 参数(这个参数允许的值是大于0的毫秒数,收集器将尽可能的保证内存回收花费的时间不超过设定值)以及直接设置吞吐量大小的 -XX:GCTimeRatio参数(值为大于0小于100的整数X,计算方式 允许最大GC时间百分比=1/(1+X),默认为99,就是允许1%的最大收集时间)
由于和吞吐量关系密切,PS收集器也经常被称为吞吐量优先的收集器,除了上面两个参数,还有一个-XX:UseAdaptiveSizePolicy值得关注,这是一个开关的参数,参数打开之后,就不需要指定新生代的大小(-Xmn),Eden区与Survivor区的比例(-XX:SurvivorRatio),晋升老年代对象大小(-XX:pretenureSizeThreshold),虚拟机会根据当前系统的运行情况来动态的调整这些参数,以提供最合适的停顿时间或者最大的吞吐量,这种调节方式叫做GC自适应调节策略,只需要-Xmx最大堆设置好,然后设置-XX:MaxGCPauseMills 参数(更关注最大停顿时间),-XX:GCTimeRatio参数还是更关注吞吐量,给JVM一个优化目标即可,自适应调节也是PS收集器与ParNew一个重要的区别.
那么除了G1我们三个新生代的收集器就说完了,下面开始老年代收集器
Serial Old收集器
Serial Old收集器是Serial的老年代版本,同样是一个单线程收集器,使用标记-整理算法,这个收集器主要也是给client模式下的虚拟机使用,一个是与PS搭配使用,另外一个,另一种是作为CMS收集器的后备方案,在并发收集的时候发生Concurrent Mode failure时使用
Parallel Old收集器(ParOldGen)
Parallel Old是Ps收集器的老年代版本,使用多线程和标记-整理算法,是jdk1.6提供的,在之前PS只能搭配Serial Old进行单线程的老年代收集,由于老年代Serial Old的拖累,使用了PS也不一定在服务器上获得最大的吞吐量,单线程的Serial对老年代的回收无法利用多核心CPu的优势,这个组合不一定比得上ParNew+CMS
直到Parallel Old出现,吞吐量优先收集器有了应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以使用吞吐量两兄弟 Parallel Scavenge与Parallel Old
来到了本文的重点,CMS与G1
CMS Concurrent Mark Sweep
CMS收集器是一种,以获取最短回收停顿时间为目标的收集器,互联网或者各种B/S服务端尤其重视服务的响应速度,希望系统停顿时间最短,CMS就最符合需求
从名字我们看出是CMS是一种标记清除算法的收集器,整个运行过程分为4个步骤
-
初始标记 CMS inital mark 触发第一次STW 初始标记需要STW,仅仅是标记一下GC ROOTS能直接关联到的对象,以及由新生代中存活对象所引用的老年代对象,这个过程是支持多线程的(JDK7 之前单线程,JDK8 之后默认并行,可通过参数 -XX:+CMSParallelInitialMarkEnabled 调整)。由于只标记直接关联对象,速度很快.
-
并发标记 CMS concurrent mark 并发标记阶段 GC 线程和应用线程并发执行,遍历
阶段1:初始标记
出来的存活对象,然后继续递归标记这些对象可达的对象。 -
并发预清理 (Concurrent Preclean) 并发预清理阶段,GC线程和应用线程也是并发执行,因为
阶段2
是与Java线程并发执行,有些引用关系已经改变,通过卡片标记(Card Marking),提前把老年代的逻辑空间划分为相等大小的区域,当引用空间改变,JVM会把已经发生改变的区域标记为脏区
(dirty Card),然后在本阶段,这些脏区会被找出来,刷新引用,清除脏区标记 其实这一步,还是在继续完成我们的并发标记工作,只是将并发标记阶段同时改变的引用关系更新 -
并发可取消的预清理 (Concurrent Abortable Preclean) 并发可取消的预清理阶段也不该停止线程,本阶段尝试在STW的最终标记阶段(Final Remark)之前尽量的多做一些工作,以尽量减少应用暂停时间
在该阶段不断循环处理:标记老年代的可达对象、扫描处理 Dirty Card 区域中的对象, 即还在处理并发预清理的事,完成对脏区的处理工作 循环的终止条件有:
- 达到循环次数
- 达到循环执行时间阈值
- 新生代内存使用率达到阈值
- 重新标记 CMS final remark 第二次也是最后一次 STW 重新标记阶段是修正并发标记期间,因为用户程序继续运作而导致的标记产生变动的那一部分对象的标记记录,这个阶段停顿时间会比初始标记阶段稍长一些,但是远比并发标记的时间短,目标是完成老年代所有的存活对象的标记,这个阶段会执行
- 遍历新生代对象重新标记
- 根据GC Roots重新标记
- 遍历老年代的Dirty Card 重新标记
-
并发清除 CMS Concurrent sweep 同用户线程一起进行并发清理工作,并发清除阶段与应用程序并发执行,不需要 STW 停顿,根据标记结果清除垃圾对象。 注意这里CMS的标记,是标记存活,清理其他的,而之前我们了解的标记清除算法,是双标记可以清理的对象
-
并发重置 (Concurrent Reset) 并发重置阶段与应用程序并发执行,重置 CMS 算法相关的内部数据, 为下一次 GC 循环做准备。
由于耗时最久的并发标记和并发清除过程中收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收是和用户线程一起并发执行的
CMS是一款优秀的老年代收集器,主要优点是:并发收集,低停顿,也称之为并发低停顿收集器(Concurrent low pause collector)
CMS的缺点
- CMS对CPU资源敏感,其实,面向并发的程序都对CPU资源比较敏感,在并发阶段,并不会导致用户线程停顿,但是会因为占用了一部分的CPU资源,导致应用程序变慢,总吞吐量降低, CMS默认启动的回收线程为
(CPU数量+3)/4
由此可见在低cpu核心数时,CMS对用户的影响很大 - CMS无法处理浮动垃圾(Floating Garbage),可能会导致"Concurrent mode failure"失败导致另一次的FULL GC,因为并发清理阶段是与用户线程并行的,CMS无法在当次收集中回收,只能留待下次GC清理,这一部分叫做浮动垃圾
- 也是因为CMS运行时用户线程还要运行,那就是说要留有足够的内存空间给用户线程使用,因此CMS不能像其他的收集器一样等老年代快要填满再进行收集,需要预留一部分空间提供并发收集时的程序运作使用,在jdk1.5的默认设置下,CMS收集器当老年代使用了68%就会被激活,可以通过 -XX:CMSInitiaingOccupancyFraction的值来提高触发的百分比,以便降低内存收集器调用次数,从而获得更好的性能,在jdk1.6中,CMS收集器的启动阈值已经到了92% 如果CMS运行期间,预留的内存无法满足程序的需要,就会出现Concurrent Mode Failure 这时JVM将启动后备方案,启动Serial Old收集器来重新进行老年代的收集,STW的时间将会很久,所以说这个百分比调的太高引发错误后,性能降低很多 最后一个缺点,由于采用标记清除算法,结束收集时将产生大量的内存碎片,为了解决这个问题,CMS默认开启了 -XX:+UseCMSCompactAtFullCollection,即进行fullGC时合并整理内存碎片,还有个参数是 -XX:CMSFullGCsBeforeCompaction,即一次内存碎片整理前多少次fullGC,默认为0,即每次都整理碎片
Young GC(Minor GC)/Old GC/Full GC/Mixed GC
- Young GC 当 JVM 无法为新对象分配在新生代内存空间时总会触发 Young GC。比如 Eden 区占满时,新对象分配频率越高,Young GC 的频率就越高。 Young GC 每次都会引起全线停顿(Stop-The-World),暂停所有的应用线程,停顿时间相对老年代 GC 造成的停顿,几乎可以忽略不计。
- Old GC 只清理老年代空间的 GC 事件,只有 CMS 的并发收集是这个模式。
- Full GC 清理整个堆的 GC 事件,包括新生代、老年代、元空间等 。
- Mixed GC 清理整个新生代以及部分老年代的 GC,只有 G1 有这个模式。
G1
G1收集器是当今收集器技术发展的前沿成果之一,是一款面向服务器端应用的垃圾收集器
它的特点是
- 并行与并发:G1能够充分的利用多CPU,多核环境下的硬件优势,使用多个CPU(core)来缩短STW时间,部分其他的收集器原本需要停顿Java线程来执行的GC操作,G1收集器仍然可以通过并发的方式,让Java程序继续执行
- 分代概念:分代概念在G1中保留,G1可以不配合其他收集器,独立完成GC堆的收集工作,但它可以用不同的方式处理新创建的对象,和已经活了一阵的,熬过多次GC的旧对象以获得更好的收集效果
- 空间整合:G1从整体上来看属于标记-整理算法,从局部(两个Region)来看是基于复制算法实现,也就说G1无论如何不会产生内存碎片,这种特性有利于程序的长时间运行,分配大对象不会因为没有连续的内存空间触发下一次GC
- 可预测的停顿:降低STW是G1和CMS共同的关注点,但是G1除了追求低停顿之外,还能建立可预测的停顿时间模型,能让使用者明确指定,在一个长度Mms的时间片段内,GC消耗的时间不超过Nms
在G1之前,其他的收集器作用域都是新生代或者老年代,G1不再是这样,G1将整个Java堆划分为多个Region,虽然保有新生代和老年代的概念,但是不再是物理隔离的,他们都是一部分Region的集合
Region 为实现大内存空间的低停顿时间的回收,将堆划分为多个大小相等的 Region。每个小堆区都可能是 Eden 区,Survivor 区或者 Old 区,但是在同一时刻只能属于某个代。 在逻辑上, 所有的 Eden 区和 Survivor 区合起来就是新生代,所有的 Old 区合起来就是老年代,且新生代和老年代各自的内存 Region 区域由 G1 自动控制,不断变动。
巨型对象 当对象大小超过 Region 的一半,则认为是巨型对象(Humongous Object),直接被分配到老年代的巨型对象区(Humongous Regions)。 这些巨型区域是一个连续的区域集,每一个 Region 中最多有一个巨型对象,巨型对象可以占多个 Region。
G1 把堆内存划分成一个个 Region 的意义在于 每次 GC 不必都去处理整个堆空间,而是每次只处理一部分 Region,实现大容量内存的 GC。 通过计算每个 Region 的回收价值,包括回收所需时间、可回收空间,在有限时间内尽可能回收更多的垃圾对象,把垃圾回收造成的停顿时间控制在预期配置的时间范围内,这也是 G1 名称的由来:Garbage-First。
G1收集器避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个Region中的垃圾堆价值大小(回收所获得的空间和回收所需要时间的经验值) 在后台维护一个列表,每次根据允许的收集时间,优先回收价值最大的Region,这种使用Region划分内存空间的以及有优先级的区域回收方式,保证了G1在有限的时间内,可以获得尽可能高的收集效率
在G1中,Region之间的对象引用,或者其他收集器中新生代和老年代之间的对象引用,虚拟机是靠Remembered Set来避免全堆扫描的 G1中每个Region都有一个与之对应的Remembered Set,JVM在发现程序在对Reference类型进行写操作时,先产生一个Write Barrier中断写操作,检查Reference引用的对象是否在不同的Region之间(分代的例子就是处于新生代和老年代之间),如果是,便通过CardTable把相关的引用信息记录到++被引用对象所属的Region++的Remembered Set中,在进行内存回收时,在GCroot枚举范围内增加Remembered Set就可以不用对全堆进行扫描
G1工作模式 针对新生代和老年代,G1 提供 2 种 GC 模式,Young GC 和 Mixed GC,两种都会导致 Stop The World。
- Young GC 当新生代的空间不足时,G1 触发 Young GC 回收新生代空间。 Young GC 主要是对 Eden 区进行 GC,它在 Eden 空间耗尽时触发,基于分代回收思想和复制算法,每次 Young GC 都会选定所有新生代的 Region。 同时计算下次 Young GC 所需的 Eden 区和 Survivor 区的空间,动态调整新生代所占 Region 个数来控制 Young GC 开销。
- Mixed GC 当老年代空间达到阈值会触发 Mixed GC,选定所有新生代里的 Region,根据全局并发标记阶段(下面介绍到)统计得出收集收益高的若干老年代 Region。 在用户指定的开销目标范围内,尽可能选择收益高的老年代 Region 进行 GC,通过选择哪些老年代 Region 和选择多少 Region 来控制 Mixed GC 开销。
G1收集器的步骤
-
初始标记 STW 标记GCroots能直接关联到的对象,修改TAMS(Next Top at Mark Start)让下一阶段,程序并发运行时,能在正确可用的Region中创建新的对象,需要STW,但是耗时很短
-
并发标记 GCroots对堆中的对象进行可达性分析,找出存活的对象,耗时长,并发执行
-
最终标记 修正并发标记期间,用户线程对对象引用的修改,JVM把这段时间对象的变动记录在线程Remembered Set lOGs里面,最终标记阶段,需要把Remembered Set Logs合并到Remembered Set中,这阶段需要STW但是可以并行执行
-
筛选回收 最后的筛选回收是首先对各个Region的回收价值和时间成本进行排序,根据用户所期望的GC停顿时间,来制定回收计划