JAVA垃圾回收器与垃圾回收算法

垃圾回收器

查看当前垃圾回收器类型命令

  1. java -XX:+PrintCommandLineFlags -version
  2. jps+jinfo:先使用jps查看java进程号,在使用jinfo查看该进程的配置

垃圾回收相关知识

评估垃圾回收器性能时,重点关注吞吐量和暂停时间。
吞吐量和暂停时间是相互矛盾的,目前我们追求的效果是:在最大吞吐量优先的情况下,减小暂停时间。

并行和并发概念补充:
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。单位时间内,多个任务同时执行.
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。一段时间内,多个任务都在执行。(单位时间内,只有一个任务在执行)

吞吐量(Thoughput) :运行用户代码时间/(运行用户代码时间+垃圾收集时间)
比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%。

自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。该模式下,年轻代大小、伊甸园区和幸存者区的比例、晋升老年代的对象年龄阈值都会自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。

浮动垃圾:并发标记阶段,用户线程并未停止,该阶段也会产生垃圾, 回收器无法对这些垃圾进行标记,只能留到下次GC时处理。

在64位系统中,理论可以访问的内存高达2的64次幂字节,实际上,在AMD64架构中只支持到52位(4PB)的地址总线和48位(256TB)的虚拟地址空间,所以目前64位的硬件实际能够支持的最大内存只有256TB。操作系统也会施加自己的约束,64位的Linux则分别支持47位(128TB)的进程虚拟地址空间和46位(64TB)的物理地址空间,64位的Windows系统只支持44位(16TB)的物理地址空间。

如何选择垃圾收集器

优先调整堆的大小让服务器自己来选择
如果内存小于100M,使用串行收集器
如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
如果允许停顿时间超过1秒,选择并行或者JVM自己选
如果响应时间最重要,并且不能超过1秒,使用并发收集器
4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

垃圾回收器变更历史

1999年JDK 1.3.1 发布第一款串行方式的Serial GC,ParNew垃圾回收器是Serial回收器的多线程版本;
2002年2月26,Parallel GC和Concurrent Mark Sweep GC(CMS)跟随JDK 1.4.2一起发布;
Parallel GC在JDK 1.6后称为HotSpot默认GC;
2012年,在JDK 1.7u4版本中,G1可用;
2017年,JDK 9中,G1成为默认垃圾回收器,CMS被标记为过时;
2018年3月,JDK 10中提升G1并行性;
2018年9月,JDK 11引入了Epsilon垃圾回收器,同时引入ZGC(实验版本);
2019年3月,JDK 12发布,增强G1,并引入Shenandoah GC(实验版本);
2019年9月,JDK 13发布,增强ZGC;
2020年3月,JDK 14发布,删除CMS,拓展ZGC在MAC和Windows上的应用。

回收器类型对比

类型 jvm开启参数 特性
Serial、Serial Old -XX:+UseSerialGC 开启后: Serial(Young区用) + Serial Old(Old区用)的收集器组合:表示新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理/压缩算法。
SerialOld是运行在Client默认的java虚拟机默认的年老代垃圾收集器。
Serial Old在Server模式下主要有两个用途:与新生代的Parallel Scavenge配合使用;作为老年代CMS回收器的后备垃圾收集方案。
Serial适用于运行在Client模式下的虚拟机或者内存不大(几十MB到一两百MB)的环境下,因为是串行的,有较长时间的STW,所以并不适用于要求快响应、交互较强的应用。
ParNew收集器 -XX:+UseParNewGC 开启后,会使用: ParNew(Young区用) + Serial Old(Old区用)的收集器组合,新生代使用并发复制算法,老年代采用单线程标记-整理算法。
Parallel、
Parallel Old回收器
-XX:+UseParallelGC或
-XX:+UseParallelOldGC
-XX:+UseParallelGC指定新生代使用Parallel Scavenge回收器;
-XX:+UseParallelOldGC指定老年代使用Parallel Old回收器,它们是成对存在的,开启一个另一个也会开启。JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能
新生代使用并发复制算法,老年代使用单线程标记-整理算法。
还可以通过-XX:ParallelGCThreads=设置并行回收器的线程数:
默认情况下,当CPU数量小于8个时,-XX:ParallelGCThreads=的值等于CPU数量;
当CPU数量大于8个,-XX:ParallelGCThreads=的值等于3+5*CPU_COUNT/8。
-XX:+UseAdaptiveSizePolicy开启Parallel Scavenge的自适应调节策略。
Parallel Scavenge为吞吐量优先,具有自适应调节策略的垃圾回收器。多用于在后台运算而不需要太多交互的任务
CMS(Concurrent Mark Sweep) -XX:+UseConcMarkSweepGC 优点:并发收集且并发清理;低延迟。
缺点:内存碎片,产生浮动垃圾,消耗CPU。
-XX:+UseConcMarkSweepGC,开启CMS GC,开启后作用于老年代,-XX:+UseParNewGC会自动打开;
CMS作为一款老年代的垃圾回收器,不能和新生代垃圾回收器Parallel Scavenge搭配使用,只能和ParNew或者Serial搭配使用。
-XX:CMSInitiatingOccupanyFraction=,设置堆内存使用率阈值,一旦达到这个阈值,CMS开始进行回收(JDK5及之前,默认值为68,JDK6及以上版本默认值为92%);
-XX:+UseCMSCompactAtFullCollection,指定在CMS回收完老年代后,对内存空间进行压缩处理,以避免碎片化问题;
-XX:CMSFullGCsBeforeCompaction,设置执行多少次CMS GC后,对内存空间进行压缩整理;
-XX:ParallelCMSThreads=,设置CMS的线程数。默认启动的线程数为(ParallelGCThreads+3)/4。我们知道,当CPU个数小于8时,ParallelGCThreads的默认值为CPU个数,所以对于一个8核CPU,默认启动的CMS线程数为3,换句话说只有62.5%的CPU资源用于处理用户线程。所以CMS不适合吞吐量要求高的场景。
G1收集器 -XX:+UseG1GC 面向服务端应用,响应优先。
优点:
1.并行与并发;
2.分代收集,可以采用不同的算法处理不同的对象;
3.空间整合,标记压缩算法意味着不会产生内存碎片;
4.可预测的停顿时间,能让使用者明确指定一个长度为M毫秒时间片段内,消耗在垃圾回收的时间不超过N毫秒(根据优先列表优先回收价值最大的region)。
缺点:
在小内存环境下和CMS相比没有优势,G1适合大的堆内存;在用户程序运行过程中,G1无论是为了垃圾回收产生的内存占用,还是程序运行时的额外执行负载都要比CMS高。
-XX:G1HeapRegionSize=,设置region的大小。值为2的幂,范围是1MB到32MB之间,目标是根据最小堆内存大小划分出约2048个区域。所以如果这个值设置为2MB,那么堆最小内存大约为4GB;
-XX:MaxGCPauseMillis=,设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值为200ms;
-XX:ParallelGCThread=,设置STW时GC线程数值,最多设置为8;
-XX:ConcGCThreads=,设置并发标记的线程数,推荐值为ParallelGCThread的1/4左右;
-XX:InitiatingHeapOccupancyPercent=,设置触发并发GC周期的Java堆占用率阈值,超过这个值就触发GC,默认值为45。
Shenandoah -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC Shenandoah stay G1以上的主要进展是在运行应用程序线程时完成更多的垃圾收集周期工作。G1只能在应用程序暂停(移动对象)时清空堆区域,Shenandoah可以在应用程序运行的同时重新定位对象。
为了实现并发重新定位,它使用了所谓的Brooks指针。指针是Shenandoah堆中的每个对象都有一个附加字段,它指向对象本身。Shenandoah这是因为,当它移动一个对象时,它还需要修复堆中引用该对象的所有对象。当Shenandoah将一个对象移动到一个新位置时,指针将保持在原来的位置,这会将引用转发到对象的新位置。引用对象时,应用程序将跟随指向新位置的前向指针。最后,需要清除带有前向指针的旧对象,但通过将清除操作与移动对象本身的步骤分离,Shenandoah更容易同时重新定位对象。
ZGC -XX:+UnlockExperimentalVMOptions -XX:+UseZGC ZGC允许Java应用程序继续运行,同时执行除线程堆栈扫描之外的所有其他垃圾收集操作。它可以从数百MB扩展到TB大小的Java堆,同时,始终保持非常低的暂停时间(通常在2毫秒以内)。适合于大型内存需求(例如,大数据)的应用程序。但是,对于需要可预测且极短暂停时间的较小堆,ZGC也是一个不错的选择。

G1回收器详解

G1适用于全堆,既可以在新生代使用和老年代使用。
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

  1. 第一步,开启G1垃圾收集器
  2. 第二步,设置堆的最大内存
  3. 第三步,设置最大的停顿时间
    G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件 下被触发。
    G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
    1、G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
    2、G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器——G1垃圾收集器。

G1是在2012年才在jdk1.7u4中可用。 oracle官方计划在jdk9中将G1变成默认的垃圾收集器以替代CMS。它是一 款面向服务端应用的收器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CMS

G1(Garbage First)回收器把堆内存分割成很多不相关的区域(region,物理上不连续),使用不同区域来表示伊甸园区,幸存者区和老年代。

G1会避免对整个Java堆进行垃圾收集,它会跟踪各个region里垃圾回收的价值大小(回收所获得的空间大小及所需时间的经验值),在后台维护一个优先列表,每次根据允许收集时间,优先回收价值最大的region。

region种类的说明:
E表示伊甸园区,S表示幸存者区、O表示老年代,空白表示未使用的内存区域;
一个region在同一时间内只能属于一种角色;
在G1中,如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。
这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对 象,就会对垃圾收集器造成负面影响。
为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。
如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。
为了能找到连续 的H区,有时候不得不启动Full GC。

G1回收垃圾过程主要分为以下几个步骤:
初始标记:仅仅是标记GC Roots能直接关联的对象,需要STW,但这个过程非常快;
并发标记:从GC Roots出发,对堆中对象进行可达性分析,找出存活对象,该阶段耗时较长,但是可与用户线程并发执行;
最终标记:主要修正在并发标记阶段因为用户线程继续运行而导致标记记录产生变动的那一部分对象的标记记录,需要STW;
筛选回收:将各个region分区的回收价值和成本进行排序,根据用户所期望的停顿时间制定回收计划。这阶段停顿用户线程,STW。

对于G1垃圾收集器优化建议
1,年轻代大小避免使用 -Xmn选项或 -XX:NewRatio等其他相关选项显式设置年轻代大小。 固定年轻代的大小会覆盖暂停时间目标。
2,暂停时间目标不要太过严苛 G1 GC 的吞吐量目标是 90% 的应用程序时间和 10%的垃圾回收时间。
评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示您愿意 承受更多的垃圾回收开销,而这会直接影响到吞吐量

Shenandoah回收器详解

Shenandoah垃圾回收器是RedHat公司发明的,非Oracle公司官方实现,不是Oracle的亲儿子,因此在一定程度上遭到了“排挤”,只在开源的OpenJDK12中开始出现,而在商业版的Oracle JDK12中则没有。

Shenandoah的目标是将垃圾回收的停顿时间控制在10ms以内,这意味着Shenandoah不仅需要在并发标记阶段实现并发,还需要在标记清除阶段实现并发。

与G1的异同点

Shenandoah与G1有很多相同点,都采用了基于Region的内存布局,在标记阶段均采用了并发标记。事实上,Shenandoah在代码实现上使用了很多G1的代码,因此Shenandoah有很多特点和G1是一样的。另外,Shenandoah在G1的基础上做了很多改变,至少存在下面3处改进。

在最终的回收阶段,采用的是并发整理,由于和用户线程并发执行,因此这一过程不会造成STW,这大大缩短了整个垃圾回收过程中系统暂停的时间。
默认情况下不使用分代收集,也就是Shenandoah不会专门设计新生代和老年代,因为Shenandoah认为对对象分代的优先级并不高,不是非常有必要实现。(至于不实现分代,对Shenandoah性能能带来什么好处,笔者也不是很清楚,猜测原因可能是,不进行分代可能在设计上更简单吧。毕竟Shenandoah是RedHat公司设计实现的,不是Oracle的官方团队,他们从零开始设计,工作量巨大)
采用“连接矩阵”代替记忆集。在G1以及其他经典垃圾回收器中均采用了记忆集来实现跨分区或者跨代引用的问题,每个Region中都维护了一个记忆集,浪费了很多内存,且导致系统负载也更重,「因此在Shenandoah中摒弃了这种实现方式,而是采用连接矩阵来解决跨分区引用的问题」。
连接矩阵可以理解为一个二维数组,当Region N的对象引用了Region M中的对象,那么就将二维数组array[N][m]设置一个标志位。

工作流程

Shenandoah的工作流程大致可以分为初始标记、并发标记、最终标记、并发清理、并发疏散、引用更新、并发清理这几个步骤,其中引用更新还可以细分为初始引用更新、并发引用更新、最终引用更新三个小步骤。

初始标记、并发标记、最终标记这三个步骤和G1一样。并发清理这一步和G1就不同了,G1中是多个GC线程并行清理,而Shenandoah中是并发清理。

GC(3) Pause Init Mark 0.771ms
GC(3) Concurrent marking 76480M->77212M(102400M) 633.213ms
GC(3) Pause Final Mark 1.821ms
GC(3) Concurrent cleanup 77224M->66592M(102400M) 3.112ms
GC(3) Concurrent evacuation 66592M->75640M(102400M) 405.312ms
GC(3) Pause Init Update Refs 0.084ms
GC(3) Concurrent update references 75700M->76424M(102400M) 354.341ms
GC(3) Pause Final Update Refs 0.409ms
GC(3) Concurrent cleanup 76244M->56620M(102400M) 12.242ms

  1. 初始标记(Init Mark)
    并发标记的初始化阶段,它为并发标记准备堆和应用线程,然后扫描root集合。这是整个GC生命周期第一次停顿,这个阶段主要工作是root集合扫描,所以停顿时间主要取决于root集合大小。

初始标记标记的是和GC Roots直接相关联的对象,会造成STW,停顿的时间长短与GC Roots的数量成正比。

  1. 并发标记(Concurrent Marking)
    贯穿整个堆,以root集合为起点,跟踪可达的所有对象。这个阶段和应用程序一起运行,即并发(concurrent)。这个阶段的持续时间主要取决于存活对象的数量,以及堆中对象图的结构。由于这个阶段,应用依然可以分配新的数据,所以在并发标记阶段,堆占用率会上升。

并发标记阶段是垃圾回收线程和用户线程并发执行,不会造成STW,这一步需要遍历整个对象图,耗时较长。

  1. 最终标记(Final Mark)
    清空所有待处理的标记/更新队列,重新扫描root集合,结束并发标记。这个阶段还会搞明白需要被清理(evacuated)的region(即垃圾收集集合),并且通常为下一阶段做准备。最终标记是整个GC周期的第二个停顿阶段,这个阶段的部分工作能在并发预清理阶段完成,这个阶段最耗时的还是清空队列和扫描root集合。

最终标记阶段是对并发标记阶段进行修正,处理那些因为用户线程同时运行导致引用关系改变的对象,这一步需要暂停用户线程,会造成STW,但暂停时间不会太长。

  1. 并发清理(Concurrent Cleanup)
    回收即时垃圾区域 -- 这些区域是指并发标记后,探测不到任何存活的对象。

GC线程和用户线程并发执行,不会造成STW。而且这一步清理仅仅只是清理一个存活对象都没有的Region(也就是说Region中的对象都是垃圾)

  1. 并发疏散(Concurrent Evacuation)
    从垃圾收集集合中拷贝存活的对到其他的Region中,这是有别于OpenJDK其他GC主要的不同点。这个阶段能再次和应用一起运行,所以应用依然可以继续分配内存,这个阶段持续时间主要取决于选中的垃圾收集集合大小(比如整个堆划分128个region,如果有16个region被选中,其耗时肯定超过8个region被选中)。

将回收集中,所有存活的对象复制到空闲的Region中,这一步也是并发执行,不会造成STW,执行时间的长短与回收集的大小以及存活对象的数量相关。并发回收是Shenandoah与其他垃圾回收器相比,最大的不同之处了。Shenandoah在回收时使用的是复制算法,而复制算法的特点是:在移动完存活对象后,还需要修改所有指向这些存活对象的引用指向,而这个过程很难一瞬间就改变过来。由于是并发执行,用户线程也在运行,当我们将存活对象移动到新的Region中时,如果引用指向还没有修改为最新的对象地址,那就可能导致程序出错。Shenandoah为了实现并发回收,采用了「Brooks Pointers」转发指针来解决该问题。

  1. 初始引用更新(Init Update Refs)
    初始化更新引用阶段,它除了确保所有GC线程和应用线程已经完成并发Evacuation阶段,以及为下一阶段GC做准备以外,其他什么都没有做。这是整个GC周期中,第三次停顿,也是时间最短的一次。

为了提供一个线程的集合点,确保所有的垃圾回收线程都完成了复制对象到新Region的任务。

  1. 并发引用更新(Concurrent Update References)
    再次遍历整个堆,更新那些在并发evacuation阶段被移动的对象的引用。这也是有别于OpenJDK其他GC主要的不同,这个阶段持续时间主要取决于堆中对象的数量,和对象图结构无关,因为这个过程是线性扫描堆。这个阶段是和应用一起并发运行的。

就是在「并发引用更新」阶段是真正的更新引用,该过程不需要遍历整个对象图,只需要按照内存的物理地址顺序,线性地搜索出引用类型,然后更新为新地址,这一步是和用户线程一起并发执行。

  1. 最终引用更新(Final Update Refs)
    通过再次更新现有的root集合完成更新引用阶段,它也会回收收集集合中的region,因为现在的堆已经没有对这些region中的对象的引用。这是整个GC周期最后一个阶段,它的持续时间主要取决于root集合的大小。

更新GC Roots中的指向旧地址的对象到新地址。

  1. 并发清理(Concurrent Cleanup)
    回收现在没有任何引用的Region集合。

将回收集中所有的Region清除,该过程和用户线程并发执行,不会产生STW。

从Shenandoah的工作流程来看,大部分阶段都是并发执行,仅有初始标记和最终标记会造成STW,并且这两个阶段停顿的时间都十分短暂,因此Shenandoah在进行垃圾回收时造成的系统延时非常低,确实是一款以低延时为目标的垃圾回收器。

ZGC收集器详解

ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

ZGC内存布局

ZGC的Region具有动态性——动态创建和销毁,以及动态的区域容量大小。
ZGC的Region可以具有大、中、小三类容量。
小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作「大型Region」,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段)的,因为复制一个大对象的代价非常高昂。

ZGC并发-整理算法的实现

染色指针

ZGC收集器采用了染色指针技术。染色指针是一种直接将少量额外的信息存储在指针上的技术。

ZGC染色指针直接把标记信息记载引用对象的指针上。
染色指针是一种直接将少量额外的信息存储在指针上的技术。
目前Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存仍然鞥呢够充分满足大型服务器的需要。鉴于此,ZGC将其高4位提取出来存储四个标志信息。
通过这些标志虚拟机就可以直接从指针中看到器引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(是否被移动过——Remapped)、是否只能通过finalize()方法才能被访问到(Finalizable)。由于这些标志位进一步压缩了原本只有46位的地址空寂,导致ZGC能够管理的内存不可以超过4TB。

每个对象有一个64位指针,这64位被分为:
18位:预留给以后使用;
1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过过finalize()才能访问;
1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的Region集合);
1位:Marked1标识;
1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC;
42位:对象的地址(所以它可以支持2^42=4T内存)

为什么有2个mark标记?

每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。
GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。
GC周期2:使用mark1, 则周期的mark标记10,所有引用都能被重新标记。
通过对配置ZGC后对象指针分析我们可知,对象指针必须是64位,那么ZGC无法支持32位操作系统,无法支持压缩指针(CompressedOops,压缩指针也是32位)。

染色指针的三大优势:

一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。
染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。
颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

ZGC的运作过程

分为四个大的阶段。四个阶段都是并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段

并发标记(Concurrent Mark):与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记(Mark Start)和最终标记(Mark End)也会STW,与G1不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。
并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,标记过程是针对全堆的,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。
ZGC指针的「自愈」(Self-Healing):ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中。如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象。
只有第一次访问旧对象会陷入转发,也就是只慢一次。由于染色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉,因为可能还有访问在使用这个转发表),哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都可以自愈。
并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在「自愈」功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。

ZGC存在的问题

ZGC最大的问题是浮动垃圾。ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。
ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。

解决方案

目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是一个治标不治本的方案。如果需要从根本上解决这个问题,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。

ZGC触发时机

ZGC目前有4中机制触发GC:
定时触发,默认为不使用,可通过ZCollectionInterval参数配置。
预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要时统计GC时间,为其他GC机制使用。
分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)。
主动触发,(默认开启,可通过ZProactive参数配置) 距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发。

垃圾回收算法

  1. 标记-清除算法(Mark-Sweep)
  2. 复制算法(Copying)
  3. 标记-整理算法(Mark-Compact)
  4. 分代收集算法

引用计数法

原理
假设有一个对象a,任何对对象A的引用,引用计数器都会加1,当引用失败时,对象A的引用计数器就-1,如果对象计数器的值为0,表示对象没有引用可以被回收了。

优缺点
优点:
实时性比较高,无需等到内存不够时才回收,运行时根据对象计数器的值为0时就可直接回收
应用在垃圾回收时不需要stw(stop the world)。如果申请内存空间不够,则立刻报错outofmemory 错误。
区域性,更新对象计数器时,只是影响到该对象,不会扫描全部对象
缺点:
每次对象被引用时都需要更新计数器。有一点时间开销
浪费cpu资源,技术内存够用,仍然在运行进行计数器的统计
无法解决循环依赖(重点)

标记-清除算法(Mark-Sweep)

分为两个阶段,标注和清除。
标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
优点:
解决了引用计数法中循环引用的问题,没有从root节点引用的对象都会被回收。
缺点:
效率问题,标记和清除都需要遍历所有对象,并且GC时,需要stw停止所有程序,对于交互性高的应用体验来说是很差的。
空间问题,会产生大量不连续的内存碎片,清理出来的内存不连贯,后续可能发生大对象不能找到可利用空间的问题。

复制算法(Copying)

按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。

使用例子:JVM中的年轻代内存空间
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor 区“To”是空的。
紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
默认情况下年龄到达15的对象会被移到老生代中。
年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对 象会被复制到“To”区域。
经过这次GC后,Eden区和From区已经被清空。
这个时候,“From”和“To”会交换他 们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前 的“To”。不管怎样,都会保证名为To的Survivor区域是空的。
GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象 移动到年老代中。

标记-整理/压缩算法(Mark-Compact)

标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
优缺点
优缺点同标记清除算法一致,解决了内存碎片化的问题,但是多了一步,对象移动内存位置的步骤,其效率也有一定影响。

分代收集算法(Generational Collection)

当前虚拟机的垃圾回收算法都才用分代收集算法,这种算法没有什么新思想,只是根据对象的存活周期将内存分为几块。
一般将java的内存分为新生代(Young Generation)和老生代(Tenured/Old Generation),所以我们可以根据内存区域的特点选择不同的垃圾回收算法。
新生代中每次收集都会有大量对象死去,所以我们可以使用标记-复制算法,只需要付出少量对象的复制成本就可以完成垃圾回收。
而老年代的对象存活几率是最高的,而且没有额外的空间对它进行分配,所以我们选择标记清除算法或标记整理算法

参考
垃圾回收器之ZGC
深入理解JVM(③)ZGC收集器
垃圾回收算法
jvm 垃圾回收(常见算法介绍)

posted @ 2022-12-22 12:31  原子切割员  阅读(161)  评论(0编辑  收藏  举报