写在开篇

本文主要介绍了JVM内存管理和垃圾收集器。

垃圾收集概念

我们把还被GC Roots引用的对象称为活的,把不再被引用的对象认为是死的,也就是我们说的垃圾,GC的工作就是找到死的对象,回收它们占用的空间。

我们把GC管理的内存称为堆(heap),垃圾收集启动的时机取决于各个垃圾收集器,通常,垃圾收集发生于整个堆或堆的部分已经被使用光了,或者使用的空间达到了某个百分比阈值。

对于内存分配请求,实现的难点在于在堆中找到一块没有被使用的确定大小的内存空间。所以,对于大部分垃圾回收算法来说避免内存碎片化是非常重要的,它将使得空间分配更加高效。

垃圾收集器的理想特征

  • 安全和全面:活的对象一定不能被清理掉,死的对象一定不能在几个回收周期结束后还在内存中。
  • 高效:不能将我们的应用程序挂起太长时间。我们需要在时间、空间、频次上作出权衡。比如,如果堆内存很小,每次垃圾收集就会很快,但是频次会增加。如果堆内存很大,很久才会被填满,但是每一次回收需要的时间很长。
  • 尽量少的内存碎片:每次将垃圾对象释放以后,这些空间可能分布在各个地方,最糟糕的情况就是,内存中到处都是碎片,在给一个大对象分配空间的时候没有内存可用,实际上内存是够的。消除碎片的方式就是压缩。
  • 可扩展性:在多核多线程应用中,内存分配和垃圾回收都不应该成为可扩展性的瓶颈。原文提到的这一点,我的理解是:单线程垃圾回收在多核系统中会浪费CPU资源,如果我理解错误,请指正我。

设计上的权衡

往下看之前,我们需要先分清楚这里的两个概念:并发和并行

  • 并行:多个垃圾回收线程同时工作,而不是只有一个垃圾回收线程在工作
  • 并发:垃圾回收线程和应用程序线程同时工作,应用程序不需要挂起

在设计或选择垃圾回收算法的时候,我们需要作出以下几个权衡:

  • 串行 vs 并行
    串行收集的情况,即使是多核 CPU,也只有一个核心参与收集。使用并行收集器的话,垃圾收集的工作将分配给多个线程在不同的 CPU 上同时进行。并行可以让收集工作更快,缺点是带来的复杂性和内存碎片问题。
  • 并发 vs Stop-the-world
    当 stop-the-world 垃圾收集器工作的时候,应用将完全被挂起。与之相对的,并发收集器在大部分工作中都是并发进行的,也许会有少量的 stop-the-world。
    stop-the-world 垃圾收集器比并发收集器简单很多,因为应用挂起后堆空间不再发生变化,它的缺点是在某些场景下挂起的时间我们是不能接受的(如 web 应用)。
    相应的,并发收集器能够降低挂起时间,但是也更加复杂,因为在收集的过程中,也会有新的垃圾产生,同时,需要有额外的空间用于在垃圾收集过程中应用程序的继续使用。
  • 压缩 vs 不压缩 vs 复制
    当垃圾收集器标记出内存中哪些是活的,哪些是垃圾对象后,收集器可以进行压缩,将所有活的对象移到一起,这样新的内存分配就可以在剩余的空间中进行了。经过压缩后,分配新对象的内存空间是非常简单快速的。
    相对的,不压缩的收集器只会就地释放空间,不会移动存活对象。优点就是快速完成垃圾收集,缺点就是潜在的碎片问题。通常,这种情况下,分配对象空间会比较慢比较复杂,比如为新的一个大对象找到合适的空间。
    还有一个选择就是复制收集器,将活的对象复制到另一块空间中,优点就是原空间被清空了,这样后续分配对象空间非常迅速,缺点就是需要进行复制操作和占用额外的空间。

性能指标

以下几个是评估垃圾收集器性能的一些指标:

  • 吞吐量:应用程序的执行时间占总时间的百分比,当然是越高越好
  • 垃圾收集开销:垃圾收集时间占总时间的百分比(1 - 吞吐量)
  • 停顿时间:垃圾收集过程中导致的应用程序挂起时间
  • 频次:相对于应用程序来说,垃圾收集的频次
  • 空间:垃圾收集占用的内存
  • 及时性:一个对象从成为垃圾到该对象空间再次可用的时间

在交互式程序中,通常希望是低延时的,而对于非交互式程序,总运行时间比较重要。实时应用程序既要求每次停顿时间足够短,也要求总的花费在收集的时间足够短。在小型个人计算机和嵌入式系统中,则希望占用更小的空间。

分代收集介绍

当我们使用分代垃圾收集器时,内存将被分为不同的代(generation),最常见的就是分为年轻代和老年代。

在不同的分代中,可以根据不同的特点使用不同的算法。分代垃圾收集基于 weak generational hypothesis 假设(通常国人会翻译成 弱分代假设):

  • 大部分对象都是短命的,它们在年轻的时候就会死去
  • 极少老年对象对年轻对象的引用

年轻代中的收集是非常频繁的、高效的、快速的,因为年轻代空间中,通常都是小对象,同时有非常多的不再被引用的对象。

那些经历过多次年轻代垃圾收集还存活的对象会晋升到老年代中,老年代的空间更大,而且占用空间增长比较慢。这样,老年代的垃圾收集是不频繁的,但是进行一次垃圾收集需要的时间更长。

对于新生代,需要选择速度比较快的垃圾回收算法,因为新生代的垃圾回收是频繁的。

对于老年代,需要考虑的是空间,因为老年代占用了大部分堆内存,而且针对该部分的垃圾回收算法,需要考虑到这个区域的垃圾密度比较低。

JVM中的垃圾收集器

HotSpot分代

在 HotSpot 虚拟机中,内存被组织成三个分代:年轻代、老年代、永久代。

  • 大部分对象初始化的时候都是在年轻代中的
  • 老年代存放经过了几次年轻代垃圾收集依然还活着的对象,还有部分大对象因为比较大所以分配的时候直接在老年代分配
  • 永久代,通常也叫 方法区,用于存储已加载类的元数据,以及存储运行时常量池等

垃圾回收类型

当年轻代被填满后,会进行一次年轻代垃圾收集(也叫做minor GC)。

当老年代或永久代被填满了,会触发full GC(也叫做 major GC),full GC 会收集所有区域,先进行年轻代的收集,使用年轻代专用的垃圾回收算法,然后使用老年代的垃圾回收算法回收老年代和永久代。如果算法带有压缩,每个代分别独立地进行压缩。

如果先进行年轻代垃圾收集,会使得老年代不能容纳要晋升上来的对象,这种情况下,不会先进行young gc,所有的收集器都会(除了CMS)直接采用老年代收集算法对整个堆进行收集(CMS收集器比较特殊,因为它不能收集年轻代的垃圾)。

内存分配

指针碰撞

如果垃圾收集完成后,存在大片连续的内存可用于分配给新对象,这种情况下分配空间是非常简单快速的,只要一个简单的指针碰撞就可以了(bump-the-pointer),每次分配对象空间只要检测一下是否有足够的空间,如果有,指针往前移动N位就分配好空间了,然后就可以初始化这个对象了。

TLABs

对于多线程应用,对象分配必须要保证线程安全性,如果使用全局锁,那么分配空间将成为瓶颈并降低程序性能。HotSpot使用了称之为Thread-Local Allocation Buffers(TLABs) 的技术,该技术能改善多线程空间分配的吞吐量。首先,给予每个线程一部分内存作为缓存区,每个线程都在自己的缓存区中进行指针碰撞,这样就不用获取全局锁了。只有当一个线程使用完了它的TLAB,它才需要使用同步来获取一个新的缓冲区。HotSpot使用了多项技术来降低TLAB对于内存的浪费。比如,TLAB的平均大小被限制在Eden区大小的1%之内。TLABs和使用指针碰撞的线性分配结合,使得内存分配非常简单高效,只需要大概10条机器指令就可以完成。

串行收集器

使用串行收集器,年轻代和老年代都使用单线程进行收集(使用一个CPU),收集过程中会stop-the-world。所以当在垃圾收集的时候,应用程序是完全停止的。

在年轻代中使用串行收集器

下图展示了年轻代中使用串行收集器的流程。

jvm内存管理01
jvm内存管理01

年轻代分为一个Eden区和两个Survivor区(From区和To区)。年轻代垃圾收集时,将Eden中活着的对象复制到空的Survivor-To区,Survivor-From区的对象分两类,一类是年轻的,也是复制到Survivor-To区,还有一类是老家伙,晋升到老年代中。

如果复制的过程中,发现Survivor-To空间满了,将剩下还没复制到Survivor-To的来自于Eden和Survivor-From区的对象直接晋升到老年代。

年轻代垃圾收集完成后,Eden区和Survivor-From就干净了,此时,将Survivor-From和 Survivor-To交换一下角色。得到下面这个样子:

jvm内存管理02
jvm内存管理02

在老年代中使用串行收集器

如果使用串行收集器,在老年代和永久代将通过使用 标记 -> 清除 -> 压缩算法。标记阶段,收集器识别出哪些对象是活的;清除阶段将遍历一下老年代和永久代,识别出哪些是垃圾;然后执行压缩,将活的对象左移到老年代的起始端(永久代类似),这样就留下了右边一片连续可用的空间,后续就可以通过指针碰撞的方式快速分配对象空间。

jvm内存管理03
jvm内存管理03

何时应该使用串行收集器

串行收集器适用于运行在client模式下的大部分程序,它们不要求低延时。在现代硬件条件下,串行收集器可以高效管理64M堆内存,并且能将full GC控制在半秒内完成。

并行收集器

现在大多数Java应用都运行在大内存、多核环境中,并行收集器,也就是大家熟知的吞吐量收集器,利用多核的优势来进行垃圾收集,而不是像串行收集器一样将程序挂起后只使用单线程来收集垃圾。

在年轻代中使用并行收集器

并行收集器在年轻代中其实就是串行收集器收集算法的并行版本。它仍然使用 stop-the-world 和复制算法,只不过使用了多核的优势并行执行,降低垃圾收集的时间,从而提高吞吐量。下图示意了在年轻代中,串行收集器和并行收集器的区别:

jvm内存管理04
jvm内存管理04

在老年代中使用并行收集器

在老年代中,并行收集器使用的是和串行收集器一样的算法:单线程,标记 -> 清除 -> 压缩。

ps:是的,并行收集器只能在年轻代中并行。

何时使用并行收集器

其适用于多核、不要求低停顿的应用,因为老年代的收集虽然不频繁,但是每次老年代的单线程垃圾收集依然可能会需要很长时间。比如说,它可以应用在批处理、账单计算、科学计算等。

你应该不会想要这个收集器,而是要一个可以对每个代都采用并行收集的并行压缩收集器,下一节将介绍这个。

并行压缩收集器

并行压缩收集器于J2SE 5.0 update 6引入,和并行收集器的区别在于它在老年代也使用并行收集算法。注意:并行压缩收集器终将会取代并行收集器。

在年轻代中使用并行压缩收集器

并行压缩收集器在年轻代中使用了和并行收集器一样的算法。即使用 并行、stop-the-world、复制 算法。

在老年代中使用并行压缩收集器

在老年代和永久代中,其使用 并行、stop-the-world、滑动压缩 算法。

一次收集分三个阶段,首先,将老年代或永久代逻辑上分为固定大小的区块。

  • 标记阶段,将GC Roots分给多个垃圾收集线程,每个线程并行地去标记存活的对象,一旦标记一个存活对象,在该对象所在的区块记录这个对象的大小和对象所在的位置
  • 汇总阶段,此阶段针对区块进行。由于之前的垃圾回收影响,老年代和永久代的左侧是 存活对象密集区,对这部分区域直接进行压缩的代价是不值得的,能清理出来的空间有限。所以第一件事就是,检查每个区块的密度,从左边第一个开始,直到找到一个区块满足:对右侧的所有区块进行压缩获得的空间抵得上压缩它们的成本。这个区块左边的区域过于密集,不会有对象移动到这个区域中。然后,计算并保存右侧区域中每个区块被压缩后的新位置首字节地址
    右侧的区域将被压缩,对于右侧的每个区块,由于每个区块中保存了该区块的存活对象信息,所以很容易计算每个区块的新位置。注意:汇总阶段目前被实现为串行进行,这个阶段修改为并行也是可行的,不过没有在标记阶段和下面的压缩阶段并行那么重要
  • 压缩阶段,在汇总阶段已经完成了每个区块新位置的计算,所以压缩阶段每个回收线程并行将每个区块复制到新位置即可。压缩结束后,就清出来了右侧一大片连续可用的空间

何时使用并行压缩收集器

首先是多核上的并行优势,这个就不重复了。其次,前面的并行收集器对于老年代和永久代使用串行,而并行压缩收集器在这些区域使用并行,能降低停顿时间。

并行压缩收集器不适合运行在大型共享主机上(如SunRays),因为它在收集的时候会独占几个CPU,在这种机器上,可以考虑减少垃圾收集的线程数(通过 –XX:ParallelGCThreads=n),或者就选择其他收集器。

Concurrent Mark-Sweep(CMS)收集器

重头戏CMS登场了,至少对于我这个web开发者来说,目前CMS最常用(使用JDK8的应用一般都切换到G1收集器了)。前面介绍的都是并行收集,这里要介绍并发收集了,也就是垃圾回收线程和应用程序线程同时运行。

对于许多程序来说,吞吐量不如响应时间来得重要。通常年轻代的垃圾收集不会停顿多长时间,但是,老年代垃圾回收,虽然不频繁,但是可能导致长时间的停顿,尤其当堆内存比较大的时候。为了解决这个问题,HotSpot虚拟机提供了CMS收集器,也叫做低延时收集器

在年轻代中使用CMS收集器

在年轻代中,CMS和并行收集器一样,即:并行、stop-the-world、复制。

在老年代中使用 CMS 收集器

在老年代的垃圾收集过程中,大部分收集任务是和应用程序并发执行的。

CMS收集过程首先是一段小停顿stop-the-world,叫做初始标记阶段(initial mark),用于确定GC Roots。然后是并发标记阶段(concurrent mark),标记GC Roots可达的所有存活对象,由于这个阶段应用程序同时也在运行,所以并发标记阶段结束后,并不能标记出所有的存活对象。为了解决这个问题,需要再次停顿应用程序,称为再次标记阶段(remark),遍历在并发标记阶段应用程序修改的对象(标记出应用程序在这个期间的活对象),由于这次停顿比初始标记要长得多,所以会使用多线程并行执行来增加效率

再次标记阶段结束后,能保证所有存活对象都被标记完成,所以接下来的并发清理阶段(concurrent sweep) 将就地回收垃圾对象所占空间。下图示意了老年代中串行、标记 -> 清理 -> 压缩收集器CMS收集器的区别:

jvm内存管理05
jvm内存管理05

由于部分任务增加了收集器的工作,如遍历并发阶段应用程序修改的对象,所以增加了CMS收集器的负载。对于大部分试图降低停顿时间的收集器来说,这是一种权衡方案。

CMS收集器是唯一不进行压缩的收集器,在它释放了垃圾对象占用的空间后,它不会移动存活对象到一边去。

jvm内存管理06
jvm内存管理06

这将节省垃圾回收的时间,但是由于之后空闲空间不是连续的,所以也就不能使用简单的指针碰撞进行对象空间分配了。它需要维护一个空闲列表,将所有的空闲区域连接起来,当分配空间时,需要寻找到一个可以容纳该对象的区域。显然,它比使用简单的指针碰撞成本要高。同时它也会加大年轻代垃圾收集的负载,因为年轻代中的对象如果要晋升到老年代中,需要老年代进行空间分配。

另外一个缺点就是,CMS收集器相比其他收集器需要使用更大的堆内存。因为在并发标记阶段,程序还需要执行,所以需要留足够的空间给应用程序。另外,虽然收集器能保证在标记阶段识别出所有的存活对象,但是由于应用程序并发运行,所以刚刚标记的存活对象很可能立马成为垃圾,而且这部分由于已经被标记为存活对象,所以只能到下次老年代收集才会被清理,这部分垃圾称为浮动垃圾

最后,由于缺少压缩环节,堆将会出现碎片化问题。为了解决这个问题,CMS 收集器需要追踪统计最常用的对象大小,评估将来的分配需求,可能还需要分割或合并空闲区域。

不像其他垃圾收集器,CMS收集器不能等到老年代满了才开始收集。否则的话,CMS收集器将退化到使用更加耗时的stop-the-world、标记-清除-压缩算法。为了避免这个,CMS收集器需要统计之前每次垃圾收集的时间和老年代空间被消耗的速度。另外,如果老年代空间被消耗了预设占用率(initiating occupancy),也将会触发一次垃圾收集,这个占用率通过 –XX:CMSInitiatingOccupancyFraction=n 进行设置,n为老年代空间的占用百分比,默认值是68。

总结下来,和并行收集器相比,CMS收集器降低了老年代收集时的停顿时间(有时是显著降低),稍微增加了一些年轻代收集的时间、降低了吞吐量以及需要更多的堆内存

何时使用CMS收集器

适用于应用程序要求低停顿,同时能接受在垃圾收集阶段和垃圾收集线程一起共享CPU资源的场景,典型的就是web应用了。

在web应用中,低延时非常重要,所以CMS几乎就是唯一选择,直到后来G1的出现。

G1

G1的主要关注点在于达到可控的停顿时间,在这个基础上尽可能提高吞吐量,这一点非常重要。

G1被设计用来长期取代CMS收集器,和CMS相同的地方在于,它们都属于并发收集器,在大部分的收集阶段都不需要挂起应用程序。区别在于,G1没有CMS的碎片化问题(或者说不那么严重),同时提供了更加可控的停顿时间。

如果你的应用使用了较大的堆(如6GB及以上)而且还要求有较低的垃圾收集停顿时间(如0.5秒),那么G1是你绝佳的选择,是时候放弃CMS了。

G1总览

首先是内存划分上,之前介绍的分代收集器将整个堆分为年轻代、老年代和永久代,每个代的空间是确定的。

而G1将整个堆划分为一个个大小相等的小块(每一块称为一个region),每一块的内存是连续的。和分代算法一样,G1中每个块也会充当Eden、Survivor、Old三种角色,但是它们不是固定的,这使得内存使用更加地灵活。

JVM内存管理07
JVM内存管理07

执行垃圾收集时,和CMS一样,G1收集线程在标记阶段和应用程序线程并发执行,标记结束后,G1也就知道哪些区块基本上是垃圾,存活对象极少,G1会先从这些区块下手,因为从这些区块能很快释放得到很大的可用空间,这也是为什么G1被取名为Garbage-First的原因

在G1中,目标停顿时间非常非常重要,用-XX:MaxGCPauseMillis=200指定期望的停顿时间。

G1使用了停顿预测模型来满足用户指定的停顿时间目标,并基于目标来选择进行垃圾回收的区块数量。G1采用增量回收的方式,每次回收一些区块,而不是整堆回收。

我们要知道G1不是一个实时收集器,它会尽力满足我们的停顿时间要求,但也不是绝对的,它基于之前垃圾收集的数据统计,估计出在用户指定的停顿时间内能收集多少个区块。

注意:G1有和应用程序一起运行的并发阶段,也有stop-the-world的并行阶段。但是,Full GC的时候还是单线程运行的,所以我们应该尽量避免发生Full GC,后面我们也会介绍什么时候会触发Full GC。

G1内存占用

G1比ParallelOld和CMS会需要更多的内存消耗,那是因为有部分内存消耗于簿记(accounting)上,如以下两个数据结构:

  • Remembered Sets:每个区块都有一个RSet,用于记录进入该区块的对象引用(如区块 A 中的对象引用了区块B,区块B的Rset需要记录这个信息),它用于实现收集过程的并行化以及使得区块能进行独立收集。总体上Remembered Sets消耗的内存小于5%
  • Collection Sets:将要被回收的区块集合。GC时,在这些区块中的对象会被复制到其他区块中,总体上Collection Sets消耗的内存小于1%

G1工作流程

G1收集器主要包括了以下4种操作:

  1. 年轻代收集
  2. 并发收集,和应用线程同时执行
  3. 混合式垃圾收集
  4. 必要时的 Full GC
年轻代收集

首先,我们来看下 G1 的堆结构:

jvm内存管理08
jvm内存管理08

年轻代中的垃圾收集流程(Young GC):

jvm内存管理09
jvm内存管理09

我们可以看到,年轻代收集概念上和之前介绍的其他分代收集器大差不差的,但是它的年轻代会动态调整。

Old GC / 并发标记周期

接下来是Old GC的流程(含Young GC阶段),其实把Old GC理解为并发周期是比较合理的,不要单纯地认为是清理老年代的区块,因为这一步和年轻代收集也是相关的。下面我们介绍主要流程:

  • 初始标记:stop-the-world,它伴随着一次普通的Young GC发生,然后对Survivor区(root region)进行标记,因为该区可能存在对老年代的引用
    因为Young GC是需要stop-the-world的,所以并发周期直接重用这个阶段,虽然会增加CPU开销,但是停顿时间只是增加了一小部分
  • 扫描根引用区:因为先进行了一次YGC,所以当前年轻代只有Survivor区有存活对象,它被称为根引用区。扫描Survivor到老年代的引用,该阶段必须在下一次Young GC发生前结束
    这个阶段不能发生年轻代收集,如果中途Eden区真的满了,也要等待这个阶段结束才能进行Young GC
  • 并发标记:寻找整个堆的存活对象,该阶段可以被Young GC中断
    这个阶段是并发执行的,中间可以发生多次Young GC,Young GC会中断标记过程
  • 重新标记:stop-the-world,完成最后的存活对象标记。使用了比CMS收集器更加高效的snapshot-at-the-beginning(SATB)算法
    Oracle的资料显示,这个阶段会回收完全空闲的区块
  • 清理:清理阶段真正回收的内存很少

到这里,G1的一个并发周期就算结束了,其实就是主要完成了垃圾定位的工作,定位出了哪些分区是垃圾最多的。因为整堆一般比较大,所以这个周期应该会比较长,中间可能会被多次stop-the-world的Young GC打断。

混合垃圾回收周期

并发周期结束后是混合垃圾回收周期,不仅进行年轻代垃圾收集,而且回收之前标记出来的老年代的垃圾最多的部分区块。

混合垃圾回收周期会持续进行,直到几乎所有的被标记出来的分区(垃圾占比大的分区)都得到回收,然后恢复到常规的年轻代垃圾收集,最终再次启动并发周期。

Full GC

到这里我们已经说了年轻代收集、并发周期、混合回收周期了,大家要熟悉这几个阶段的工作。

下面我们来介绍特殊情况,那就是会导致Full GC的情况,也是我们需要极力避免的:

  • concurrent mode failure:并发模式失败,CMS 收集器也有同样的概念。G1并发标记期间,如果在标记结束前,老年代被填满,G1会放弃标记
    这个时候说明堆需要增加了,或者需要调整并发周期,如增加并发标记的线程数量,让并发标记尽快结束,或者就是更早地进行并发周期,默认是整堆内存的45% 被占用就开始进行并发周期
  • 晋升失败:并发周期结束后,是混合垃圾回收周期,伴随着年轻代垃圾收集,进行清理老年代空间,如果这个时候清理的速度小于消耗的速度,导致老年代不够用,那么会发生晋升失败
    说明混合垃圾回收需要更迅速完成垃圾收集,也就是说在混合回收阶段,每次年轻代的收集应该处理更多的老年代已标记区块
  • 疏散失败:年轻代垃圾收集的时候,如果 Survivor 和 Old 区没有足够的空间容纳所有的存活对象。这种情况肯定是非常致命的,因为基本上已经没有多少空间可以用了,这个时候会触发 Full GC 也是很合理的
    最简单的就是增加堆大小
  • 大对象分配失败,我们应该尽可能地不创建大对象,尤其是大于一个区块大小的那种对象
小结

首先,最好不要把上面的Old GC当做是一次GC来看,而应该当做并发标记周期来理解,虽然它确实会释放出一些内存。

并发标记结束后,G1也就知道了哪些区块是最适合被回收的,那些完全空闲的区块会在这这个阶段被回收。如果这个阶段释放了足够的内存出来,其实也就可以认为结束了一次GC。

我们假设并发标记结束了,那么下次GC的时候,还是会先回收年轻代,如果从年轻代中得到了足够的内存,那么结束;过了几次后,年轻代垃圾收集不能满足需要了,那么就需要利用之前并发标记的结果,选择一些活跃度最低的老年代区块进行回收。直到最后,老年代会进入下一个并发周期。

那么什么时候会启动并发标记周期呢?这个是通过参数控制的,下面马上要介绍这个参数了,此参数默认值是45,也就是说当堆空间使用了45%后,G1就会进入并发标记周期。

G1调优的目标是尽量避免出现Full GC,其实就是给老年代足够的空间,或相对更多的空间。

有以下几点我们可以进行调整的方向:

  • 增加堆大小,或调整老年代和年轻代的比例,这个很好理解
  • 增加并发周期的线程数量,其实就是为了加快并发周期快点结束
  • 让并发周期尽早开始,这个是通过设置堆使用占比来调整的(默认45%)
  • 在混合垃圾回收周期中回收更多的老年代区块

G1的很重要的目标是达到可控的停顿时间,所以很多的行为都以这个目标为出发点开展的。

我们通过设置-XX:MaxGCPauseMillis=N来指定停顿时间(单位ms,默认200ms),如果没有达到这个目标,G1会通过各种方式来补救:调整年轻代和老年代的比例,调整堆大小,调整晋升的年龄阈值,调整混合垃圾回收周期中处理的老年代的区块数量等等。

当然了,调整每个参数满足了一个条件的同时往往也会引入另一个问题,比如为了降低停顿时间,我们可以减小年轻代的大小,可是这样的话就会增加年轻代垃圾收集的频率。如果我们减少混合垃圾回收周期处理的老年代区块数量,虽然可以更容易满足停顿时间要求,可是这样就会增加Full GC的风险等等。

文章大篇幅整理自: