垃圾回收器
如果说收集算法是内存回收的方法论,那垃圾回收器就是内存回收的实践者
Serial收集器
Serial收集器是最基础,历史最悠久的收集器,曾经(再jdk1.3.1之前) 是hotSpot虚拟机新生代收集器的唯一选择,这个收集器是一个单线程工作的收集器,但是他的"单线程"的意义并不仅仅是说明他只会使用一个处理器或者一条收集县城去完成垃圾收集工作,更重要的是强调他进行垃圾收集时,必须暂停其他所欲的工作线程"Stop The World",直到他收集结束
工作流程图
从jdk1.3开始,一直到现在最新的jdk19,HotSpot虚拟机开发团队为消除或降低用户线程因垃圾收集而导致停顿的努力一直持续进行,从Serial收集器到parallel收集器,再到Conccurrent Mark Sweep(CMS) 和 Garbage First(G1)收集器,最终至现在的垃圾收集器的最前沿成果Shenandoah和ZGC等,我们看到了一个个越来越构思精巧,越来越优秀,也越来越复杂的垃圾收集器不断涌现,用户县城的停顿时间再持续缩短,但是仍然没有办法彻底消除,探索更优秀垃圾收集器的工作仍在继续
可以使用命令来查看当前JDK使用的垃圾收集器版本
-XX:+PrintCommandLineFlags
特点
对于单核处理器或者处理器核心数较少的环境来说,Serial收集器由于没有现成交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率,再用户桌面的应用场景以及近年来流行的部分为服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量)垃圾收集的停顿时间完全可以控制在十几,几十,至多一百多毫秒以内,只要不是频繁地发生收集,这点停顿时间对于许多用户来说完全可以接受的,所以Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择
开启Serial收集器的命令:-XX:+UseSerialGC
ParNew收集器
我们运行在服务器上的java系统,其实可以充分利用服务器的多核CPU资源的优势,比如通常服务器配置为4核CPU,如果我们使用Serial收集器单线程进行垃圾回收,是没有充分利用我们的CPU资源的
特点
ParNew垃圾收集器主打的就是多线程垃圾回收机制,除了同事使用多条线成进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio,-XX:pretenureSizeThreshold,-XX:HandlePromotionFailure等),,收集算法,Stop the World,对象分配规则,回收策略等都与Serial收集器完全一致,再实现上的两种收集器也共用了相当多的代码
运行流程图
补充说明
ParNew收集器除了多线程实现垃圾收集之外,其他没有什么太多的创新之处,但是它确实是Server模式下的新生代首选虚拟机收集器,其中一个重要的原因就是除了Serial收集器之外,只有他能与CMS配合使用
新生代使用ParNew,老年代使用CMS收集器,这是目前大部分线上生产系统的标配组合
Paralle收集器(Parallel Scavenge + Parallel Old)主要适合再后台运算而不需要太多交互的分析任务,最高效率利用CPU资源,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数(新生代大小,Eden和Survivor比例,晋升老倪爱戴对象大小等)以提供最合适的停顿时间或者最大的吞吐量
老年代CMS收集器
一般老年代选择的垃圾回收器就是CMS(Concurrent Mark Sweep)收集器,这是一种获取最短回收停顿时间为目标的收集器,我们大部分互联网网站或者基于浏览器的B/S系统的服务端,这类应用通常都会较为关注服务的相应速度,希望系统停顿时间尽可能缩短,以给用户带来良好的交互体验,CMS收集器就非常符合这类应用的需求
运行流程
从名字(Mark Sweep)上可以看出来CMS收集器是基于'标记-清楚算法'实现的,他的运作过程相对于前面集中收集起来说要更复杂一些,整个过程分为四个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
初始标记
首先根据"可达性分析算法"来判断有哪些对象是被GC RootS给引用的,如果是的话,就是存活对象,否则就是垃圾对象,然后将垃圾对象标记出来
注意:
初始标记的过程会让系统停止工作,进入到"Stop The World"状态,不过这个过程很快,仅仅标记GCRoots直接引用的那些对象
并发标记
并发标记阶段恢复系统正常运行,可以随意的创建对象,同事并发标记线程也开始工作,这里由于一边进行对象的创建,必然会持续增加新的对象产生,同时也有可能对一些对象失去引用变成垃圾对象
针对所有老年代种存在的对象以及不断新增的对象都会被进行标记,而我们的系统县城也会一直工作不断产生对象,所以该阶段也是最耗时的,虽然是耗时的,但是垃圾回收与系统是并行进行的,所以并不会对系统的运行造成影响
重新标记
由于我们的第二个阶段是并发标记,那么肯定会造成有部分对象已经失去了引用变成垃圾对象没有来得及进行更正,以及新创建的对象还未来得及标记,因此当前阶段:重新标记,会暂停我们的系统县城,并重新整理
并发清除
将上述标记出来的垃圾对象进行整理清除
总结
通过上述CMS工作的整个过程,总结如下
最耗时的阶段:并发标记与并发清除,不过该阶段是与用户线程并发执行并不影响系统
初始标记和重新标记:需要stop the world,暂停系统工作,但是这两个阶段速度很快几乎影响不大
CMS 的优缺点
CMS 是一款优秀的收集器,主要的优点在于并发收集,低停顿,一些官方公开文档也称之为并发低停顿收集器,CMS收集器是HotSpot虚拟机追求地停顿的第一次成功的尝试,但是下面还有三个明显的缺点
1.并发导致CPU资源紧张
CMS默认启动的回收线程数量(处理器核心数量+3)/4
比如我们常见的机器:2核4G,那么分配给CMS的回收线程数=(2+3)/4=1,直接占据了一半的CPU资源
因此建议在实际开发中,可以配置的服务器至少为4核或者以上
2.Con-current Mode Failure问题
【浮动垃圾】,由于CMS收集器无法处理浮动垃圾,有可能出现"Con-current mode failure"失败而导致另一次完全"Stop The World"的Full GC的产生
再CMS 的并发标记和并发清理阶段,用户线程是还在继续运行的,程序再运行自然就会伴随着有新的垃圾对象不断的产出,但是这一部分垃圾对象是出现在标记过程结束以后,CMS无法再档次收集中处理掉他们,只好留待下一次垃圾收集时再清理掉,这一部分垃圾就称为浮动垃圾
3.预留空间
由于垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他垃圾收集器一样等待到老年代几乎完全被填满了在进行收集,必须预留一部分空间供并发收集时的程序运作使用
4.新对象分配失败,要是CMS运行期间预留的内存无法满足程序分配新对象的需求,就会出现一次“并发失败”(Current Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用SerialOld收集器来重新进行老年代的垃圾收集,但是这样停顿的时间太长了
再JDK6时,CMS收集器的启动阈值已经默认提升到了92%,可以通过参数-XX: CMSInitiatingOccu-pancyFraction 的值来提高CMS的触发百分比,但是该参数设置的太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来衡量设置
5.内存碎片问题
CMS是一款基于"标记-清除"算法实现的收集器,收集结束时会有大量的空间碎片产生,空间碎片过多的时,会给大对象的分配带来很大的麻烦,从而导致不得不提前触发一次FullGC的情况
为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数,默认是开启的,此参数从jdk9开始废弃,用于再CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,在Shenandoah和ZGC出现前,是无法并发的,这样空间碎片问题是解决了,但是停顿时间会变长
G1收集器
里程碑式的产物
概念
Garbage First(G1),开创了收集器面向局部手机的设计思路和基于Region的内存布局形式
就是将Java堆内存拆分为多个大小相等的Region,并且也有新生代和老年代的概念,只不过是逻辑概念,每个Region可以根据需要,扮演新生代的Eden空间,Survivor空间,或者老年代的空间
思想
G1收集器之前,其他所有的收集器,包括CMS在内,垃圾收集的目标范围要么就是整个新生代,要么就是整个老年代,再要么就是整个Java堆,而G1跳出了这个樊笼,他可以面向堆内存中任何部分来组成回收集(Collection Set 一般简称CSet),进行回收,衡量标准不在是它属于哪个分代,而是那块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式
G1收集器可以同时回收新生代和老年代对象,不需要两个垃圾回收器在进行配合使用了
Region中还有一类特殊的Humongous区域,专门用来存储大对象,G1认为只要大小超过了一个Region容量一半的对象就判断为大对象,每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1M-32M且应为2的N次幂,而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待
优势
G1收集器可以建立可预测的停顿时间模型
我们现在可以通过直接给定G1收集器一个指定时间,交给G1全权负责,达成时间限制的目标
G1是如何做到对垃圾回收导致的系统停顿可控的?
具体思路是,G1会对每个Region里回收价值进行追踪,动态的判断如何回收
回收价值:G1必须搞清楚每个Region里面到底有多少的垃圾对象需要回收,以及回收这些独享需要消耗多长的时间
G1收集器去跟踪每个Region里面的垃圾堆积的价值大小,价值及回收所需要的空间大小以以及回收所需要的时间的经验值,然后再后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,使用参数(-XX:MaxGCPauseMillis指定,默认是200毫秒),优先处理回收价值收益最大的那些Region,这也就是"Garbage First"名字的由来,这种使用Region划分内存空间以及具有优先级的区域回收方式,保证了G1收集器再有限的时间内获取尽可能搞得收集效率,这也是G1的核心设计思路
相关的JVM参数:
-XX: +UseG1GC: 在JDK8中可以通过手动指定使用G1收集器进行回收
-XX: G1HeapRegionSize=size : 指定一个Region的大小
-XX: MaxGCPauseMillis=time 指定收集的停顿时间,默认是200ms
G1中的Region是如何分配的?每个Region的大小是多少
默认情况下是自己分配和设置,我们可以通过参数-Xms和-Xmx来设置堆内存的大小,默认情况下是分配2048个Region,比如我们设置堆内存大小为2G,那么分配到2048个Region钟,每个Region的大小就是1MB,而且Region的大小也必须是2的倍数,比如1MB,2MB,4MB等
我们可以通过-XX: G1HeapRegionSize来手动指定每一个Region的大小
这里还有一些默认配置:
新生代默认开始分配占比是:5%,可以通过"-XX: G1NewSizePercent"来设置新生代的初始占比
新生代的运行过程中最多可以分配到60%的堆内存,可以通过"-XX: G1MaxNewSizePercent"来设置
新生代钟默认也是按照8:1:1来对Eden和Survivor去进行分配