细说jvm(五)、垃圾回收器入门

简介: 细说jvm(五)、垃圾回收器入门

接下来会用几篇的功夫来讲讲垃圾回收器,这块是个比较重要的地方,我也会在垃圾回收器这部分内容讲关于GC的优化,在涉及到CMS以及G1的时候篇幅会比较大,因为这是现在最常用的垃圾回收器,我得多讲点才能对你有所帮助。

我的文章里总共会讲到Serial,Serial Old,Parallel Scavenge,Parallel Old,ParNew,CMS,G1,以及ZGC这些垃圾回收器,用的多的我会细讲,用的少的我就只会说说工作过程。所以本篇先只说Serial,Serial Old,Parallel Scavenge,Parallel Old,ParNew这五款回收器,CMS和G1和ZGC我会单独用文章来说。


Serial 和 Serial old


从Serial这个单词我们可以理解出,这个垃圾回收器在运行的时候,它必须要暂停其他的工作线程直到它收集结束,另外这个玩意是单线程的,是针对新生代的回收器,老年代则是使用Serial old,老年代和新生代采取的算法也不相同,新生代使用的是复制算法,而老年代使用的是标记整理算法。这个收集器在单cpu的工作条件下表现会比较优秀,因为避免了线程上下文切换带来的开销。


ParNew


这个垃圾回收器可以说是Serial 的多线程版本,也是一款并行收集器。


并行和并发的区别是:并发是指的是和用户线程一起运行,即并发过程不会暂停用户线程,但是并行是需要暂停用户线程的,也就
是说,ParNew在GC的时候是需要STW的。


除过多线程之外,这个收集器其他行为和Serial可以说是完全一样的(我自己就一直是这么理解的)。我们可以使用-XX:ParallelGCThreads来调整进行垃圾回收的线程数量,值得注意的是,垃圾回收的线程的数量绝对不是越多越好,越多的线程只会增大线程上下文切换的开销。另外,ParNew是针对新生代的收集器,使用ParNew的时候,老年代需要另外选择其他的收集器,可以是Serial或者是CMS,一般会选择CMS。


Parallel 和 Parallel Old


Parallel的全称是Parallel Scavenge,这玩意是一个针对新生代的回收器,与之对应的Parallel Old则是针对老年代的回收器。在jdk1.8中,这两个回收器是一套的,什么意思呢?就是当我们用了-XX:+UseParallelGC之后就会自动的在老年代给我们搭配上Parallel Old。事实上即使在jdk1.8之前,能和Parallel Scavenge搭配的老年代回收器也只有Serial Old和Parallel Old。Parallel是不能和CMS搭配使用的,这个原因是因为Parallel和CMS使用了不同的jvm分代框架。


为什么不能和CMS搭配使用?这里看R大在这个链接里的回答 https://hllvm-group.iteye.com/group/topic/27629#post-199089


Parallel的这两个回收器是属于并行收集器,它们设计的目标是为了尽可能的提高吞吐量。所谓吞吐量,指的是运行用户代码和总的运行时间的比值,总的运行时间包含垃圾回收时间和运行用户代码的时间。算法的使用上,新生代的Parallel Scavenge使用的是复制算法,老年代的Parallel Old则使用的是标记整理算法。Parallel提供了两个参数-XX:MaxGCPauseMillis和-XX:GCTimeRatio以便于我们可以更精准的控制吞吐量,我们一个一个的来看一下它们的用法。

-XX:MaxGCPauseMillis

这个参数的值是一个毫秒的值,意思是所允许每次的最大停顿时间,如果设置了这个值的话,jvm将尽力满足我们的这个要求。但是千万别以为这个值是越小越好的,因为太小的停顿往往意味着牺牲了吞吐量,具体原因我们在本文的最后一部分说。

-XX:GCTimeRatio

这个参数的值一个0到100的整数,意思是所允许的垃圾回收时间占总的程序运行时间的比例,默认是99,意思就是最大允许百分之一的垃圾回收时间。

除过这两个参数之外,我们还可以设置自适应的策略,-XX:+UseAdaptiveSizePolicy,这个参数设置之后,我们就不需要再设置新生代大小(-Xmn),以及eden和survivor的比例(-XX:SurvivorRatio)以及晋升到老年代对象大小(-XX:PretenureSizeThreshold)等细节的参数了。

关于Parallel收集器,我们再多说说它的内存分配策略和悲观策略,因为这点也是它很不一样的地方。

内存分配策略

常规的收集器在当年轻代放不下的时候,往往出触发一次young gc,但是到了Parallel这里,有一些特殊,具体分为两点,一是当整个新生代放不下某个对象的时候,这个对象会直接进入老年代,另一方面是当整个新生代都可以放下但只是eden的空间不够时,则尝试young一次。我们使用代码来证明刚才说的这两个点是对的,来看看代码


jvm参数,证明第一点用
//-Xms100m
//-Xmx100m
//-Xmn50m
//-XX:SurvivorRatio=8
//-verbose:gc              // 和printGCDetails这个参数的作用是一样的
//-XX:+PrintGCDetails
//-XX:+PrintGCDateStamps
//-XX:+PrintHeapAtGC       //每次gc前后打印堆内存空间使用的情况
//-XX:+UseParallelOldGC
public static void main(String[] args) {
      byte[][] use = new byte[7][];
      use[0] = allocM(10);
      use[1] = allocM(10);
      use[2] = allocM(10);
      use[3] = allocM(20);
}
private static byte[] allocM(int n) {
      return new byte[1024 * 1024 * n];
}


输出结果如下:


1686814731883.png


我一点一点解释下,在main方法的最后一行代码这里,分配了20M空间给新的对象,此时其实新生代eden的空间已经不够了,但是并没有发生GC,而是直接将对象分配在了老年代。输出结果的图这个只是程序运行结束时候的内存使用情况,我们可以清楚的看出来根本就没有发生GC,另外我们可以看到,老年代被使用了20480K,这正好是我们最后分配的对象的大小,可能会有同学奇怪为什么新生代被使用的大小大于30M,这个是因为在堆内存中还有着一些常量,它们占据了额外的内存,因此新生代大小是大于30M的,注意哈,这些常量也是受GC约束的。到这里第一点就证明了。我们再来看看第二点,这时候,我们保持参数不变,把上面的代码改成下面这个样子:


public static void main(String[] args) throws InterruptedException {
    allocM(10);
    allocM(10);
    allocM(10);
    allocM(10);
}
private static byte[] allocM(int n) {
    return new byte[1024 * 1024 * n];
}


可以看到输出如下:


1686814720515.png


在main方法的最后一行代码中,我们分配了10M的空间给新的对象,此时eden已经被使用了30M,剩下eden的空间明显不足以分配新对象,但是此时Survivor的两个区域(共10M,注意eden和两个Survivor区域加起来的和是新生代)还没有被使用,满足第二个点的条件,于是触发了一次young gc,也有人叫minor gc。


minor gc == young gc ,都是针对新生代的垃圾回收


悲观策略

这个策略具体指的是:在执行Young GC之后,如果预计下次晋升老年代的平均大小,比当前老年代的剩余空间要大的话,则会触发一次Full GC。


其他收集器没有这个策略,其他收集器仅仅是在执行young gc之前,如果估计之前晋升老年代的平均大小,比当前老年代的剩余空间要大的话,则会放弃young gc,转而触发full gc,Parallel不仅仅有这个,还多了上面的悲观策略


我们依然使用代码证明下,保持上面的参数,仅仅把代码改成如下样子:


public static void main(String[] args) throws InterruptedException {
    byte[][] use = new byte[7][];
    use[0] = allocM(10);
    use[1] = allocM(10);
    use[2] = allocM(10);
    System.out.println("要发生GC了。。。。。。。");
    use[3] = allocM(10);
}
private static byte[] allocM(int n) {
    return new byte[1024 * 1024 * n];
}


输出结果如下


1686814711987.png


大家不要被这么长的日志吓到,这都是纸老虎而已(后边我还会教大家用可视化工具分析GC,所以别怕),其实就是因为我们在每次gc的前后打印了堆内存空间的使用情况而已,所以显得日志很长。我们可以看到,最后一次分配对象之前发生了两次GC,其中第一次是young gc,第二次是full gc,young gc这个还顺便证明了上面说过的第二点,但是在young gc之后,发现之前晋升到老年代的大小已经大于当前老年代剩下的空间了,所以触发了一次full gc,注意蓝色方框内的字Ergonomics而不是Allocation Failure,这个意味这这次full gc并不是由于分配失败引起的,而是由于jvm自身的机制引起的。

画外音:用Parallel 这两个收集器的时候full gc可能比你想象中的会多一些


jvm不可能三角之间的矛盾


jvm的内存大容量,以及低停顿和吞吐量之间这三者是个不可能三角,这个的意思就是很难同时把三点全部都做的非常好,内存的大容量,意味着垃圾回收器就必须回收更多的空间,这就必须造成更大的停顿时间,而为了缩小停顿时间,就必须并发执行,并发执行,增加了线程上下文切换带来的开销,于是吞吐量就会被降低。我们一般来说更关注的是延迟,因为很难去忍受应用有个一两秒的停顿(这个在2G以上堆内存的时候发生full gc尤其容易有这样时间的停顿)。你可以想象一下,我们的服务是一个很长链路中的一部分,然后我们的应用由于full gc停了一秒多,如果是高并发的情况下,上游很快就会堆积很多请求。另外就是这和用户体验是息息相关的,用户是很难以忍受我们的应用频繁卡顿的,你想如果双十一抢东西的时候页面卡顿那用户的心情简直可以美如画,然后把你喷的体无完肤最后还会附上一句看看人家系统都不卡的致命嘲讽。

目录
相关文章
|
4月前
|
存储 算法 Oracle
极致八股文之JVM垃圾回收器G1&ZGC详解
本文作者分享了一些垃圾回收器的执行过程,希望给大家参考。
|
1月前
|
监控 算法 Java
Java虚拟机(JVM)的垃圾回收机制深度解析####
本文深入探讨了Java虚拟机(JVM)的垃圾回收机制,旨在揭示其背后的工作原理与优化策略。我们将从垃圾回收的基本概念入手,逐步剖析标记-清除、复制算法、标记-整理等主流垃圾回收算法的原理与实现细节。通过对比不同算法的优缺点及适用场景,为开发者提供优化Java应用性能与内存管理的实践指南。 ####
|
22天前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
28 0
|
19天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
21天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
25天前
|
机器学习/深度学习 监控 算法
Java虚拟机(JVM)的垃圾回收机制深度剖析####
本文深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法、性能调优策略及未来趋势。通过实例解析,为开发者提供优化Java应用性能的思路与方法。 ####
36 1
|
28天前
|
监控 算法 Java
Java虚拟机垃圾回收机制深度剖析与优化策略####
【10月更文挑战第21天】 本文旨在深入探讨Java虚拟机(JVM)中的垃圾回收机制,揭示其工作原理、常见算法及参数调优技巧。通过案例分析,展示如何根据应用特性调整GC策略,以提升Java应用的性能和稳定性,为开发者提供实战中的优化指南。 ####
41 5
|
1月前
|
存储 算法 安全
JVM常见面试题(四):垃圾回收
堆区域划分,对象什么时候可以被垃圾器回收,如何定位垃圾——引用计数法、可达性分析算法,JVM垃圾回收算法——标记清除算法、标记整理算法、复制算法、分代回收算法;JVM垃圾回收器——串行、并行、CMS垃圾回收器、G1垃圾回收器;强引用、软引用、弱引用、虚引用
|
28天前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
1月前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
54 1