作者:javacodegeeks
起源:http://www.importnew.com/1582…
在 Plumbr 从事 GC 暂停检测相干性能的工作时,我被迫用本人的形式,通过大量文章、书籍和演讲来介绍我所做的工作。在整个过程中,常常对 Minor、Major、和 Full GC 事件的应用感到困惑。这也是我写这篇博客的起因,我心愿能分明地解释这其中的一些纳闷。
文章要求读者相熟 JVM 内置的通用垃圾回收准则。 堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,代假如和其余不同的 GC 算法超出了本文探讨的范畴。
Minor GC
从年老代空间(包含 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于了解。然而,当产生 Minor GC 事件的时候,有一些乏味的中央须要留神到:
1、当 JVM 无奈为一个新的对象调配空间时会触发 Minor GC,比方当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
2、内存池被填满的时候,其中的内容全副会被复制,指针会从 0 开始跟踪闲暇内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所应用内存池的顶部。
3、执行 Minor GC 操作时,不会影响到永恒代。从永恒代到年老代的援用被当成 GC roots,从年老代到永恒代的援用在标记阶段被间接疏忽掉。
4、 质疑惯例的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,进行应用程序的线程。 对于大部分应用程序,进展导致的提早都是能够忽略不计的。其中的假相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相同,Eden 区大部分新生对象不合乎 GC 条件,Minor GC 执行时暂停的工夫将会长很多。
所以 Minor GC 的状况就相当分明了—— 每次 Minor GC 会清理年老代的内存。
Major GC vs Full GC
大家应该留神到,目前,这些术语无论是在 JVM 标准还是在垃圾收集钻研论文中都没有正式的定义。然而咱们一看就晓得这些在咱们曾经晓得的根底之上做出的定义是正确的,Minor GC 清理年老带内存应该被设计得简略:
- Major GC 是清理老年代。
- Full GC 是清理整个堆空间—包含年老代和老年代。
很可怜,实际上它还有点简单且令人困惑。首先,许多 Major GC 是由 Minor GC 触发的,所以很多状况下将这两种 GC 拆散是不太可能的。另一方面,许多古代垃圾收集机制会清理局部永恒代空间,所以应用“cleaning”一词只是局部正确。
这使得咱们不必去关怀到底是叫 Major GC 还是 Full GC,大家应该关注以后的 GC 是否进行了所有应用程序的线程,还是可能并发的解决而不必停掉应用程序的线程。
这种凌乱甚至内置到 JVM 规范工具。上面一个例子很好的解释了我的意思。让咱们比拟两个不同的工具 Concurrent Mark 和 Sweep collector (-XX:+UseConcMarkSweepGC) 在 JVM 中运行时输入的跟踪记录。
第一次尝试通过 jstat 输入:
my-precious: me$ jstat -gc -t 4235 1sTime 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 毫秒。通过 jconsole 或者 jvisualvm 这样的基于 GUI 的工具你能失去同样的后果。
java -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC eu.plumbr.demo.GarbageProducer3.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 在永恒代中不同阶段运行了两次:
1、最后的标记阶段,用了 0.0041705 秒也就是 4ms 左右。这个阶段会暂停“全世界(stop-the-world)”的事件,进行所有应用程序的线程,而后开始标记。
2、并行执行标记和荡涤阶段。这些都是和应用程序线程并行的。
3、最初 Remark 阶段,破费了 0.0462010 秒约 46ms。这个阶段会再次暂停所有的事件。
4、并行执行清理操作。正如其名,此阶段也是并行的,不会进行其余线程。
所以,正如咱们从垃圾回收日志中所看到的那样,实际上只是执行了 Major GC 去清理老年代空间而已,而不是执行了两次 Full GC。
如果你是前期做决 定的话,那么由 jstat 提供的数据会疏导你做出正确的决策。它正确列出的两个暂停所有事件的状况,导致所有线程进行了共计 50ms。然而如果你试图优化吞吐量,你会被误导的。清 单只列出了回收初始标记和最终 Remark 阶段,jstat 的输入看不到那些并发实现的工作。
论断
思考到这种状况,最好防止以 Minor、Major、Full GC 这种形式来思考问题。而应该监控利用提早或者吞吐量,而后将 GC 事件和后果分割起来。
随着这些 GC 事件的产生,你须要额定的关注某些信息,GC 事件是强制所有应用程序线程进行了还是并行的解决了局部事件。
近期热文举荐:
1.1,000+ 道 Java 面试题及答案整顿 (2021 最新版)
2. 别在再满屏的 if/ else 了,试试策略模式,真香!!
3. 卧槽!Java 中的 xx ≠ null 是什么新语法?
4.Spring Boot 2.5 重磅公布,光明模式太炸了!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!