垃圾收集器
如果说收集算法是内存回收的方法论,那么Garbage收集器就是内存回收的实践者
Serial
Serial收集器是最基础、历史最悠久的收集器。这个收集器只能单线程工作。并不仅指的是只使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调它在进行垃圾收集的时候,必须暂停其他所有工作线程,直到它结束。
与其他收集器比较,Serial收集器简单高效,对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般不会特别大,比较适合用Serial。
ParNew
PreNew实质上就是Serial收集器的多线程并发版本,除了同时使用多条线程进行垃圾收集外,其余的行为与Serial收集器完全一致。在JDK7之前的遗留的系统中首选的新生代收集器,因为除了Serial收集器外,目前只有它能与CMS收集器配合工作
它默认开启与处理器核心数量相同的收集线程数。可以用 -XX:ParallelGCThreads参数来限制垃圾收集的线程数
Parallel Scavenge
Parallel Scavenge收集器也是一款新生代收集器,它同样基于标记-复制算法实现实现。
Parallel Scavenge收集器关注点与其他收集器不同,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput -> CPU用于运行用户代码的时间与CPU总消耗时间的比值)
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:-XX:GCTimeRatio参数用于设置吞吐量大小,-XX:MaxGCPauseMillis参数用于控制最大垃圾收集停顿时间,收集器会尽可能的保证用户的设定值
Parallel Scavenge收集器也被称作“吞吐量优先处理器”
Serial Old
Serial Old时Serial收集器的老年代版本,它同样是单线程收集器,使用标记-整理算法。
主要是给客户端使用
如果是给服务端使用:可能是JDK5以前与Parallel Scavenge收集器搭配使用,要么是CMS收集器发生失败时的备选预案
Parallel Old
Parallel Old时Parallel Scavenge收集器的老年代版本,支持多线程并行收集,基于标记-整理算法实现。直到JDK6才开始提供。
在注重吞吐量或者处理器资源比较稀缺的场合,都可以优先考虑Parallel Scavenge 加 Parallel Old组合
CMS
CMS(Concurrent Mark Sweep,并发-标记-清除)是目前最常用的 JVM 垃圾回收器
CMS 是一种基于并发、使用标记清除算法的垃圾回收器。CMS 会尽可能让 GC 线程与用户线程并发执行,可以消除长时间的 GC 停顿(STW)。
CMS 不会对新生代做垃圾回收,默认只针对老年代进行垃圾回收。此外,CMS 还可以开启对永久代的垃圾回收(或元空间),避免由于 PermGen 空间耗尽带来 Full GC,JDK6以上受参数 -XX:+CMSClassUnloadingEnabled 控制,这个参数在 JDK8 之前默认关闭,JDK8 默认开启了。
CMS 要与一个新生代垃圾回收器搭配使用,所谓"分代收集"。能与 CMS 配合工作的新生代回收器有 Serial 收集器和 ParNew 收集器,我们一般使用支持多线程执行的 ParNew 收集器。
使用 CMS GC 策略时,GC 类别可以分为:Young GC(又称 Minor GC),Old GC(又称 Major GC、CMS GC),以及Full GC。其中 Full GC 是对整个堆的垃圾回收,STW 时间较长,对业务影响较大,应该尽量避免 Full GC。
CMS工作过程
并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。 整个收集过程分为4步:
- 初始标记(CMS initial mark) STOP THE WORLD;初始标记仅仅只标记一下GC Roots能关联到的对象,速度很快。
- 并发标记(CMS concurrent mark);并发标记就是GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时长,但不需要停顿用户线程。
- 重新标记(CNS remark) STOP THE WORLD ,增量更新;重新标记就是为了修正并发标记期间,因用户程序继续运行而导致的标记产生变动的那一部分对象的标记记录。
- 并发清除(CMS concurrent sweep);并发清除就是清理删除掉标记阶段判断的依已经死亡的对象,由于不需要移动存货对象,所以这个阶段也不需要STW。
缺点:
- 占用一部分线程而导致应用程序变慢,降低总吞吐量(CMS默认启动的回收线程是 (处理器核心数量+3)/ 4
- CMS收集器无法处理“浮动垃圾”(Floating Garbage)。CMS收集器不能像其他收集器那样等待年老带几乎完全被填满了在进行收集,必须预留一部分空间供并发收集时的程序运作使用。CMS收集器当年老带使用68%后就会被激活(保守估计),还可能面临“并发失败”(Concurrent Mode Failure1)
- CMS是基于标记-清除算法的,这意味着收集结束时会有大量空间碎片产生
可以修改-XX:CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获得更好的性能,JDK6时,CMS收集器的启动阈值已经默认提升至92%。但但CMS运行期间如果内存无法满足程序分配新对象的需要,就会出现一次“并发失败”,这时虚拟机不得不冻结用户线程的执行,临时启用Serial Old收集器来重新进行年老带的垃圾收集,这样停顿时间就长了
Garbage First
G1收集器时垃圾收集器技术发展历史上的里程碑式的成果,它开创了垃圾收集器面向局部收集的设计思路和基于Region的内存布局形式
在JDK Update40时,G1提供并发的类卸载的支持。这个版本以后的G1才被Oracle官方称为“全能的垃圾收集器”(Fully-Featured Grabage Collector)
G1是一款主要面向服务端应用的垃圾收集器
在G1出现之前的其他所有垃圾收集器,包括CMS在内,垃圾收集的目标要么是整个新生代(Minor GC),要么就是整个年老带(Major GC),在要么就是整个Java堆(Full GC)。而G1面向堆内任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是那块内存中存放的垃圾数量最多,回收受益最大,这就是G1收集器的Mixed GC模式
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survior空间或者年老带空间。
Region还有一类特殊的Humongous区域,专门用于存储大对象,G1认为只要对象大小超过了一个Region容量的一半即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,去职位1MB-32MB,且应为2的N词幂。对于超过了整个Region容量的超级对象,将会被存放到N个连续的Humongous Region之中,G1的大多数行为都把Homongous Region作为年老带的一部分来进行看待
G1将Region作为单次回收的最小单元,即收集手机到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾回收。
G1的记忆集在存储结构的本质上就是一种Hash表,Key是别的Region的起始地址, Value是一个集合,里面存储的元素是卡表的索引号。(双向卡表的结构,卡表是“我指向谁”,这种结构还记录了“谁指向我”)
G1至少消耗大约相当于Java对容量的10%20%的额外内存来维护收集器工作。
并发标记阶段,G1采用原始快照(SATB)算法来实现,G1为每个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针以上。如果内存回收的速度赶不上内存分配的速度,就会导致Full GC而产生长时间的STW
G1运作过程大致可以划分为以下四个步骤:
- 初始标记(Initial Marking):仅仅只标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值。这个阶段需要停顿线程的(STW),但耗时很短。
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆中的对象图,找到需要回收的对象,这个阶段耗时较长,但可以与用户程序并罚执行。
- 最终标记(Final Marking):对用户线程做另一个短暂的停顿(STW),用户处理并发阶段结束后仍然遗留下来的最后那少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择多个Region构成回收集,然后决定把回收的Region内存活的对象复制到空的Region中,在清理掉整个旧Region的全部空间。这里的操作涉及到存活对象的移动,所以必须暂停用户空间,有多条收集器县城并发完成
G1收集器除了并发标记外,其余阶段也是要完成暂停用户线程的
CMS时用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样(其实是更复杂)的卡表维护外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发是的指针变化。
G1的写屏障的复杂操作要比CMS奥做更占用资源,所以CMS写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,写前屏障和邂逅屏障要做的事都要放到消息队列中,然后进行异步处理
Shenandoah
Shenandoah是第一款不由Oracle公司的虚拟机团队所领导开发的HotSpot垃圾收集器,不可避免的受到了“官方”的排挤,Sgebabdiag是一款只有在Open JDK中的垃圾收集器。
Shenandoah收集器的工作过程大致可以划分为以下九个阶段:
·初始标记(Initial M arking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍
是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。 ·并发标记(Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段
是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
- 最终标记(Final M arking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。
- 并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。
- 并发回收(Concurrent Evacuation):并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之 中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决。并发回收阶段运行的时间长短取决于回收集的大小。
- 初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未 做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收 集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的 停顿。
- 并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户 线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它 不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为 新值即可。
- 最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots 中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
- 并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已 再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收 这些Region的内存空间,供以后新对象分配使用。
ZGC
Z Garbage Collector是在JDK11中新加入的实现性质的低延迟垃圾收集器(ZGC在JDK11中不支持类卸载,JDK12支持类卸载)
ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
特征:ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器
ZGC的Region可以具有大、中、小三类容量:
- 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
- 大型Region(Large Region):容量不固定,可以动态变化,但必须为2M B的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型 Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实 现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到) 的,因为复制一个大对象的代价非常高昂。
并发实现算法:ZGC虽然也是用到了读屏障但实现的思路和Shenandoah完全不同
ZGC收集器有一个标志性的设计就是它采用染色指针技术(Colored Pointer,其他类似的技术中可能将它称为Tag Pointer或Version Pointer) ZGC是最直接的,、最纯粹的,它直接把标记信息记在引用对象的指针上,Linux下64位指针的高18位不能用来寻址,剩余的46位指针所,能支持的64TB内存也仍然够用,ZGC的染色指针盯上了这46位指针,将其高4位提取出来存储四个标志信息。通过这些标识位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集、是否只能通过finalize()方法才能被访问到。这也是直接导致了ZGC管理的内存不可以超过4TB
染色指针的三大优势
- 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。
- ·染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况。实际上,到目前为止ZGC都并未使用任何写屏障,只使用了读屏障(一部分是染色指针的功劳,一部分是ZGC现在还不支持分代收集,天然就没有跨代引用的问题)。
- 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。现在Linux下的64位指针还有前18位并未使用,它们虽然不能用来寻址,却可以通过其他手段用于信息记录。
ZGC运作过程分为四个阶段
- 并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记阶段是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相似的。与G1、Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针Marked0、Marked1标志位
- 并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件得出那些Region需要清理,哪些需要重分配集(Relocation Set)。此外JDK12的ZGC重开始支持类卸载以及弱引用的处理,也是在这个阶段完成的
- 并发重分配(Concurrent Relocate):重分配是ZGC的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从就对象到新对象的转向关系的转换关系。得益于染色指针的支持,ZGC收集器能从引用上得知一个对象是否处于重分配集中,如果一个线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,是其直接指向新对象,ZGC将这种行为称为指针的自愈(Self-Healing)能力。这样做的好处是只有一次访问就对象会陷入转发,也就是只慢一次,对比Shenandoah的Brooks转发指针,那是每次访问都要付出的固定开销(每次都慢),因此ZGC对用户程序的运行时负载要比Shenandoah更低
- 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中就对象的所有引用,这一点从目标角度看来是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务,因为即使是就引用,也是可以自愈的。
Epsilon
在JDK11中出现的Epsilon是一款不能够进行垃圾回收器的垃圾收集器
该垃圾回收器可以用于需要玻璃垃圾收集器影响的性能测试和压力测试
垃圾收集器的权衡
- 应用的主要关注点是什么:如果是数据分析、科学计算类的人物,目标是能尽快算出结果,那吞吐量就是一个主要关注点;如果是SLA应用,那停顿时间直接影响复制质量,严重时甚至导致服务超时,这样延迟就是主要关注点;如果是客户端应用或嵌入式应用那么内存占用则是不可忽视的。
- 运行应用的基础设施如何:比如硬件规格,设计到系统价格是x86-32/64、SPARC还是ARM/Aarch64;处理器的数量,内存大小;选择的操作系统是Liinux还是Saolaris还是Windows
- JDK的发行商是什么?版本号是多少?ZingJDK/Zulu、Oracle JDK、OpenJDK等
如果是windows系统则ZGC无法使用,可以试试Shenandoah
如果是4GB-6GB以下的堆内存,CMS一般能处理的比较好,对于更大的内存,可重点考察G1