垃圾回收器
参考:https://baijiahao.baidu.com/s?id=1673186429952651208&wfr=spider&for=pc
https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html
最常用的几种组合:
- Serial/Serial Old
- ParNew/Serial Old:与上边相比,只是比年轻代多了多线程垃圾回收而已
- ParNew/CMS:当下比较高效的组合
- Parallel Scavenge/Parallel Old:自动管理的组合
- G1:最先进的收集器,但是需要JDK1.7update14以上
Serial/SerialOld
可以看到新生代或老年代在进行垃圾回收时都会暂停所有的用户线程,图中的SafePoint表示线程能够安全暂停的时机,即JVM要进行垃圾回收时,不可能立马就停止所有的线程,那样是非常危险的,必须要确保线程处于安全点才能暂停它。这里先有这个概念,细节在下一篇进行阐述。
该组合可以通过-XX:+UseSerialGC参数开启。
ParNew
该收集器就是Serial的多线程版本,但在单核处理器环境中表现还不如Serial(涉及线程的切换)。它默认开启的收集线程数与处理器核心数量相同,在处理器核心非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
另外需要注意的是它是除了Serial之外唯一可以与CMS配合的垃圾收集器,在激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它,在JDK9以后ParNew成为了CMS的一部分。
Parallel Scavenge/ParallelOld
Parallel Scavenge与其它垃圾收集器不同,其它的是追求尽可能小的GC停顿时间,而它主要关注吞吐量,所谓吞吐量就是代码运行时间/(代码运行时间+垃圾回收时间)。比如虚拟机运行100分钟,垃圾回收耗时1分钟,那么吞吐量就是99%。但是这款收集器在JDK1.6之前比较尴尬,没有与之对应的并行的老年代收集器,只能采用SerialOld老年代收集器,使得表现比不上PareNew+CMS的组合。直到ParallelOld出现后,ParallelScavenge才能真正的展现它吞吐量的优势。
ParallelScavenge有以下几个重要的参数:
-XX:MaxGCPauseMillis:该参数的值是一个大于0的毫秒数,收集器尽量保证GC停顿时间不超过该值,但是不要天真的认为该值越小越好。该值设置的太小会导致每次GC的回收率降低,垃圾堆积,GC发生的越来越频繁。比如原先需要100ms收集500M空间,现在设置为50ms,那么可能就只能回收300M或者更小的垃圾。
-XX:GCTimeRatio:控制垃圾回收时间比率。比如允许最大垃圾回收时间占总时间的5%,那么需要将该值设置为19(公式是1/(1 + 19))。
-XX:+UseAdaptiveSizePolicy:这个参数激活后,就不再需要我们手动设定新生代各区(Eden、from、to)的比例(-XX:SurvivorRatio),晋升老年代对象的大小(-XX:PretenureSizeThreshold),虚拟机会监控运行时的状态,进行动态的调整,这种方式称为垃圾收集的自适应调节策略(GC Ergonomics)。
CMS
CMS(Concurrent Mark Sweep)是第一款并发垃圾收集器,并发是指垃圾收集可以和用户线程同时进行。同时它也是唯一采用标记清除算法对老年代进行回收的垃圾回收器。
从上面我们可以发现CMS的整个过程中只有初始标记和重新标记是需要暂停用户线程的,而初始标记只是标记与GCRoots直接关联的对象,所以耗时只和GCRoots的数量有关,非常快;重新标记的耗时会比初始标记略长,但也远远比并发标记用时短,所以CMS就是通过细分GC的阶段来降低GC的停顿时间。
你可能会好奇为什么需要重新标记并且暂停所有用户线程,因为在与用户线程并发执行的同时肯定会存在引用变动的情况,而要处理这个问题,都是必须要暂停用户线程的,关于引用变动的处理在下一篇会详细分析。
CMS的缺点:
-
CPU敏感:虽然并发标记和并发标记是和用户线程并发执行的,但是也因此占用了系统的资源,导致应用程序忽然变慢,降低吞吐量。CMS默认启动的线程数是(处理器核心数+3)/4,因此当核心数量大于等于4时,GC占用资源不超过25%,但核心数小于4时,就会占用大量系统资源。 -
大量的内存碎片:因为CMS是使用标记清除算法实现垃圾回收,所以会产生大量的内存碎片。为了避免这个问题,CMS采用了一个折中的办法,即提供一个-XX:+UseCMS-CompactAtFullCollection参数,该参数默认开启,控制CMS在进行FullGC的同时进行空间整理,但这样又会导致停顿时间加长,所以还提供了-XX:CMSFullGCsBefore-Compaction参数,控制CMS在进行了多少次不带整理的FullGC后进行一次带整理的FullGC,默认值是0,即每次FullGC都会整理,该参数JDK9后被废弃。 -
浮动垃圾:因为最终清除的过程也是和用户线程并发执行的,因此这个过程中必然会产生新的垃圾,这一部分垃圾需要预留空间来存放,等待下一次GC的时候再清理,因此会浪费一部分空间。在JDK5的默认配置下,当老年代使用空间超过68%时就会进行GC,到JDK6时,这个阈值就提高到了92%,另外也可以通过-XX:CMSInitiatingOccu-pancyFraction参数控制。但该值越高,那么并发清理过程中可使用的内存就越小,当放不下时,就会出现一次Concurrent Mode Failure,这时候虚拟机就会冻结线程并采用SerialOld进行垃圾回收,导致停顿时间变得更长。
Garbage First
G1是目前最前沿且可商用的垃圾收集器,另外还有ZGC等更为前沿的垃圾收集器还处于试验阶段。它与其它垃圾收集器不同的是,他将堆空间化整为零,将内存区域划分为多个大小相等的独立区域(Region),使得它可以回收堆中的任何一个区域,而不是像其它的垃圾收集器要么只能回收新生代,要么只能回收老年代。但不是说G1就没有新生代和老年代了,它的每个Region都可以根据需要扮演Eden、Survivor或老年代,垃圾收集器也会针对不同角色的Region采用不同的策略去处理。
每个Region的大小可以通过-XX:G1HeapRegionSize设定,取值范围为1M~32M,且必须为2的N次幂。超过单个Region一半容量的对象即为大对象,而对于超过整个Region的对象将会使用多个连续的Humongous空间存放,G1大多数情况下都把Humongous作为老年代一部分看待。
G1的运行过程如上,它也包含了以下4个步骤:
- 初始标记:STW,也是只标记GC Roots直接关联的对象,并修改TAMS的指针值(G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上,垃圾回收时也不会回收这部分空间),这个过程耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。
- 并发标记:可达性分析找出要回收的对象,在对象扫描完成后,由于是与用户线程并发执行的,所以存在引用变动的对象,这部分对象会由SATB算法来解决(原始快照,下一篇详细分析)。
- 最终标记:STW,处理并发阶段遗留的少量遗留的SATB记录。
- 筛选回收:根据用户设定的-XX:MaxGCPauseMillis最大GC停顿时间对Region进行排序,并回收价值最大的Region,尽量保证满足参数设定的值(该值效果和Parallel Scavenge部分讲解的是一样的)。这里的回收算法就是讲存活的对象复制到空的Region中,即G1局部Region之间采用的是复制算法,而整体上采用的是标记整理算法。G1适合上百G的堆空间回收,与CMS的权衡在6~8G之间,较大的堆内存才能凸显G1的优势,可以通过-XX:+UseG1GC参数开启。
补充:
G1跟踪各个region里面的垃圾堆积的价值(回收后所获得的空间大小以及回收所需时间长短的经验值),在后台维护一张优先列表,每次根据允许的收集时间,优先回收价值最大的region,这种思路:在指定的时间内,扫描部分最有价值的region(而不是扫描整个堆内存),并回收,做到尽可能的在有限的时间内获取尽可能高的收集效率。
优点:
- 停顿时间可以预测:我们指定时间,在指定时间内只回收部分价值最大的空间,而CMS需要扫描整个年老代,无法预测停顿时间
- 无内存碎片:垃圾回收后会整合空间,CMS采用"标记-清理"算法,存在内存碎片
- 筛选回收阶段:
- 由于只回收部分region,所以STW时间我们可控,所以不需要与用户线程并发争抢CPU资源,而CMS并发清理需要占据一部分的CPU,会降低吞吐量。
- 由于STW,所以不会产生"浮动垃圾"(即CMS在并发清理阶段产生的无法回收的垃圾)
缺点:
? 存在诸多STW的情况:
标记阶段停顿分析
- 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
- 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
- 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。
清理阶段停顿分析
- 清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。
复制阶段停顿分析
- 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。
!!!!G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。(为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。)**
ZGC
HotSpot的垃圾收集器,有几种不同的标记实现方案。
- 把标记直接记录在对象头上(Serial 收集器)。
- 把标记记录在于对象相互独立的数据结构上(G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息)。
- ZGC染色指针直接把标记信息记载引用对象的指针上。
? 为了实现,上述的并发转移,引入了染色指针技术,即第三种方案。
? 着色指针是一种将信息存储在指针中的技术。
当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。ZGC之所以设置三个虚拟地址空间,是因为它使用“空间换时间”思想,去降低GC停顿时间。“空间换时间”中的空间是虚拟空间,而不是真正的物理空间.
通过这些标志虚拟机就可以直接从指针中看到器引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(是否被移动过——Remapped)、是否只能通过finalize()方法才能被访问到(Finalizable)
- 初始化:ZGC初始化之后,整个内存空间的地址视图被设置为Remapped。程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动,此时进入标记阶段。
- 并发标记阶段:第一次进入标记阶段时视图为M0,如果对象被GC标记线程或者应用线程访问过,那么就将对象的地址视图从Remapped调整为M0。所以,在标记阶段结束之后,对象的地址要么是M0视图,要么是Remapped。如果对象的地址是M0视图,那么说明对象是活跃的;如果对象的地址是Remapped视图,说明对象是不活跃的。(详细的说,初始标记后是M0,并发标记为M1)
- 并发转移阶段:标记结束后就进入转移阶段,此时地址视图再次被设置为Remapped。如果对象被GC转移线程或者应用线程访问过,那么就将对象的地址视图从M0调整为Remapped。
|