欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JVM探秘:垃圾收集器

程序员文章站 2022-03-10 12:33:18
本系列笔记主要基于《深入理解Java虚拟机:JVM高级特性与最佳实践 第2版》,是这本书的读书笔记。 垃圾收集器 垃圾收集算法是是内存回收的方法论,垃圾收集器是内存回收的具体实现。不同的虚拟机会有不同的垃圾收集器的实现,我们主要讨论的是默认的HotSpot虚拟机,这个虚拟机包含的垃圾收集器如下图; ......

本系列笔记主要基于《深入理解java虚拟机:jvm高级特性与最佳实践 第2版》,是这本书的读书笔记。

垃圾收集器

垃圾收集算法是是内存回收的方法论,垃圾收集器是内存回收的具体实现。不同的虚拟机会有不同的垃圾收集器的实现,我们主要讨论的是默认的hotspot虚拟机,这个虚拟机包含的垃圾收集器如下图;

JVM探秘:垃圾收集器

如上图所示,一共有7种垃圾收集器,如果两个垃圾收集器之间有双箭头连线,则两个垃圾收集器可搭配使用。上面是新生代的收集器,下面是老年代的收集器。每个垃圾收集器都有各自适合的使用场景。

serial 收集器

serial是一个“单线程”的新生代收集器,使用复制算法,它只会使用一个cpu或者一条收集器线程去完成垃圾收集工作,并且它在垃圾收集时,必须暂停所有其他的工作线程,直到它收集结束。“stop the world”会在用户不可见的情况下,把用户的工作线程全部停掉,这往往是令人难以接受的。

下图是 serial/serial old 收集器运行示意图:

JVM探秘:垃圾收集器

上图中,新生代是serial收集器采用复制算法,老年代是serial old收集器采用标记-整理算法。serial虽然是一个缺点鲜明的收集器,但它依然是虚拟机在client模式下的默认收集器,它也有优点,比如简单高效(与其他收集器单线程相比),对于单个cpu来说,serial由于没有线程交互的开销,效率比较高,对于桌面应用来说,分配给虚拟机的内存不会很大,收集时的停顿也是在可接受范围内的。

parnew 收集器

parnew收集器是serial收集器的多线程版本,也是使用复制算法的新生代收集器,它除了使用多条线程进行垃圾收集以外,其他的比如收集器的控制参数、收集算法、stop-the-world、对象分配规则、回收策略都和serial收集器完全一样。

下图是 parnew/serial old 收集器运行示意图:

JVM探秘:垃圾收集器

上图中,新生代是parnew收集器采用复制算法,老年代是serial old收集器采用标记-整理算法。parnew是许多server模式下虚拟机的首选新生代收集器,多是因为它能与cms收集器配合工作。cms收集器是hotspot虚拟机中第一个并发的垃圾收集器,cms第一次实现了让用户线程与垃圾收集线程同时工作。

简单介绍下垃圾收集中的并行与并发概念:

  • 并行(parallel):指多条垃圾收集线程并行工作,但此时用户线程是等待状态。
  • 并发(concurrent):指用户线程与垃圾收集线程同时执行,用户程序运行的同时,垃圾收集程序运行于另一个cpu上。

parallel scavenge 收集器

parallel scavenge也是使用复制算法的新生代收集器,并且也是一个并行的多线程收集器。parallel收集器跟其它收集器关注gc停顿时间不同,它关注的是吞吐量。低停顿时间适合需要与用户交互的程序,而高吞吐量可以高效率的利用cpu时间,能尽快完成运算任务,适合用于后台计算较多而交互较少的任务。

  • 吞吐量(throughput):cpu用于运行用户代码的时间与cpu总消耗时间的比值,吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)。

parallel收集器提供了两个虚拟机参数用以控制吞吐量,-xx:maxgcpausemillis参数可以控制垃圾收集的最大停顿时间,-xx:gctimeratio参数可以直接设置吞吐量大小。

-xx:maxgcpausemillis的值是一个大于0的毫秒数,使用它减小gc停顿时间是牺牲吞吐量和新生代空间换来的,例如系统把新生代调小,收集300m的新生代肯定比500m的快,这也导致垃圾收集发生的更频繁,原来10秒收集一次每次停顿100毫秒,现在5秒收集一次每次停顿70毫秒,停顿时间下降了,但是吞吐量也下降了。

-xx:gctimeratio的值是一个0到100的整数,通过它我们告诉jvm吞吐量要达到的目标值,-xx:gctimeratio=n指定目标应用程序线程的执行时间(与总的程序执行时间)达到n/(n+1)的目标比值。例如,它的默认值是99,就是说要求应用程序线程在整个执行时间中至少99/100是活动的(gc线程占用其余的1/100),也就是说,应用程序线程应该运行至少99%的总执行时间。

除这两个参数外,还有一个参数-xx:-useadaptivesizepolicy值得关注,这是一个开关参数,当它打开之后,就不需要手工指定新生代大小(-xmn)、eden与survivor区的比例(-xx:survivorratio)、晋升老年代对象年龄(-xx:pretenuresizethreshold)等细节参数了,虚拟机会根据系统的运行情况收集性能监控信息,动态的调整这些参数来提高gc性能,这种调节方式称为gc自适应调节策略。这个参数是默认激活的,自适应行为也是jvm优势之一。

serial old 收集器

serial old是serial收集器的老年代版本,同样是一个“单线程”收集器,使用标记-整理算法。这个收集器主要是给client模式下的虚拟机使用,server模式下还有两个用途,一个是在jdk1.5及之前的版本中与parallel scavenge收集器搭配使用,另一个是作为cms收集器的后备预案,在并发收集发生concurrent mode failure时使用。工作过程请看serial 收集器部分的 serial/serial old 收集器运行示意图。

parallel old 收集器

parallel old收集器是parallel scavenge的老年代版本,使用多线程标记-整理算法。此收集器在jdk1.6中开始出现,在parallel old出现之前,只有serial old能够与parallel scavenge收集器配合使用。由于serial old这种单线程收集器的性能拖累,导致在老年代比较大的场景下,parallel scavenge和serial old的组合吞吐量甚至还不如parnew加cms的组合。而有了parallel old收集器之后,parallel scavenge与parallel old成了名副其实的吞吐量优先的组合,在注重吞吐量和cpu资源敏感的场景下,都可以优先考虑这对组合。

下图是 parnew/serial old 收集器运行示意图:

JVM探秘:垃圾收集器

cms 收集器

cms(concurrent mark sweep)收集器是基于标记-清除算法的老年代收集器,它以获取最短回收停顿时间为目标。cms是一款优秀的收集器,特点是并发收集、低停顿,它的运行过程稍微复杂些,分为4个步骤:

  1. 初始标记(cms initial mark)
  2. 并发标记(cms concurrent mark)
  3. 重新标记(cms remark)
  4. 并发清除(cms concurrent sweep)

4个步骤中只有初始标记、重新标记这两步需要“stop the world”。初始标记只是标记一下gc roots能直接关联的对象,速度很快。并发标记是进行gc roots tracing的过程,也就是从gc roots开始进行可达性分析。重新标记则是为了修正并发标记期间因用户线程继续运行而导致标记发生变动的那一部分记录。并发清理当然就是进行清理被标记对象的工作。

下图是 cms 收集器运行示意图:

JVM探秘:垃圾收集器

整个过程中,并发标记与并发清除过程耗时最长,但它们都可以与用户线程一起工作,所以整体上说,cms收集器的内存回收过程是与用户线程一起并发执行的。

但是cms收集器也并不完美,它有以下3个缺点:

  1. cms收集时对cpu资源非常敏感,并发阶段虽然不会导致用户线程停顿,但是会因为占用cpu资源导致应用程序变慢、总吞吐量变低。
  2. cms收集器无法处理浮动垃圾(floating garbage),可能会产生full gc。浮动垃圾就是在并发清理阶段,依然在运行的用户线程产生的垃圾。这部分垃圾出现在标记过程之后,cms无法在当次集中处理它们,只能等下一次gc时清理。
  3. cms是基于标记-清除算法的收集器,可能会产生大量的空间碎片,从而无法分配大对象而导致full gc提前产生。

g1 收集器

g1(garbage-first)收集器是面向服务端应用的垃圾收集器,它被寄予厚望以用来替换cms收集器。在g1之前的收集器中,收集的范围要么是整个新生代要么就是老年代,而g1不再从物理上区分新生代老年代,g1可以独立管理整个java堆。它将java堆划分为多个大小相等的独立区域(region),虽然还有新生代老年代的概念,但不再是物理隔离的,而都是一部分region(不需要连续)的集合。

与其他收集器相比,g1收集器的特点有:

  1. 并行与并发:g1能充分利用多cpu或者多核心的cpu,来缩短stop the world的停顿时间。
  2. 分代收集:虽然g1收集器可以独立管理整个gc堆,但它能采用不同的方式处理“新对象”和“老对象”,以达到更好的收集效果。
  3. 空间整合:g1从整体看是基于标记-整理算法的,从局部看(两个region之间)是基于复制算法实现的,这两个算法在收集时都不会产生空间碎片,这样就有连续可用的内存用以分配大对象。
  4. 可预测的停顿:g1除了追求低停顿外,还能建立可预测的停顿时间模型,可以明确指定一个最大停顿时间(-xx:maxgcpausemillis),停顿时间需要不断调优找到一个理想值,过大过小都会拖慢性能。

g1收集器之所以能建立可预测的停顿时间模型,是因为它可以避免在整个java堆中进行全区域的垃圾收集,g1根据各个region里垃圾堆积的价值大小(回收所获空间大小及所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region,这也是garbage-first名称的由来。

g1收集器的region如下图所示:

JVM探秘:垃圾收集器

图中的e代表是eden区,s代表survivor,o代表old区,h代表humongous表示巨型对象(大于region空间的对象)。从图中可以看出各个区域逻辑上并不是连续的,并且一个region在某一个时刻是eden,在另一个时刻就可能属于老年代。g1在进行垃圾清理的时候就是将一个region的对象拷贝到另外一个region中。

再介绍一个概念:remembered set(记忆集)。每个region中都有一个remembered set,记录的是其他region中的对象引用本region对象的关系(谁引用了我的对象)。所以在垃圾回收时,在gc根节点的枚举范围中加入remembered set即可保证不对全堆扫描也不会有遗漏。g1里面还有另外一种数据结构叫collection set,collection set记录的是gc要收集的region的集合,collection set里的region可以是任意代的。在gc的时候,对于跨代对象引用,只要扫描对应的collection set中的remembered set即可。

不算上维护remembered set的话,g1收集器的收集过程如下图所示:

JVM探秘:垃圾收集器

如图所示,g1收集过程有如下几个阶段:

  1. 初始标记(initial marking)
  2. 并发标记(concurrent marking)
  3. 最终标记(final marking)
  4. 筛选回收(live data counting and evacuation)

初始标记只标记一下gc roots能关联到的对象,需要停顿线程但是耗时短,会停顿用户线程(stop the world)。并发标记是从gc root开始对堆中对象进行可达性分析,找出存活对象,这阶段耗时长但是可以与用户线程并发执行。最终标记就是为了修正在并发标记阶段,因用户线程继续运行而导致标记产生变动的那一部分标记记录,这阶段需要停顿用户线程(stop the world),但是可并行执行。筛选回收阶段会对各个region的回收价值和成本进行排序,根据用户期望的gc停顿时间来制定回收计划,该阶段也是会停顿用户线程(stop the world)。

垃圾收集参数

查询当前使用的垃圾收集器:
java -xx:+printcommandlineflags -version
此命令让jvm打印出那些已经被用户或者jvm设置过的详细的xx参数的名称和值。

vm参数 描述
-xx:+useserialgc 指定serial收集器+serial old收集器组合执行内存回收
-xx:+useparnewgc 指定parnew收集器+serilal old组合执行内存回收
-xx:+useparallelgc 指定parallel收集器+serial old收集器组合执行内存回收
-xx:+useparalleloldgc 指定parallel收集器+parallel old收集器组合执行内存回收
-xx:+useconcmarksweepgc 指定cms收集器+parnew收集器+serial old收集器组合执行内存回收。优先使用parnew收集器+cms收集器的组合,当出现concurrentmode fail或者promotion failed时,则采用parnew收集器+serial old收集器的组合
-xx:+useg1gc 指定g1收集器并发、并行执行内存回收
-xx:+printgcdetails 打印gc详细信息
-xx:+printgctimestamps 输出gc的时间戳(以基准时间的形式)
-xx:+printgcdatestamps 输出gc的时间戳(以日期的形式)
-xx:+printheapatgc 在进行gc的前后打印出堆的信息
-xx:+printtenuringdistribution 在进行gc时打印survivor中的对象年龄分布信息
-xloggc:$catalina_home/logs/gc.log 指定输出路径收集日志到日志文件
-xx:newratio 新生代与老生代(new/old generation)的大小比例(ratio). 默认值为 2
-xx:survivorratio eden/survivor 空间大小的比例(ratio). 默认值为 8
-xx:gctimeratio gc时间占总时间的比率,默认值99%,仅在parallel scavenge收集器时生效
-xx:maxgcpausemills 设置gc最大停顿时间,仅在parallel scavenge收集器时生效
-xx:pretensuresizethreshold 直接晋升到老年代的对象大小,大于这个参数的对象直接在老年代分配
-xx:maxtenuringthreshold 提升年老代的最大临界值(tenuring threshold). 默认值为 15
-xx:useadaptivesizepolicy 动态调整java堆中各个区域的大小及进入老年代的年龄
-xx:handlepromotionfailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代整个eden和survivor中对象都存活的极端情况
-xx:parallelgcthreads 设置垃圾收集器在并行阶段使用的线程数,默认值随jvm运行的平台不同而不同
-xx:parallelcmsthreads 设定cms的线程数量
-xx:concgcthreads 并发垃圾收集器使用的线程数量. 默认值随jvm运行的平台不同而不同
-xx:cmsinitiatingoccupancyfraction 设置cms收集器在老年代空间被使用多少后触发垃圾收集,默认68%
-xx:+usecmscompactatfullcollection 设置cms收集器在完成垃圾收集后是否要进行一次内存碎片的整理
-xx:cmsfullgcsbeforecompaction 设定进行多少次cms垃圾回收后,进行一次内存压缩
-xx:+cmsclassunloadingenabled 允许对类元数据进行回收
-xx:cmsinitiatingpermoccupancyfraction 当永久区占用率达到这一百分比时,启动cms回收
-xx:usecmsinitiatingoccupancyonly 表示只在到达阀值的时候,才进行cms回收
-xx:initiatingheapoccupancypercent 指定当整个堆使用率达到多少时,触发并发标记周期的执行,默认值是45%
-xx:g1heapwastepercent 并发标记结束后,会知道有多少空间会被回收,再每次ygc和发生mixedgc之前,会检查垃圾占比是否达到此参数,达到了才会发生mixedgc
-xx:g1reservepercent 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10
-xx:g1heapregionsize 使用g1时java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1mb, 最大值为 32mb