本篇文章为大家展示了Java中JVM内存布局的GC原理是怎样的,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
刚开始时,对象分配在 eden 区,s0(即:from)及 s1(即:to)区,几乎是空着。
随着应用的运行,越来越多的对象被分配到 eden 区。
当 Eden 区放不下时,就会发生 minor GC(也被称为 young GC),第1步当然是要先标识出不可达垃圾对象(即:下图中的黄色块),然后将可达对象,移动到 s0 区(即:4 个淡蓝色的方块挪到 s0 区),然后将黄色的垃圾块清理掉,这一轮过后,eden 区就成空的了。
注:这里其实已经综合运用了“【标记-清理 eden】 + 【标记-复制 eden->s0】”算法。
继续,随着对象的不断分配,eden 空可能又满了,这时会重复刚才的 minor GC 过程,不过要注意的是,这时候 s0 是空的,所以 s0 与 s1 的角色其实会互换,即:存活的对象,会从 eden 和 s1 区,向 s0 区移动。然后再把 eden 和 s1 区中的垃圾清除,这一轮完成后,eden 与 s1 区变成空的,如下图。
**对于那些比较“长寿”的对象一直在 s0 与 s1 中挪来挪去,一来很占地方,而且也会造成一定开销,降低 gc 效率,于是有了“代龄(age)”及“晋升”。 ** 对象在新生代的 3 个区(edge,s0,s1)之间,每次从 1 个区移到另 1 区,年龄+1,在 young 区达到一定的年龄阈值后,将晋升到老年代。下图中是 8,即:挪动 8 次后,如果还活着,下次 minor GC 时,将移动到 Tenured 区。
下图是晋升的主要过程:对象先分配在新生代,经过多次 Young GC 后,如果对象还活着,晋升到老年代。
如果老年代,最终也放满了,就会发生 major GC(即 Full GC),由于老年代的的对象通常会比较多,因为标记-清理-整理(压缩)的耗时通常会比较长,会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少 Full GC (STW)的原因。
如果分配的新对象比较大,YGC之后,Eden 区放不下,但是 old 区可以放下时,会直接分配到 old 区(即没有晋升这一过程,直接到老年代了)。
如果分配的新对象比较大,Eden 区放不下,YGC之后发现S(TO)区域放不下,但是 old 区可以放下时,直接会晋升到old区。
如果分配的新对象比较大,Eden 区放不下,YGC之后发现S(TO)区域放得下,但是超过了 pre tenured threshold值 ,直接会晋升到old区。
不算最新出现的神器 ZGC,历史上出现过 7 种经典的垃圾回收器。
这些回收器都是基于分代的,把 G1 除外,按回收的分代划分,横线以上的 3 种:Serial ,ParNew, Parellel Scavenge 都是回收新生代的,横线以下的 3 种:CMS,Serial Old, Parallel Old 都是回收老年代的。
单线程用标记-复制算法,快刀斩乱麻,单线程的好处避免上下文切换,早期的机器,大多是单核,也比较实用。但执行期间,会发生 STW(Stop The World)。
Serial 的多线程版本,同样会 STW,在多核机器上会更适用。
ParNew 的升级版本(注重吞吐量),主要区别在于提供了两个参数: -XX:MaxGCPauseMillis 最大垃圾回收停顿时间(通过牺牲Eden区回收范围来控制时间); -XX:GCTimeRatio 垃圾回收时间与总时间占比(主要是通过预先计算回收时间的平均值进行预测相关执行时间),通过这2个参数,可以适当控制回收的节奏,更关注于吞吐率,即总时间与垃圾回收时间的比例。充分利用CPU的执行时间,提高吞吐效率。但是回收时间不应当最快和最短
因为老年代的对象通常比较多,占用的空间通常也会更大,如果采用复制算法,得留 50%的空间用于复制,相当不划算,而且因为对象多,从 1 个区,复制到另 1 个区,耗时也会比较长,所以老年代的收集,通常会采用“标记-整理”法。从名字就可以看出来,这是单线程(串行)的, 依然会有 STW。
一句话:Serial Old 的多线程版本。
全称:Concurrent Mark Sweep,从名字上看,就能猜出它是并发多线程的。这是 JDK 7 中广泛使用的收集器,有必要多说一下,借一张网友的图说话:
相对 Serial Old 收集器或 Parallel Old 收集器而言,这个明显要复杂多了,分为 4 个阶段:
1)Inital Mark 初始标记:主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
2)Concurrent Mark 并发标记:根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
3)Remark 再标志:为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
4)Concurrent Sweep:并行清理,这里使用多线程以“Mark Sweep-标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。
试想下,高铁上的垃圾清理员,从车厢一头开始吆喝“有需要扔垃圾的乘客,请把垃圾扔一下”,一边工作一边向前走,等走到车厢另一头时,刚才走过的位置上,可能又有乘客产生了新的空瓶垃圾。所以,要完全把这个车厢清理干净的话,她应该喊一下:所有乘客不要再扔垃圾了(STW),然后把新产生的垃圾收走。
当然,因为刚才已经把收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。
等等,刚才我们不是提到过“标记清理”法,会留下很多内存碎片吗?确实,但是也没办法,如果换成“Mark Compact 标记-整理”法,把垃圾清理后,剩下的对象也顺便排整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了。
虽然仍不完美,但是从这 4 步的处理过程来看,以往收集器中最让人诟病的长时间 STW,通过上述设计,被分解成二次短暂的 STW,所以从总体效果上看,应用在 GC 期间卡顿的情况会大大改善,这也是 CMS 一度十分流行的重要原因。
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然会有新垃圾产生,这部分垃圾得标记过程之后,所以CMS无法在当收集中处理掉他们,只好留待下一次GC清理掉,这一部分垃圾称为浮动垃圾。
在jdk1.5默认设置下,CMS收集器当老年代使用了68%的空间就会被激活,可以通过-XX:CMSInitialOccupancyFraction的值来提高触发百分比,在jdk1.6中CMS启动阈值提升到了92%,要是CMS运行期间预留的内存无法满足程序的需要,就会出现”Concurrent Mode Failure“,然后降级临时启用Serial Old收集器进行老年代的垃圾收集,这样停顿时间就很长了。(当存在并发清除的时候,业务线程还可以产生来及,结果造成CMS回收的时候,无法区存储过大新生对象内存快,导致出现CMF)
所以-XX:CMSInitialOccupancyFraction设置太高容易导致大量”Concurrent Mode Failure“。
CMS是一款基于“标记-清除”算法实现的,所以会产生空间碎片。为了解决这个问题,CMS提供了-XX:UseCMSCompactAtFullCollection开发参数用于开启内存碎片的合并整理,由于内存整理是无法并行的,所以停顿时间会变长。还有-XX:CMSFullGCBeforeCompaction,这个参数用于设置多少次不压缩Full GC后,跟着来一次带压缩的(默认为0)。
CMS默认启动的回收线程数是(cpu数量+3)/4。所以CPU数量少会导致用户程序执行速度降低较多。这就是它会降低吞吐量的原因之一。
G1 的全称是 Garbage-First。鉴于 CMS 的一些不足之外,比如: 老年代内存碎片化,STW 时间虽然已经改善了很多,但是仍然有提升空间。G1 就横空出世了,它对于 heap 区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。
如下图,G1 将 heap 内存区,划分为一个个大小相等(1-32M,2 的 n 次方)、内存连续的 Region 区域,每个 region 都对应 Eden、Survivor 、Old、Humongous 四种角色之一,但是 region 与 region 之间不要求连续。
注:Humongous,简称 H 区是专用于存放超大对象的区域,通常>= 1/2 Region Size,且只有 Full GC 阶段,才会回收 H 区,避免了频繁扫描、复制/移动大对象。
所有的垃圾回收,都是基于 1 个个 region 的。JVM 内部知道,哪些 region 的对象最少(即:该区域最空),总是会优先收集这些 region(因为对象少,内存相对较空,肯定快),这也是 Garbage-First 得名的由来,G 即是 Garbage 的缩写, 1 即 First。
理论上讲,只要有一个 Empty Region(空区域),就可以进行垃圾回收。
由于region 与 region 之间并不要求连续,而使用 G1 的场景通常是大内存,比如 64G 甚至更大,为了提高扫描根对象和标记的效率,G1 使用了二个新的辅助存储结构:
Remembered Sets:简称 RSets,用于根据每个 region 里的对象,是从哪指向过来的(即:谁引用了我),每个 Region 都有独立的 RSets。(Other Region -> Self Region)。
Collection Sets :简称 CSets,记录了等待回收的 Region 集合,GC 时这些 Region 中的对象会被回收(copied or moved)。
RSets的引入,在YGC时,将新生代 Region 的 RSets 做为根对象,可以避免扫描老年代的 region,能大大减轻 GC 的负担。注:在老年代收集 Mixed GC 时,RSets 记录了 Old->Old 的引用,也可以避免扫描所有 Old 区。
也称为 Old Generation Collection
按 oracle 官网文档描述分为 5 个阶段:Initial Mark(STW) -> Root Region Scan -> Cocurrent Marking -> Remark(STW) -> Copying/Cleanup(STW && Concurrent)
注:也有很多文章会把 Root Region Scan 省略掉,合并到 Initial Mark 里,变成 4 个阶段。
存活对象的“初始标记”依赖于 Young GC,GC 日志中会记录成 young 字样。
2019-06-09T15:24:37.086+0800: 500993.392: [GC pause (G1 Evacuation Pause) (young), 0.0493588 secs] [Parallel Time: 41.9 ms, GC Workers: 8] [GC Worker Start (ms): Min: 500993393.7, Avg: 500993393.7, Max: 500993393.7, Diff: 0.1] [Ext Root Scanning (ms): Min: 1.5, Avg: 2.2, Max: 4.4, Diff: 2.8, Sum: 17.2] [Update RS (ms): Min: 15.8, Avg: 18.1, Max: 18.9, Diff: 3.1, Sum: 144.8] [Processed Buffers: Min: 110, Avg: 144.9, Max: 163, Diff: 53, Sum: 1159] [Scan RS (ms): Min: 4.7, Avg: 5.0, Max: 5.1, Diff: 0.4, Sum: 39.7] [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Object Copy (ms): Min: 16.4, Avg: 16.5, Max: 16.6, Diff: 0.2, Sum: 132.0] [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Termination Attempts: Min: 1, Avg: 4.9, Max: 7, Diff: 6, Sum: 39] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3] [GC Worker Total (ms): Min: 41.7, Avg: 41.8, Max: 41.8, Diff: 0.1, Sum: 334.1] [GC Worker End (ms): Min: 500993435.5, Avg: 500993435.5, Max: 500993435.5, Diff: 0.0] [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.2 ms] [Other: 7.2 ms] [Choose CSet: 0.0 ms] [Ref Proc: 4.3 ms] [Ref Enq: 0.1 ms] [Redirty Cards: 0.1 ms] [Humongous Register: 0.1 ms] [Humongous Reclaim: 0.1 ms] [Free CSet: 0.6 ms] [Eden: 1340.0M(1340.0M)->0.0B(548.0M) Survivors: 40.0M->64.0M Heap: 2868.2M(12.0G)->1499.8M(12.0G)] [Times: user=0.35 sys=0.00, real=0.05 secs]
并发标记过程中,如果发现某些 region 全是空的,会被直接清除。
进入重新标记阶段。
并发复制/清查阶段。这个阶段,Young 区和 Old 区的对象有可能会被同时清理。GC 日志中,会记录为 mixed 字段,这也是 G1 的老年代收集,也称为 Mixed GC 的原因。
2019-06-09T15:24:23.959+0800: 500980.265: [GC pause (G1 Evacuation Pause) (mixed), 0.0885388 secs] [Parallel Time: 74.2 ms, GC Workers: 8] [GC Worker Start (ms): Min: 500980270.6, Avg: 500980270.6, Max: 500980270.6, Diff: 0.1] [Ext Root Scanning (ms): Min: 1.7, Avg: 2.2, Max: 4.1, Diff: 2.4, Sum: 17.3] [Update RS (ms): Min: 11.7, Avg: 13.7, Max: 14.3, Diff: 2.6, Sum: 109.8] [Processed Buffers: Min: 136, Avg: 141.5, Max: 152, Diff: 16, Sum: 1132] [Scan RS (ms): Min: 42.5, Avg: 42.9, Max: 43.1, Diff: 0.5, Sum: 343.1] [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [Object Copy (ms): Min: 14.9, Avg: 15.2, Max: 15.4, Diff: 0.5, Sum: 121.7] [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [Termination Attempts: Min: 1, Avg: 8.2, Max: 11, Diff: 10, Sum: 66] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2] [GC Worker Total (ms): Min: 74.0, Avg: 74.0, Max: 74.1, Diff: 0.1, Sum: 592.3] [GC Worker End (ms): Min: 500980344.6, Avg: 500980344.6, Max: 500980344.6, Diff: 0.0] [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.5 ms] [Other: 13.9 ms] [Choose CSet: 4.1 ms] [Ref Proc: 1.8 ms] [Ref Enq: 0.1 ms] [Redirty Cards: 0.2 ms] [Humongous Register: 0.1 ms] [Humongous Reclaim: 0.1 ms] [Free CSet: 5.6 ms] [Eden: 584.0M(584.0M)->0.0B(576.0M) Survivors: 28.0M->36.0M Heap: 4749.3M(12.0G)->2930.0M(12.0G)] [Times: user=0.61 sys=0.00, real=0.09 secs]
通过这几个阶段的分析,虽然看上去很多阶段仍然会发生 STW,但是 G1 提供了一个预测模型,通过统计方法,根据历史数据来预测本次收集,需要选择多少个 Region 来回收,尽量满足用户的预期停顿值(-XX:MaxGCPauseMillis 参数可指定预期停顿值)。
注:如果 Mixed GC 仍然效果不理想,跟不上新对象分配内存的需求,会使用 Serial Old GC(Full GC)强制收集整个 Heap。
小结:与 CMS 相比,G1 有内存整理过程(标记-压缩),避免了内存碎片;STW 时间可控(能预测 GC 停顿时间)。
上述内容就是Java中JVM内存布局的GC原理是怎样的,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。