写点什么

☕【JVM 技术之旅】彻底弄清楚 Minor GC 和 Major GC 及 Full GC

发布于: 2021 年 05 月 22 日
☕【JVM技术之旅】彻底弄清楚Minor GC和Major GC及Full GC

每日一句


每一日你所付出的代价都比前一日高,因为你的生命又消短了一天,所以每一日你都要更用心。

前提概要

对于 JVM 而言,最难能够掌握就是 GC 回收部分的研究和探索。而对于虚拟机而言根据不同的区域以及范围和方案分为 Minor GC、Major GC 和 Full GC 等,此处暂时不描述 Mixed GC,后续章节会详细做专题探究。

GC 的引入


堆内存划分为 Eden、Survivor(2)和 Tenured/Old 空间。

发生在年轻代的 GC——Minor GC



其中 Minor GC 如下图所示

  • 虚拟机给每个对象定义一个对象年龄(Age)计数器。对象在 Eden 生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。

  • 对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。

  • 对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold 来设置



从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。


  • JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。

    内存池被填满的时候,其中的内容全部会被复制,指针会从 0 开始跟踪空闲内存。

    Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。

    所以 Eden 和 Survivor 区不存在内存碎片,写指针总是停留在所使用内存池的顶部。

  • 执行 Minor GC 操作时,不会影响到方法区从方法区到年轻代的引用被当成 GCRoots,从年轻代到方法区的引用在标记阶段被直接忽略掉

  • 质疑常规的认知,所有的 Minor GC 都会触发 “全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。

    其中的真相就是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。

  • 如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。

Minor GC 触发机制:

当年轻代满时就会触发 Minor GC,这里的年轻代满指的是 Eden 代满,Survivor 满不会引发 GC。


发生在老年代的 GC——Major GC/Full GC

  • Major GC:主要是清理老年代:MajorGC 的速度一般会比 Minor GC 慢 10 倍以上

  • Full GC:主要是清理整个堆空间—包括年轻代和老年代。

    首先,许多 Major GC 是由 Minor GC 触发,所以很多情况下将这两种 GC 分离是不太可能的。

    另一方面,许多现代垃圾收集机制会清理部分老年代空间,所以使用“cleaning”一词只是部分正确。


这使得我们不用去关心到底是叫 Major GC 还是 Full GC,大家应该关注当前的 GC 是否停止了所有应用程序的线程,还是能够并发的处理而不用停掉应用程序的线程。

老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。

Full GC 触发机制:

  • (1)调用 System.gc 时,系统建议执行 Full GC,但是不必然执行

  • (2)老年代空间不足

  • (3)方法区空间不足:当永久代/元数据空间满时也会引发 Full GC,会导致 Class、Method 元信息的卸载。

  • (4)通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存

  • (5)由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小


Full GC 定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8 以上版本取代 perm gen)的全局范围的 GC

Minor GC 和 Major GC 是俗称,在 Hotspot JVM 实现的 Serial GC, Parallel GC, CMS, G1 GC 中大致可以对应到某个 Young GC 和 Old GC 算法组合


第一次尝试通过 jstat 输出:

my-precious: me$ jstat -gc -t 4235 1s复制代码
复制代码


Time S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT    5.7 34048.0 34048.0  0.0   34048.0 272640.0 194699.7 1756416.0   181419.9  18304.0 17865.1 2688.0 2497.6      3    0.275   0      0.000    0.275 6.7 34048.0 34048.0 34048.0  0.0   272640.0 247555.4 1756416.0   263447.9  18816.0 18123.3 2688.0 2523.1      4    0.359   0      0.000    0.359 7.7 34048.0 34048.0  0.0   34048.0 272640.0 257729.3 1756416.0   345109.8  19072.0 18396.6 2688.0 2550.3      5    0.451   0      0.000    0.451 8.7 34048.0 34048.0 34048.0 34048.0 272640.0 272640.0 1756416.0  444982.5  19456.0 18681.3 2816.0 2575.8      7    0.550   0      0.000    0.550 9.7 34048.0 34048.0 34046.7  0.0   272640.0 16777.0  1756416.0   587906.3  20096.0 19235.1 2944.0 2631.8      8    0.720   0      0.000    0.72010.7 34048.0 34048.0  0.0   34046.2 272640.0 80171.6  1756416.0   664913.4  20352.0 19495.9 2944.0 2657.4      9    0.810   0      0.000    0.81011.7 34048.0 34048.0 34048.0  0.0   272640.0 129480.8 1756416.0   745100.2  20608.0 19704.5 2944.0 2678.4     10    0.896   0      0.000    0.89612.7 34048.0 34048.0  0.0   34046.6 272640.0 164070.7 1756416.0   822073.7  20992.0 19937.1 3072.0 2702.8     11    0.978   0      0.000    0.97813.7 34048.0 34048.0 34048.0  0.0   272640.0 211949.9 1756416.0   897364.4  21248.0 20179.6 3072.0 2728.1     12    1.087   1      0.004    1.09114.7 34048.0 34048.0  0.0   34047.1 272640.0 245801.5 1756416.0   597362.6  21504.0 20390.6 3072.0 2750.3     13    1.183   2      0.050    1.23315.7 34048.0 34048.0  0.0   34048.0 272640.0 21474.1  1756416.0   757347.0  22012.0 20792.0 3200.0 2791.0     15    1.336   2      0.050    1.38616.7 34048.0 34048.0 34047.0  0.0   272640.0 48378.0  1756416.0   838594.4  22268.0 21003.5 3200.0 2813.2     16    1.433   2      0.050    1.484
复制代码

这个片段是 JVM 启动后第 17 秒提取的。基于该信息,我们可以得出这样的结果,运行了 12 次 Minor GC、2 次 Full GC,时间总跨度为 50 毫秒。

java -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC eu.plumbr.demo.GarbageProducer

3.157: [GC (Allocation Failure) 3.157: [ParNew: 272640K->34048K(306688K), 0.0844702 secs] 272640K->69574K(2063104K), 0.0845560 secs] [Times: user=0.23 sys=0.03, real=0.09 secs] 4.092: [GC (Allocation Failure) 4.092: [ParNew: 306688K->34048K(306688K), 0.1013723 secs] 342214K->136584K(2063104K), 0.1014307 secs] [Times: user=0.25 sys=0.05, real=0.10 secs] ... cut for brevity ...11.292: [GC (Allocation Failure) 11.292: [ParNew: 306686K->34048K(306688K), 0.0857219 secs] 971599K->779148K(2063104K), 0.0857875 secs] [Times: user=0.26 sys=0.04, real=0.09 secs] 12.140: [GC (Allocation Failure) 12.140: [ParNew: 306688K->34046K(306688K), 0.0821774 secs] 1051788K->856120K(2063104K), 0.0822400 secs] [Times: user=0.25 sys=0.03, real=0.08 secs] 12.989: [GC (Allocation Failure) 12.989: [ParNew: 306686K->34048K(306688K), 0.1086667 secs] 1128760K->931412K(2063104K), 0.1087416 secs] [Times: user=0.24 sys=0.04, real=0.11 secs] 13.098: [GC (CMS Initial Mark) [1 CMS-initial-mark: 897364K(1756416K)] 936667K(2063104K), 0.0041705 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 13.102: [CMS-concurrent-mark-start]13.341: [CMS-concurrent-mark: 0.238/0.238 secs] [Times: user=0.36 sys=0.01, real=0.24 secs] 13.341: [CMS-concurrent-preclean-start]13.350: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 13.350: [CMS-concurrent-abortable-preclean-start]13.878: [GC (Allocation Failure) 13.878: [ParNew: 306688K->34047K(306688K), 0.0960456 secs] 1204052K->1010638K(2063104K), 0.0961542 secs] [Times: user=0.29 sys=0.04, real=0.09 secs] 14.366: [CMS-concurrent-abortable-preclean: 0.917/1.016 secs] [Times: user=2.22 sys=0.07, real=1.01 secs] 14.366: [GC (CMS Final Remark) [YG occupancy: 182593 K (306688 K)]14.366: [Rescan (parallel) , 0.0291598 secs]14.395: [weak refs processing, 0.0000232 secs]14.395: [class unloading, 0.0117661 secs]14.407: [scrub symbol table, 0.0015323 secs]14.409: [scrub string table, 0.0003221 secs][1 CMS-remark: 976591K(1756416K)] 1159184K(2063104K), 0.0462010 secs] [Times: user=0.14 sys=0.00, real=0.05 secs] 14.412: [CMS-concurrent-sweep-start]14.633: [CMS-concurrent-sweep: 0.221/0.221 secs] [Times: user=0.37 sys=0.00, real=0.22 secs] 14.633: [CMS-concurrent-reset-start]14.636: [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
复制代码

在点头同意这个结论之前,让我们看看来自同一个 JVM 启动收集的垃圾收集日志的输出。显然- XX : + PrintGCDetails 告诉我们一个不同且更详细的故事:

基于这些信息,我们可以看到 12 次 Minor GC 后开始有些和上面不一样了。没有运行两次 Full GC,这不同的地方在于单个 GC 在永久代中不同阶段运行了两次:

最初的标记阶段,用了 0.0041705 秒也就是 4ms 左右。这个阶段会暂停“全世界( stop-the-world)”的事件,停止所有应用程序的线程,然后开始标记。

并行执行标记和清洗阶段。这些都是和应用程序线程并行的。 最后 Remark 阶段,花费了 0.0462010 秒约 46ms。这个阶段会再次暂停所有的事件。 并行执行清理操作。正如其名,此阶段也是并行的,不会停止其他线程。

所以,正如我们从垃圾回收日志中所看到的那样,实际上只是执行了 Major GC 去清理老年代空间而已,而不是执行了两次 Full GC。

用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论 (1 条评论)

发布
用户头像
以上图片都是来源于本人掘金账号
2021 年 05 月 22 日 15:29
回复
没有更多了
☕【JVM技术之旅】彻底弄清楚Minor GC和Major GC及Full GC