前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >垃圾收集器

垃圾收集器

作者头像
胖虎
发布2020-11-24 10:16:48
3650
发布2020-11-24 10:16:48
举报
文章被收录于专栏:晏霖晏霖

点击上方“晏霖”,选择“置顶或者星标”

曾经有人关注了我

后来他有了女朋友

1.5垃圾收集器

HotSpot按照分代收集,所以在不同代上产生了多种不同的收集器,随着时间的推移,有些已经弃用,有些已经成为经典,还有目前广泛使用的,如图1-19所示就是7个经典的垃圾收集器,其中G1是目前应用最为广泛的,还有一些是JDK8以上支持的垃圾收集器,图中并未展示,后面小结中会提到。

图 1-19 HotSpot中经典垃圾收集器

1.5.1 Serial收集器

这个收集器是最基本的,也是历史最悠久的收集器,目前基本不会用了,想当年也是新生代的唯一选择,但是这么多年过去了,HotSpot也没有说过河拆桥把它废掉。

他是一个单线程收集器,他在工作的时候,必须暂停其他所有的工作线程,直到收集结束。这里要知道一个很严重的问题就是,暂停一切线程的结果就是当前运行在这个JDK的所有程序里的用户线程全部暂停,也就是说这一瞬间都是死掉的,用户看到的现象就是页面无任何响应,如果这种现象出现的时间长且频繁用户就崩溃了。图1-20所示Serial/Serial Old垃圾收集器运行过程。

图1-20 Serial/Serial Old垃圾收集器运行过程

这里埋下了一个伏笔,越优秀的收集器,他的停顿时间一定越短,这也是所有收集器共同追求的目标。

1.5.2 ParNew收集器

他是Serial收集器多线程版本,其所有控制参数、收集算法、对象分配规则、回收策略等都与Serial完全一样。下面是ParNew收集器工作的过程。他的重要之处在于,除了多线程提高了性能之外,他还可以与CMS收集器(下面介绍)搭配使用的原因。在单CPU环境下ParNew的性能没办法超过Serial,但是随着CPU数量增多他的优势就会越来越明显。如图1-21所示 ParNew收集器工作流程。

图1-21 ParNew/Serial Old工作流程

1.5.3 Parallel Scavenge收集器

他也是一款新生代收集器,使用的是复制算法,并且是并行对线程收集器。可以看到收集器的进步都是保留上一代之长,弥补上一代之短。很多收集器关注用户线程的停顿时间,但是Parallel Scavenge则关注吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),例:虚拟机运行100分钟,其中垃圾收集时间用了1分钟,那吞吐量就是99%。他是怎样控制吞吐量呢?使用参数控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis ,以及直接设置吞吐量大小 -XX:GCTimeRatio参数。MaxGCPauseMillis参数是一个大于0的毫秒数,收集器一次工作尽可能不超过设定的这个值,但是设置太小GC停顿时间缩短,造成了垃圾收集频率变快。如果你设定停顿100毫秒,10秒收集一次的频率,改成70毫秒的停顿时间,那么频率就可能变成5秒一次。停顿时间下降,吞吐量也会下降,GC还会变得更频繁。XX:GCTimeRatio参数设置垃圾收集时间占总时间的比率,0<n<100的整数;GCTimeRatio相当于设置吞吐量大小。垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n)。例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%--1/(1+19);默认值是1%--1/(1+99),即n=99。看来找准最优的临界点真的是Parallel Scavenge收集器比较配置的。不要担心,HotSpot又提供了一个参数 XX:+UseAdptiveSizePolicy帮助我们实现GC自适应的调节策略,他会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量。这个参数开启,JVM就可以动态分配新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;这也是Parallel Scavenge收集器优越于ParNew收集器一个重要点。

1.5.4 Serial Old收集器

Serial Old是 Serial收集器的老年代版本,也是继承Serial收集器单线程的特点。在JDK8时声明ParNew+Serial Old组合废弃,在JDK9中完全取消。

工作模型图在Serial收集器中展示了。

1.5.5 Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本,继承了Parallel New多线程对特点,在JDK1.6及之后用来代替老年代的Serial Old收集器。参数"-XX:+UseParallelOldGC":指定使用Parallel Old收集器。Parallel Scavenge和Parallel Old的组合可以说整个虚拟机都在为吞吐量优先而生的,无论新生代还是老年代均已吞吐量优先。

工作模型图在Parallel New收集器中展示了。

1.5.6 CMS收集器

在进入CMS收集器前我们要了解一个概念,当然在1.4.8章节中我有提到过一次,就是虚拟机中的并发与并行是什么关系。

n 并行(Parallel)

指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。如ParNew、Parallel Scavenge、Parallel Old;

n 并发(Concurrent)

指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行)。用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上。如CMS、G1(也有并行)。

CMS是基于标记-清除算法的,因为过程中有并发标记阶段因此可以叫做并发标记清理收集器,并发标记清理收集器也称为并发低停顿收集器或低延迟垃圾收集器。他的宗旨是:低停顿。CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器。

CMS的工作流程分为4个阶段,如图1-22所示:

n 初始标记 (Initial Mark)

停止一切用户线程,仅使用一条初始标记线程对所有与GC Roots直接相关联的 老年代对象进行标记,速度很快。

n 并发标记 (Concurrent Marking Phase)

使用多条并发标记线程并行执行,并与用户线程并发执行.此过程进行可达性分析,标记所有这些对象可达的存货对象,速度很慢。

n 重新标记 ( Remark)

因为并发标记时有用户线程在执行,标记结果可能有变化,停止一切用户线程,并使用多条重新标记线程并行执行,重新遍历所有在并发标记期间有变化的对象进行最后的标记.这个过程的运行时间介于初始标记和并发标记之间。

n 并发清除 (Concurrent Sweeping)

只使用一条并发清除线程,和用户线程们并发执行,清除刚才标记的对象,这个过程非常耗时。

图1-22 CMS收集器工作流程

他采用的是“标记-清除算法",因此会生大量的空间碎片。为了解决这个问题,CMS可以通过配置以下两种参数解决:

1. -XX:+UseCMSCompactAtFullCollection:参数(默认开启,在JDK9中移除),强制JVM在FGC完成后対老年代迸行圧縮,执行一次空间碎片整理,但是空间碎片整理阶段也会引发STW。为了减少STW次数,CMS还可以通过配置。

2. -XX:+CMSFullGCsBeforeCompaction=n :参数(此参数JDK9废弃),在执行了n次FGC后, JVM再在老年代执行空间碎片整理

1.5.7Garbage First收集器

G1(Garbage-First)是JDK7-u4才推出商用的收集器,在JDK9成为HotSpot默认的垃圾收集器,CMS从此退出历史舞台。他比CMS更高级了,他是并行与并发,能充分利用多CPU、多核环境下的硬件优势。G1和CMS的初衷是一样的,都希望成为低停顿时间模型的收集器,因G1的收集器内存分布模型的原因,使得在低停顿的角度上做的比CMS更好,他不在于限制把堆划为年轻代和老年代,而是把整个堆分为很多个区域,我们称这些区域为Region,其实G1并没有完全放弃分代收集的思想,而是根据需要的把Region变成一个Eden空间、Survivor空间或者老年代空间,除了可以把每个Region变成这三块区域,还可以变成Humongous区域,这个区域是专门用来存储大对象的,G1定义超过Region一般以上的对象成为大对象,也可以根据参数定义-XX:G1HeapRegionSize设定,值为2的N次幂,范围在1-32M。我们把所有这些G1被分成的区域成为回收集(Collection Set,简称CSet)。

因为G1把堆分成多个区域,因此在发生GC时,影响的范围就不会太多,他是要判断哪个区域垃圾数量多才会优先回收哪个Regioin的,这样的回收效益是最大的。因为各个区域都存在对象。为了避免全堆扫描,G1也是使用了记忆集的概念,包括为了解决对象之间的依赖,G1也是沿用卡表来处理跨代指针,但是实现起来都要比CMS复杂的多。

更好的理解G1内存模型,我们绘制了如图1-23所示的样子

图1-23 G1分区示意图

G1收集器运行的流程如下:

n 初始标记

标记与GC Roots直接关联的对象,停止所有用户线程,只启动一条初始标记线程,这个过程很快。

n 并发标记

进行全面的可达性分析,开启一条并发标记线程与用户线程并行执行,这个过程比较长,还要用原始快照的方式记录并发标记过程中发生引用变动的对象。

n 最终标记

标记出并发标记过程中用户线程新产生的垃圾,停止所有用户线程,并使用多条最终标记线程并行执行。

n 筛选回收

回收废弃的对象,根据分析各个Region回收的“性价比”进行排序,根据停顿时间指定回收计划,然后进行回收,最后将存活的对象复制到空的Region中,再清掉旧的Region,此过程也需要停止一切用户线程,并使用多条筛选回收线程并行执行。

结合上述阶段绘制G1收集器的运行示意图,如图1-24所示。

图1-24 G1收集器工作示意图

相对于CMS,G1有以下特点

l 并行与并发

能充分利用多CPU、多核环境下的硬件优势。

可以并行来缩短"Stop The World"停顿时间。

也可以并发让垃圾收集与用户程序同时进行。

l 分代收集,收集范围包括新生代和老年代

能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配。

能够采用不同方式处理不同时期的对象。

虽然保留分代概念,但Java堆的内存布局有很大差别。

将整个堆划分为多个大小相等的独立区域(Region)。

新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合。

l 结合多种垃圾收集算法,空间整合,不产生碎片

从整体看,是基于标记-整理算法。

从局部(两个Region间)看,是基于复制算法。

这是一种类似火车算法的实现。

都不会产生内存碎片,有利于长时间运行。

l 可预测的停顿:低停顿的同时实现高吞吐量

G1除了追求低停顿处,还能建立可预测的停顿时间模型。

可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。

如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒。

1.5.8 ZGC收集器

ZGC是一款在JDK 11中新加入的具有实验性质的低延迟垃圾收集器,是由Oracle公司研发的,承诺在数TB的堆上具有非常低的暂停时间。这款收集器的目标,对吞吐量影响不大对前提下,在任何大小的堆下都可以把垃圾收集的停顿时间限制在十毫秒内,降低对整体应用性能影响(吞吐量<15%)。

ZGC是在尽量不影响吞吐量的前提下缩小停顿时间,可见吞吐量可能是ZGC要适当取舍的方面,下面是Jfokus VM2018中Per liden的演讲PPT中的数据,如图1-25所示,ZGC、Parallel Scavenge、G1三款收集器的吞吐量对比,我们直观的看出ZGC的吞吐量并未为低停顿作出较大的牺牲,仅次于我们以吞吐量为首的Parallel Scavenge收集器。

图1-25 ZGC吞吐量测试数据

以下是停顿时间在其他两款收集器的对比,如图1-26所示,左侧是线性对比,右侧为对数对比。

图1-26 ZGC停顿时间对比图

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

n 着色指针

着色指针是一种将信息存储在指针(或使用Java术语引用)中的技术。因为在64位平台上(ZGC仅支持64位平台),指针可以处理更多的内存,因此可以使用一些位来存储状态。ZGC将限制最大支持4Tb堆(42-bits),那么会剩下22位可用,它目前使用了4位:finalizable,remap,mark0和mark1。我们稍后解释它们的用途。

着色指针的一个问题是,当您需要取消着色时,它需要额外的工作(因为需要屏蔽信息位)。 像SPARC这样的平台有内置硬件支持指针屏蔽所以不是问题,而对于x86平台来说,ZGC团队使用了简洁的多重映射技巧。

n 多重映射

要了解多重映射的工作原理,我们需要简要解释虚拟内存和物理内存之间的区别。 物理内存是系统可用的实际内存,通常是安装的DRAM芯片的容量。虚拟内存是抽象的,这意味着应用程序对(通常是隔离的)物理内存有自己的视图。操作系统负责维护虚拟内存和物理内存范围之间的映射,它通过使用页表和处理器的内存管理单元(MMU)和转换查找缓冲器(TLB)来实现这一点,后者转换应用程序请求的地址。

多重映射涉及将不同范围的虚拟内存映射到同一物理内存。 由于设计中只有一个remap,mark0和mark1在任何时间点都可以为1,因此可以使用三个映射来完成此操作。ZGC源代码中有一个很好的图表可以说明这一点。

n 读屏障

由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色了。那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。

ZGC目前只支持Linux/x64系统,不同的是ZGC具有动态类卸载(JDK12后支持),以及动态区域容量。我们可以对ZGC对Region分为三类容量:

小型:固定为2MB,存放小于256KB的小对象。

中型:固定为32MB,存放大于等于256KB小于4MB对象。

大型:可动态根据设置的堆进行合理的扩容,扩容必须是2的整数倍,用于存放大于等于4MB的对象。

图1-27 ZGC堆内存布局

ZGC的工作周期内可分为四个阶段,这四个阶段都是并发进行的,其中只有两个阶段中的小阶段会出现了短暂的停顿,例如GC Root直接关联对象Mark Start。ZGC的四个阶段分别是:并发标记、并发预备重分配、并发重分配、并发重映射。整个ZGC的周期图下图1-28所示,我截取了前一个周期的末尾和后一个周期的开头,这样大家可以更直观看出并发标记和并发重映射的关系。

图1-28 ZGC工作周期

ZGC最重要的调整选项是设置最大堆大小(-Xmx<size>)。由于ZGC是并发收集器,因此必须选择一个最大堆大小,以便:1)堆可以容纳应用程序的活动集,以及2)堆中有足够的净空以允许在GC处于运行状态时为分配提供服务运行。需要多少空间非常取决于分配率和应用程序的实时设置大小。通常,给ZGC的内存越多越好。但是同时,浪费内存是不可取的,因此,这全都在于在内存使用和GC需要运行的频率之间找到平衡。

设置并发GC线程数(-XX:ConcGCThreads=<number>)。ZGC具有启发式功能,可以自动选择此数字。这种启发式方法通常效果很好,但是根据应用程序的特性,可能需要对其进行调整。此选项从根本上决定了应该给GC多少CPU时间。给它太多,GC将占用应用程序太多的CPU时间。给它太少,应用程序分配垃圾的速度可能比GC收集垃圾的速度快。

由于使用ZGC会设置较大的堆内存,那么ZGC还可以将未使用的内存返还到操作系统。默认情况下,ZGC取消提交未使用的内存,将其返回给操作系统。这对于关注内存占用的应用程序和环境很有用。可以使用禁用此功能-XX:-ZUncommit。此外,不会取消分配内存,以使堆大小缩小到最小堆大小(-Xms)以下。这意味着,如果最小堆大小(-Xms)配置为等于最大堆大小(-Xmx),则将隐式禁用此功能。

可以使用以下命令配置未提交延迟-XX:ZUncommitDelay=<seconds>(默认为300秒)。此延迟指定在可以取消提交之前,应使用多长时间的内存。

注意,在Linux上,取消使用未使用的内存需要fallocate(2)(允许调用者直接操纵该文件的分配盘空间称为通过FD的字节范围起始于 偏移并持续LEN字节。从内核2.6.23开始,fallocate()在Linux上可用。从版本2.10开始,glibc提供了支持)获得FALLOC_FL_PUNCH_HOLE支持,该支持首先出现在内核版本3.5(对于tmpfs)和4.3版本(对于hugetlbfs)中。

将ZGC配置为使用大页面通常会产生更好的性能(在吞吐量,延迟和启动时间方面),并且没有真正的缺点,除了设置稍微复杂些。安装过程通常需要root特权,这就是默认情况下未启用它的原因。

在Linux / x86上,大页面(也称为“大页面”)的大小为2MB。

假设您想要16G Java堆。这意味着您需要16G / 2M = 8192个大页面。

首先,将至少16G(8192页)的内存分配给大页池。“至少”部分很重要,因为在JVM中启用大页面的使用意味着不仅GC将尝试将这些页面用于Java堆,而且JVM的其他部分还将尝试将它们用于各种操作。内部数据结构(代码堆,标记位图等)。因此,在此示例中,我们将保留9216页(18G),以允许2G的非Java堆分配使用大页。

使用显式大页面(如上所述)的替代方法是使用透明大页面。通常不建议对延迟敏感的应用程序使用透明的大页面,因为它会导致不必要的延迟峰值。但是,可能值得尝试一下,看看是否/如何影响工作量。但请注意,您的里程可能会有所不同。

注意!在Linux上,使用启用透明大页面的ZGC需要内核> = 4.7。

ZGC具有NUMA支持,这意味着它将尽最大努力将Java堆分配定向到NUMA本地内存。默认情况下启用此功能。但是,如果JVM检测到它绑定到系统中CPU的子集,它将自动被禁用。通常,您无需担心此设置,但是如果要显式覆盖JVM的决定,则可以使用-XX:+UseNUMA或-XX:-UseNUMA选项来实现。

在NUMA计算机(例如多插槽x86计算机)上运行时,启用NUMA支持通常可以显着提高性能。

1.5.9 Shenandoah收集器

这是在OpenJDK12中出现了一款新的垃圾收集器,而在OracleJDK12中是被排除掉的,可能这款收集器并非Oracle公司自研的,也可能是其他原因,总之他出现在“免费开源版”可供大家使用。

这款收集器的目标是在任何大小的堆下都可以把垃圾收集的停顿时间限制在十毫秒内,从出发点看,他和CMS、G1的目标是一致的,从低停顿角度看,Shenandoah比G1停顿时间更短,从实现角度看,他和G1有很多相似之处,甚至是在一些代码上。总结一下相比于G1的优点在于:

1. 与G1同样支持并发的整理算法,但Shenandoah的回收阶段可以和用户线程并发执行;

2. Shenandoah 目前不使用分代收集,也就是没有年轻代Region和老年代Region的概念在里面了;

3. Shenandoah 摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗。

讲到这里我们又接触到一个新的模型“连接矩阵”模型,Shenandoah 用连接矩阵模型解决跨表引用的记忆集。个人认为Shenandoah使用的连接矩阵更像是一个“有向图邻接矩阵”,因此以下我会用一种邻接矩阵来帮助大家理解。了解连接矩阵模型可以帮助我们更好的理解Shenandoah工作原理。

假设A、B、C、D为四块连续的Region,其中A对象引用了D对象,E对象引用了B对象,我们可以用如表1-1来表示。

表1-1 Shenandoah “连接矩阵”模型图

A

B

C

D

E

A

0

0

0

0

0

B

0

0

0

0

1

C

0

0

0

0

0

D

1

0

0

0

0

E

0

0

0

0

0

在工作流程方面Shenandoah可划分为九个阶段,而前三个阶段和G1是完全一致的,这里不在赘述。

n 初始标记:与G1一样。

n 并发标记:与G1一样。

n 最终标记:与G1一样。

n 并发清理:用于清理那些整个区域内连一个存活对象都没有找到的Region,与用户线程并发进行。

n 并发回收:这个阶段是Shenandoah收集器的核心阶段。在这个阶段,Shenandoah要把回收集里面的存活对象先复制到其他未被使用的Region中。此时与用户线程并发进行,难点也在于此,因为这时用户线程可能不停的对被移动的对象进行读写访问,导致这些被访问的对象引用发生变化。对于这个难点,Shenandoah将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决。此阶段运行时间取决于回收集的大小。

n 初始引用更新:这个阶段是把刚刚并发回收时发生引用改变的对象进行一个修正,这个操作称为引用更新。初始引用更新时间很短,但也会产生一个停顿。

n 并发引用更新:这是真正开始进行引用更新的操作,该阶段与用户线程一起并发的,更新时间取决于内存中涉及的引用数量。

n 最终引用更新:完成引用更新后修正GC Roots 中的引用。需要STW,也是Shenandoah的最后一次停顿,执行时间与GC Roots的数量有关。

n 并发清理:回收那些现在没有任何引用的Region集合。

1.5.10 Epsilon收集器

Epsilon(A No-Op Garbage Collector)垃圾回收器控制内存分配,但是不执行任何垃圾回收工作,只负责堆的管理与布局,对象的分配,与解释器、编译器、监控子系统的协作。一旦Java的堆被耗尽,JVM就直接关闭。设计的目的是提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低消费内存占用量和内存吞吐时的延迟时间。一个好的实现是隔离代码变化,不影响其他GC,最小限度的改变其他的JVM代码。

Epsilon适合运行时间短、在内存耗尽前就可退出的应用程序,它更适合性能调优和测试内存分配时使用。

胖虎

热 爱 生 活 的 人

终 将 被 生 活 热 爱

我在这里等你哟!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 晏霖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
应用性能监控
应用性能监控(Application Performance Management,APM)是一款应用性能管理平台,基于实时多语言应用探针全量采集技术,为您提供分布式性能分析和故障自检能力。APM 协助您在复杂的业务系统里快速定位性能问题,降低 MTTR(平均故障恢复时间),实时了解并追踪应用性能,提升用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档