JVM 的垃圾回收器,你真的搞懂这些了吗?

JVM的GC经过多年的发展,大家对 Minor GC、 major GC的理解并不完全一致,所以我不打算在本文中使用这个概念。我把GC大概分为一下4类:

Young GC:只是负责回收年轻代对象的GC;

Old GC:只是负责回收老年代对象的GC;

Full GC:回收整个堆的对象,包括年轻代、老年代、持久带;

Mixed GC: 回收年轻代和部分老年代的GC (G1);

因为笔者目前使用G1还是比较少的,所以本文不打算将G1。

垃圾回收器算法

目前主流垃圾回收器都采用的是可达性分析算法来判断对象是否已经存活,不使用引用计数算法判断对象时候存活的原因在于该算法很难解决相互引用的问题。

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

标记-清除算法由标记阶段和清除阶段构成。标记阶段是把所有活着的对象都做上标记的阶段;清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。通过这两个阶段,就可以令不能利用的内存空间重新得到利用。

从 标记-清除算法我们可以看出, 该算法不涉及对象移动 ,但是 可能会产生内存碎片化 问题。空间碎片太高可能会导致程序运行时需要分配较大内存时候,无法找到足够的连续内存,需要其他垃圾回收帮助回收内存。

复制算法(Copying)

复制算法 内存空间分为两块区域: From、to ,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

上面那种复制算法有一半的空间是浪费的。所以在Java新生代把内存区域分为Eden空间、from、to空间3个部分, from和to空间也称为survivor 空间,用于存放未被回收的对象 。对象开始都是 Eden生成;当回收时,将Eden和from中存活的对象移动到to区域中 。

复制算法存在空间浪费的情况,始终都要保持一个Survivor是空闲的,并且在GC的时候要是存活对象大小超过了Survivor中的大小,就需要另外的策略存储存活对象。

目前open JDK新生代回收策略就是采用的复制算法,其中Eden和Survivor的默认配置为8:1

标记-压缩算法(Mark-Compact)

标记-压缩算法由标记阶段和压缩阶段构成。标记阶段标记-清除算法中的标记阶段完全一样,压缩阶段是让所有存活的对象向一端移动。这样空闲内存都在另外一端,属于连续空间,不存在内存碎片化问题,但是会产生对象移动。

分代算法(Generational GC)

根据对象的不同生命周期分别管理, JVM 中将对象分为我们熟悉的新生代、老年代和永久代分别管理。这样做的好处就是可以根据不同类型对象进行不同策略的管理,例如新生代中对象更新速度快,就会使用效率较高的复制算法。老年代中内存空间相对分配较大,而且时效性不如新生代强,就会常常使用Mark-Sweep-Compact (标记-清除-压缩)算法。

各种算法性能比较

常见的垃圾回收器

垃圾回收器分类

总体上可以把Java的垃圾回收器分为3类:

串行垃圾回收器(Serial Garbage Collector)

并行垃圾回收器(Parallel Garbage Collector)

并发标记扫描垃圾回收器(CMS Garbage Collector)

Java垃圾回收器主要有6种,各自优缺点以及组合关系如下:

其中的连线表示young gc和old gc可以搭配使用 

垃圾回收器选择策略 :

客户端程序 : Serial + Serial Old;

吞吐率优先的服务端程序(比如:计算密集型) : Parallel Scavenge + Parallel Old;

响应时间优先的服务端程序 :ParNew + CMS。

目前很大一部分的Java应用都集中在互联网的服务器端,这类应用尤其关系服务的响应时间,希望应用暂停时间更短,所以基本上使用的都是 ParNew + CMS ,这也是我司默认使用的配置。

CMS垃圾回收器

在启动JVM参数加上 -XX:+UseConcMarkSweepGC ,这个参数表示对于老年代的回收采用 CMS。

CMS执行过程

CMS 的回收过程主要分为下面的几个步骤:

初始标记(Initial Mark)

并发标记(Concurrent marking)

并发预清理(Concurrent pre-preclean)

重新标记(Final Remark)

并发清理(Concurrent sweep)

并发重置(Concurrent reset)

CMS日志解析

标准的CMS日志如下: 

2018-11-10T18:23:27.531+0800: 1495270.652: [GC ( CMS Initial Mark ) [1 CMS-initial-mark: 2008820K(2510848K)] 2038212K(4398336K), 0.0231086 secs] [Times: user=0.01 sys=0.00, real=0.03 secs] 

2018-11-10T18:23:27.554+0800: 1495270.675: [CMS-concurrent-mark-start]

2018-11-10T18:23:27.644+0800: 1495270.765: [ CMS-concurrent-mark : 0.090/0.090 secs] [Times: user=0.34 sys=0.03, real=0.09 secs] 

2018-11-10T18:23:27.644+0800: 1495270.765: [CMS-concurrent-preclean-start]

2018-11-10T18:23:27.654+0800: 1495270.775: [ CMS-concurrent-preclean : 0.010/0.010 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

2018-11-10T18:23:27.655+0800: 1495270.775: [CMS-concurrent-abortable-preclean-start]

2018-11-10T18:23:32.305+0800: 1495275.425: [ CMS-concurrent-abortable-preclean :  4.623/4.650 secs] [Times: user=7.01 sys=1.01, real=4.65 secs] 

2018-11-10T18:23:32.307+0800: 1495275.427: [GC ( CMS Final Remark ) [YG occupancy: 847369 K (1887488 K)]1495275.427: [Rescan (parallel) , 0.0902177 secs]1495275.518: [weak refs processing, 0.0514433 secs]1495275.569: [class unloading, 0.0256119 secs]1495275.595: [scrub symbol table, 0.0074695 secs]1495275.602: [scrub string table, 0.0015014 secs][1 CMS-remark: 2008820K(2510848K)] 2856190K(4398336K), 0.1806988 secs] [Times: user=0.68 sys=0.00, real=0.18 secs] 

2018-11-10T18:23:32.488+0800: 1495275.609: [CMS-concurrent-sweep-start]

2018-11-10T18:23:33.660+0800: 1495276.781: [ CMS-concurrent-sweep : 1.172/1.172 secs] [Times: user=1.89 sys=0.24, real=1.17 secs] 

2018-11-10T18:23:33.661+0800: 1495276.782: [CMS-concurrent-reset-start]

2018-11-10T18:23:33.667+0800: 1495276.788: [ CMS-concurrent-reset:  0.006/0.006 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

初始标记(CMS Initial Mark)

该阶段进行可达性分析, 标记GC ROOTS能直接关联到的对象 。 该阶段会暂停应用 。

2008820K – 当前老年代使用情况;

(2510848K) – 老年代可用容量;

2038212K – 当前整个堆的使用情况;

(4398336K) – 整个堆的容量;

.0231086 secs] [Times: user=0.01 sys=0.00, real=0.03 secs] – 时间计量;

并发标记( CMS-concurrent-mark )

并发标记就需要标记出 GC ROOTS 关联到的对象的引用对象有哪些。比如说 A -> B (A 引用 B,假设 A 是 GC Roots 关联到的对象),那么这个阶段就是标记出 B 对象, A 对象会在初始标记中标记出来。

并发预清理( CMS-concurrent-preclean 

这个阶段主要并发查找在做并发标记阶段时从年轻代晋升到老年代的对象或老年代新分配的对象(大对象直接进入老年代)或被用户线程更新的对象,来减少重新标记阶段的工作量。

重新标记 ( CMS Final Remark )

由于在并发标记和并发预清理这个阶段,用户线程和GC 线程并发,假如这个阶段用户线程产生了新的对象,总不能被 GC 掉吧。这个阶段就是为了让这些对象重新标记。 该阶段也会暂停应用

YG occupancy: 847369 K (1887488 K)]– 年轻代当前占用情况和容量;

Rescan (parallel) , 0.0902177 secs – 这个阶段在应用停止的阶段完成存活对象的标记工作;

weak refs processing, 0.0514433 secs – 第一个子阶段,随着这个阶段的进行处理弱引用;

class unloading, 0.0256119 secs – 第二个子阶段(that is unloading the unused classes, with the duration and timestamp of the phase);

scrub symbol table, 0.0074695 secs – 最后一个子阶段(that is cleaning up symbol and string tables which hold class-level metadata and internalized string respectively)

2008820K(2510848K)] – 在这个阶段之后老年代占有的内存大小和老年代的容量;

2856190K(4398336K) – 在这个阶段之后整个堆的内存大小和整个堆的容量;

0.1806988 secs – 这个阶段的持续时间;

[Times: user=0.68 sys=0.00, real=0.18 secs]  – 同上;

并发清理 ( CMS-concurrent-sweep )

这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。注意这个阶段会产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾。

并发重置 ( CMS-concurrent-reset )

CMS清除内部状态,为下次回收做准备。

注意:CMS虽然是老年代算法,但也是需要扫描新生代区域的。

CMS算法降级

cms存在着内存碎片化问题: 申请内存时,虽然总内存大于申请内存,但是没有连续内存大于申请内存,导致内存申请失败。 CMS提供了机制(CMS GC降级到Full GC)来解决该问题。 Full GC使用的算法是 mark-sweep-compact( 类似于 Serial垃圾回收器), 他的作用域在 整个堆的对象,包括年轻代、老年代、持久代 , 但compaction是可选的 。其中参数 CMSFullGCsBeforeCompaction=N表示每隔N次真正的full GC才做一次压缩 (而不是每N次CMS GC就做一次压缩,目前JVM里没有这样的参数), CMSFullGCsBeforeCompaction默认值是0,也就是每次full GC都会进行内存压缩。这个尽量使用默认值,不然内存 碎片化可能会更严重些。

那么配置的CMS GC啥时候会触发Full gc呢?主要有下面几种情况触发Full Gc:

旧生代空间不足:java.lang.outOfMemoryError:java heap space;

Perm空间满:java.lang.outOfMemoryError:PermGen space;

CMS GC时出现promotion failed(当进行 Young GC 时,有部分新生代代对象仍然可用,但是S0或S1放不下,因此需要放到老年代,但此时老年代空间无法容纳这些对象) 和concurrent  mode failure(当 CMS GC 正进行时,此时有新的对象要进行老年代,但是老年代空间不足造成的);在此我向大家推荐一个架构学习交流群。交流学习群号:821169538

统计得到的minor GC晋升到旧生代的平均大小大于旧生代的剩余空间;

主动触发Full GC( System.gc()、jmap等 )。

如何识别是执行的是CMS GC还是 Full GC呢?主要是根据GC log,CMS GC会在日志中标记出各个执行阶段,但是要是执行Full GC只会显示full次数加1。

CMS相关参数

-XX:CMSInitiatingOccupancyFraction=N 和-XX:+UseCMSInitiatingOccupancyOnly

这两个设置一般配合使用, 目的在于降低CMS GC频率或者增加频率。

-XX:CMSInitiatingOccupancyFraction=N 是指设定CMS在对内存占用率达到N%的时候开始进行CMS GC。

-XX:+UseCMSInitiatingOccupancyOnly 只是用设定的回收阈值(上面指定的N%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整.

-XX:+CMSScavengeBeforeRemark

这个参数表示CMS GC前启动一次ygc,目的在于减少old区域对ygc区域的引用,降低remark时的开销,一般CMS的GC耗时80%都在remark阶段

-XX:+UseCMSCompactAtFullCollection和 -XX:CMSFullGCsBeforeCompaction=N

这两个参数要配合使用,其中 CMSFullGCsBeforeCompaction上面已经讲解过了。

CMS 的缺点

会产生空间碎片。CMS 垃圾回收器采用的基础算法是 Mark-Sweep,没有内存整理的过程,所以经过 CMS 收集的堆会产生空间碎片。

对CPU资源非常敏感。为了让应用程序不停顿,CMS 线程需要和应用程序线程并发执行,这样就需要有更多的 CPU,同时会使得总吞吐量降低。

CMS无法处理浮动垃圾,所以一般需要更大的堆空间。因为CMS 在标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在 CMS 回收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。

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

推荐阅读更多精彩内容