评价GC算法的指标和GC原理

吞吐量

垃圾回收算法(6 个字太长了,以下简称 GC)算是对程序完成它想做的事情的一种辅助,并不是程序的主要目的(废话)。所以 GC 占用的时间越少越好,程序花在正事上面的时间越多越好。

这个指标其实很好理解。举个例子吧,标记清除算法要遍历两次,第一次遍历所有活跃的对象,将它们标记为“不是垃圾”。第二次遍历所有的对象,将没有被标记的垃圾回收掉。复制收集算法只需要遍历一次,将活跃的对象从内存的一般复制到另一半。所以从这个指标的角度讲,复制收集算法完爆标记清除。

但实际上,这个指标是不能“静态衡量”的。依然是上面两种算法,标记清除遍历的时候速度很快,只要写一个标记就可以了。而复制收集算法要有 copy 操作。随着堆中活动对象的增加,甚至会出现复制收集吞吐量小于标记清除的情况。

内存使用率

这个指标也比较好理解,GC 算法需要一些标记,但是如果算法本身所使用的内存占得很多,就得不偿失了。这个算法本身的目的就是回收内存,本身却占用了很多内存,听起来就不合理。

其实,从算法的角度讲,内存是空间,吞吐量是时间。算法上有“用空间换时间”的策略,自然 GC 也会有。这是一种 Trade off,很多情况不可兼得。

拿引用计数来说,用几个位来表示引用计数是门学问。简单的话,占用 8 个位表示,那每个对象 1byte 就没有了。假设是占用 2 个字节的对象,那么内存占用就扩大了整整 1.5 倍。 而大多数对象仅仅会被引用 1 次而已。所以引用计数方法就发展出一些优化措施,减少引用计数占用的内存,配合其他算法来处理计数器溢出的问题。有一种极端的方式是只使用 1 位来计数(倒不如说这是一种标记了)。

最大暂停时间

这个指标和“吞吐量”看起来有些像,GC 算法速度越快,时间就越小。但是吞吐量指的是总体的速度,最大暂停时间指的是 GC 算法执行的时候,程序在等待 GC 完成的最大时间。这段时间由于 GC 的运行程序无法做其他的事情。

为什么这个指标会重要呢?有些程序可能可以忍受吞吐量不高,但是实时性要求很强的。比如说,A 算法每分钟暂停 1 次,一次暂停 1 秒,一小时一共暂停 60s。另一个 B 算法每小时暂停 1 次,一次暂停 30s。可想而知,如果是用户程序暂停时间长是体验很糟糕的,另外比如机器人控制程序,迈开一只腿这时候到了暂停时间,机器人就摔倒了。

引用计数的最大暂停时间是最好的,因为对象的引用到 0 的时候立即会回收,这个过程不需要暂停程序。而复制算法和标记清除算法需要在无法申请出更多内存的时候,暂停程序,开始清除阶段/复制阶段。

连续性(缓存友好程度)

我们知道,越快的存储价格就越高。CPU 寄存器速度最快,但是只有几个寄存器。高速缓存非常快,但是极其昂贵。然后是内存、硬盘等。

如果程序在执行的时候缓存命中率高,那么运行效率就会高。

在这篇文章中,我们可以将程序粗略的分成两部分:GC 运行的部分和程序运行的部分。如果 GC 运行的时候需要频繁寻找对象,然后对象的引用又在很远的地方,那么缓存命中率就会很低;另一方面说,如果 GC 算法将程序的对象变得很离散,那么程序在运行的时候,互相引用的对象离得很远,效率就会很低。

标记清除算法会造成内存的碎片,对缓存不友好。复制收集算法由于是拷贝不是垃圾的对象,所以在一次拷贝操作之后,垃圾对象被释放,非垃圾对象都在一起了,所以命中率会高。另外上面提到的 1 位引用计数算法由于只拷贝指针,而不需要去找到对象,所以缓存命令率也会高。

GC基础原理

1 GC调优目标

大多数情况下对 Java 程序进行GC调优, 主要关注两个目标:响应速度、吞吐量

响应速度(Responsiveness)
响应速度指程序或系统对一个请求的响应有多迅速。比如,用户订单查询响应时间,对响应速度要求很高的系统,较大的停顿时间是不可接受的。调优的重点是在短的时间内快速响应

吞吐量(Throughput)
吞吐量关注在一个特定时间段内应用系统的最大工作量,例如每小时批处理系统能完成的任务数量,在吞吐量方面优化的系统,较长的GC停顿时间也是可以接受的,因为高吞吐量应用更关心的是如何尽可能快地完成整个任务,不考虑快速响应用户请求

GC调优中,GC导致的应用暂停时间影响系统响应速度,GC处理线程的CPU使用率影响系统吞吐量

2 GC分代收集算法

现代的垃圾收集器基本都是采用分代收集算法,其主要思想: 将Java的堆内存逻辑上分成两块:新生代、老年代,针对不同存活周期、不同大小的对象采取不同的垃圾回收策略

image.png
  • 新生代(Young Generation)

新生代又叫年轻代,大多数对象在新生代中被创建,很多对象的生命周期很短。每次新生代的垃圾回收(又称Young GC、Minor GC、YGC)后只有少量对象存活,所以使用复制算法,只需少量的复制操作成本就可以完成回收.

新生代内又分三个区:一个Eden区,两个Survivor区(S0、S1,又称From Survivor、To Survivor),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足晋升到老年代条件的对象将被复制到另外一个Survivor区。对象每经历一次复制,年龄加1,达到晋升年龄阈值后,转移到老年代

老年代(Old Generation)
在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代,该区域中对象存活率高。老年代的垃圾回收通常使用“标记-整理”算法

3 GC事件分类

根据垃圾收集回收的区域不同,垃圾收集主要通常分为Young GC、Old GC、Full GC、Mixed GC
(1) Young GC
新生代内存的垃圾收集事件称为Young GC(又称Minor GC),当JVM无法为新对象分配在新生代内存空间时总会触发 Young GC,比如 Eden 区占满时。新对象分配频率越高, Young GC 的频率就越高

Young GC 每次都会引起全线停顿(Stop-The-World),暂停所有的应用线程,停顿时间相对老年代GC的造成的停顿,几乎可以忽略不计

Old GC,只清理老年代空间的GC事件,只有CMS的并发收集是这个模式
Full GC,清理整个堆的GC事件,包括新生代、老年代、元空间等

Mixed GC,清理整个新生代以及部分老年代的GC,只有G1有这个模式

GC日志分析

GC日志是一个很重要的工具,它准确记录了每一次的GC的执行时间和执行结果,通过分析GC日志可以调优堆设置和GC设置,或者改进应用程序的对象分配模式,开启的JVM启动参数如下:

-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps  -XX:+PrintGCTimeStamps

常见的Young GC、Full GC日志含义如下:

image.png

image.png

免费的GC日志图形分析工具推荐下面2个:

  • GCViewer,下载jar包直接运行
  • gceasy,web工具,上传GC日志在线使用
5 内存分配策略

Java提供的自动内存管理,可以归结为解决了对象的内存分配和回收的问题,前面已经介绍了内存回收,下面介绍几条最普遍的内存分配策略

对象优先在Eden区分配
大多数情况下,对象在先新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Young GC

大对象直接进入老年代
JVM提供了一个对象大小阈值参数(-XX:PretenureSizeThreshold,默认值为0,代表不管多大都是先在Eden中分配内存),大于参数设置的阈值值的对象直接在老年代分配,这样可以避免对象在Eden及两个Survivor直接发生大内存复制

长期存活的对象将进入老年代
对象每经历一次垃圾回收,且没被回收掉,它的年龄就增加1,大于年龄阈值参数(-XX:MaxTenuringThreshold,默认15)的对象,将晋升到老年代中

空间分配担保
当进行Young GC之前,JVM需要预估:老年代是否能够容纳Young GC后新生代晋升到老年代的存活对象,以确定是否需要提前触发GC回收老年代空间,基于空间分配担保策略来计算:

continueSize:老年代最大可用连续空间

image.png

Young GC之后如果成功(Young GC后晋升对象能放入老年代),则代表担保成功,不用再进行Full GC,提高性能;如果失败,则会出现“promotion failed”错误,代表担保失败,需要进行Full GC
动态年龄判定
新生代对象的年龄可能没达到阈值(MaxTenuringThreshold参数指定)就晋升老年代,如果Young GC之后,新生代存活对象达到相同年龄所有对象大小的总和大于任一Survivor空间(S0 或 S1总空间)的一半,此时S0或者S1区即将容纳不了存活的新生代对象,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

另外,如果Young GC后S0或S1区不足以容纳:未达到晋升老年代条件的新生代存活对象,会导致这些存活对象直接进入老年代,需要尽量避免

CMS原理及调优

1 名词解释
  • 可达性分析算法:用于判断对象是否存活,基本思想是通过一系列称为“GC Root”的对象作为起点(常见的GC Root有系统类加载器、栈中的对象、处于激活状态的线程等),基于对象引用关系,从GC Roots开始向下搜索,所走过的路径称为引用链,当一个对象到GC Root没有任何引用链相连,证明对象不再存活
  • Stop The World:GC过程中分析对象引用关系,为了保证分析结果的准确性,需要通过停顿所有Java执行线程,保证引用关系不再动态变化,该停顿事件称为Stop The World(STW)
  • Safepoint:代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要GC,线程可以在这个位置暂停。HotSpot采用主动中断的方式,让执行线程在运行期轮询是否需要暂停的标志,若需要则中断挂起
2 CMS简介

CMS(Concurrent Mark and Sweep 并发-标记-清除),是一款基于并发、使用标记清除算法的垃圾回收算法,只针对老年代进行垃圾回收。CMS收集器工作时,尽可能让GC线程和用户线程并发执行,以达到降低STW时间的目的
通过以下命令行参数,启用CMS垃圾收集器:

-XX:+UseConcMarkSweepGC

值得补充的是,下面介绍到的CMS GC是指老年代的GC,而Full GC指的是整个堆的GC事件,包括新生代、老年代、元空间等,两者有所区分

3 新生代垃圾回收

能与CMS搭配使用的新生代垃圾收集器有Serial收集器和ParNew收集器。这2个收集器都采用标记复制算法,都会触发STW事件,停止所有的应用线程。不同之处在于,Serial是单线程执行,ParNew是多线程执行

image.png
4 老年代垃圾回收

CMS GC以获取最小停顿时间为目的,尽可能减少STW时间,可以分为7个阶段


image.png

阶段 1: 初始标记(Initial Mark)

此阶段的目标是标记老年代中所有存活的对象, 包括 GC Root 的直接引用, 以及由新生代中存活对象所引用的对象,触发第一次STW事件
这个过程是支持多线程的(JDK7之前单线程,JDK8之后并行,可通过参数CMSParallelInitialMarkEnabled调整)

image.png
image.png

阶段 2: 并发标记(Concurrent Mark)
此阶段GC线程和应用线程并发执行,遍历阶段1初始标记出来的存活对象,然后继续递归标记这些对象可达的对象

image.png

阶段 3: 并发预清理(Concurrent Preclean)

此阶段GC线程和应用线程也是并发执行,因为阶段2是与应用线程并发执行,可能有些引用关系已经发生改变。
通过卡片标记(Card Marking),提前把老年代空间逻辑划分为相等大小的区域(Card),如果引用关系发生改变,JVM会将发生改变的区域标记位“脏区”(Dirty Card),然后在本阶段,这些脏区会被找出来,刷新引用关系,清除“脏区”标记

image.png

阶段 4: 并发可取消的预清理(Concurrent Abortable Preclean)

此阶段也不停止应用线程. 本阶段尝试在 STW 的 最终标记阶段(Final Remark)之前尽可能地多做一些工作,以减少应用暂停时间
在该阶段不断循环处理:标记老年代的可达对象、扫描处理Dirty Card区域中的对象,循环的终止条件有:
1 达到循环次数
2 达到循环执行时间阈值
3 新生代内存使用率达到阈值

阶段 5: 最终标记(Final Remark)

这是GC事件中第二次(也是最后一次)STW阶段,目标是完成老年代中所有存活对象的标记。在此阶段执行:
1 遍历新生代对象,重新标记
2 根据GC Roots,重新标记
3 遍历老年代的Dirty Card,重新标记

阶段 6: 并发清除(Concurrent Sweep)

此阶段与应用程序并发执行,不需要STW停顿,根据标记结果清除垃圾对象


image.png

阶段 7: 并发重置(Concurrent Reset)
此阶段与应用程序并发执行,重置CMS算法相关的内部数据, 为下一次GC循环做准备

5 CMS常见问题

最终标记阶段停顿时间过长问题
CMS的GC停顿时间约80%都在最终标记阶段(Final Remark),若该阶段停顿时间过长,常见原因是新生代对老年代的无效引用,在上一阶段的并发可取消预清理阶段中,执行阈值时间内未完成循环,来不及触发Young GC,清理这些无效引用
通过添加参数:-XX:+CMSScavengeBeforeRemark。在执行最终操作之前先触发Young GC,从而减少新生代对老年代的无效引用,降低最终标记阶段的停顿,但如果在上个阶段(并发可取消的预清理)已触发Young GC,也会重复触发Young GC

并发模式失败(concurrent mode failure) & 晋升失败(promotion failed)问题

image.png

并发模式失败:当CMS在执行回收时,新生代发生垃圾回收,同时老年代又没有足够的空间容纳晋升的对象时,CMS 垃圾回收就会退化成单线程的Full GC。所有的应用线程都会被暂停,老年代中所有的无效对象都被回收

image.png

晋升失败:当新生代发生垃圾回收,老年代有足够的空间可以容纳晋升的对象,但是由于空闲空间的碎片化,导致晋升失败,此时会触发单线程且带压缩动作的Full GC

并发模式失败和晋升失败都会导致长时间的停顿,常见解决思路如下:

  • 降低触发CMS GC的阈值,即参数-XX:CMSInitiatingOccupancyFraction的值,让CMS GC尽早执行,以保证有足够的空间
  • 增加CMS线程数,即参数-XX:ConcGCThreads,
  • 增大老年代空间
  • 让对象尽量在新生代回收,避免进入老年代

内存碎片问题
通常CMS的GC过程基于标记清除算法,不带压缩动作,导致越来越多的内存碎片需要压缩,常见以下场景会触发内存碎片压缩:

  • 新生代Young GC出现新生代晋升担保失败(promotion failed)
  • 程序主动执行System.gc()

可通过参数CMSFullGCsBeforeCompaction的值,设置多少次Full GC触发一次压缩,默认值为0,代表每次进入Full GC都会触发压缩,带压缩动作的算法为上面提到的单线程Serial Old算法,暂停时间(STW)时间非常长,需要尽可能减少压缩时间

G1原理及调优

G1(Garbage-First)是一款面向服务器的垃圾收集器,支持新生代和老年代空间的垃圾收集,主要针对配备多核处理器及大容量内存的机器,G1最主要的设计目标是: 实现可预期及可配置的STW停顿时间

2 G1堆空间划分
image.png

Region

为实现大内存空间的低停顿时间的回收,将划分为多个大小相等的Region。每个小堆区都可能是 Eden区,Survivor区或者Old区,但是在同一时刻只能属于某个代
在逻辑上, 所有的Eden区和Survivor区合起来就是新生代,所有的Old区合起来就是老年代,且新生代和老年代各自的内存Region区域由G1自动控制,不断变动

巨型对象

当对象大小超过Region的一半,则认为是巨型对象(Humongous Object),直接被分配到老年代的巨型对象区(Humongous regions),这些巨型区域是一个连续的区域集,每一个Region中最多有一个巨型对象,巨型对象可以占多个Region
G1把堆内存划分成一个个Region的意义在于:

每次GC不必都去处理整个堆空间,而是每次只处理一部分Region,实现大容量内存的GC
通过计算每个Region的回收价值,包括回收所需时间、可回收空间,在有限时间内尽可能回收更多的内存,把垃圾回收造成的停顿时间控制在预期配置的时间范围内,这也是G1名称的由来: garbage-first

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354