华为二面:你真的明白 Java 垃圾回收器吗?我用这份笔记狂怼面试官
今日分享开始啦,请大家多多指教~
第一阶段:串行垃圾回收器:jdk1.3.1 之前 Java 虚拟机仅仅只支持 Serial 收集器。
第二阶段:并行垃圾回收器:随着多核的出现,Java 引入了并行垃圾回收器,充分利用多核性能提升垃圾回收效率。
第三阶段:并发标记清理回收器 CMS:垃圾回收器可以和应用程序同时运行,降低暂停用户线程执行的时间。
第四阶段:G1(并发)回收器:初衷是在清理非常大的堆空间的时候能满足特定的暂停应用程序的时间,与 CMS 相比会有更少的内存碎片。
1 垃圾回收算法
1-1 标记清除算法
算法概述
优点:回收速度快
缺点:造成内存碎片,无法分配大的连续空间。
算法思想
在 Java9 之前,Java 默认使用的垃圾回收器是 ParallelGC,从 Java9 开始 G1 作为了默认的垃圾回收器
step1: 第一次扫描,通过 GC root 对象判断堆内存中哪些对象可以进行垃圾回收,进行标记。
step2: 第二次扫描, 将那些标记的 GC root 对象进行垃圾回收,只需要将起始内存地址与终止内存地址放入空闲内存区就行。
1-2 标记整理算法
第一个依旧是标记,第二步会进行一个空间整理,从而不产生碎片。
优点:避免了内存碎片
缺点:对空间的整理使得效率比较低下。
1-3 复制算法
特点:
将管理的内存分为 2 块区域,from 区域与 to 区域,将那些不需要回收的对象从 from 区域拷贝到 to 区域。复制的过程中完成内存区域的整理。之后交换 from 和 to 的指向。
优点:不会产生内存碎片
缺点:需要双倍的内存空间,内存利用率不高,而且拷贝也需要时间。
1-4 三种垃圾回收算法总结
注意:实际的 JVM 垃圾回收算法中上面的三种算法是综合使用的。
2 JVM 分代回收算法
2-1 概述
Garden of Eden:伊甸园 garbage:垃圾
新生代主要由三部分内容组成,分别是 Eden 区,幸存区 from,幸存区 to。 通常情况下只有 Eden 区与幸存区 from 会存放数目,幸存区 to 只有垃圾回收时,复制对象会用到。堆内存的新生代进行一次垃圾回收(Minor GC),大部分对象都会都会被回收。
老年代通常存放一些经常被使用的对象,一个对象如果经历多次垃圾回收仍然幸存,那么该对象会从新生代放入老年代。只有新生代内存不足并且老年代内存也不足的时候才会触发 full GC 对老年代的对象进行垃圾回收。
为什么需要进行划分?
实际环境中,对象的生命周期是不同的,老年代的对象生命周期比较长,可能很长时间才进行一次垃圾回收。新生代的对象生命周期比较短,垃圾回收比较频繁。这种分区法方便采用不同的垃圾回收算法更加有效地进行垃圾回收。
2-2 分代垃圾回收示例
step1:程序刚刚开始运行,产生的对象先放入 Eden 区,当 Eden 区放不下的时候。
step2:对 Eden 区进行 Minor GC,并将没有被垃圾回收的对象复制的幸存区 To,然后交换幸存区 To 和幸存区 From,第一次垃圾回收的最终的效果如下图所示:
step3: 第一次 Minor GC, Eden 区又有空间可以分配给新的对象使用,经过一段时间 Eden 又不够用了,触发第二次 Minor GC, 这次垃圾会检查 Eden 区以及幸存区 From 哪些对象可以存活,并将这些对象复制到幸存区 To,然后交换幸存区 To 和幸存区 From,这个时候 Eden 区又空了出来,可以放置新的对象。
实际垃圾回收过程中,JVM 会对每个对象经过垃圾回收幸存下来的次数进行记录,比如上图中,幸存区的 2 个对象经过垃圾回收的次数分别是 1 和 2。
step4: 当一些对象经过垃圾回收的次数仍然幸存的次数达到一个阈值(说明这个对象价值比较高),那么这个对象会被移动到老年代。
极端情况考虑:Eden 区,from 区,老年区都已经满了?
此时会触发 Full GC(优先 Minor GC,Minor GC 依旧内存不够)
2-3 分代垃圾回收的总结
对象首先分配在伊甸园区域
新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1 并且交换 from to。
minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行暂停时间较短,由于新生代大部分对象都是垃圾,复制的对象很少,所以效率较高。
当对象寿命超过阈值时,会晋升至老年代,最大寿命是 15(4bit,对象头存储)。
当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW 的时间更长。
Full GC 的 stop the world 的时间要比 MInor GC 时间长,老年代存活对象较多加上空间整理时间,所以停止时间会较长。如果 Full GC 后,空间仍然不足会触发内存不足的异常。
2-4 垃圾回收相关的虚拟机参数
垃圾回收器概述
2-5 垃圾回收案例分析
情况 1:什么都不放的情况
new generation:新生代 tenured generation:老年代
情况 1 执行结果
可以看到即使用户没有创建对象,系统对象也要占据一部分堆内存空间。
Java 的内存对象都是分配在堆上吗
情况 2:新生代堆空间放满,触发 GC
情况 2 执行结果
情况 3: 新生代内存随着对象的增多放不下了
执行结果
新生代放不下,将新生代的对象放置到老年代。
情况 4:一开始直接分配大于新生代的内存,如果老年代放得下,则直接放到老年代
执行结果
当内存比较紧张的时候,即新生代内存放不下的时候,有时候会直接将对象分配到老年代,或者直接在回收次数较少(未达到 15 次)的情况下,直接将新生代对象弄到老年代。
2 垃圾回收器
2-1 垃圾回收器概述
CMS 垃圾回收器后来被 G1 垃圾回收器取代。
2-2 串行垃圾回收器
开启串行垃圾回收器的 JVM 参数
-XX:+UseSerialGC = Serial + SerialOld
// Serial:工作在新生代,采用复制的垃圾回收算法
// SerialOld:工作在老生代,采用标记+整理的垃圾回收算法
总结:触发垃圾回收时,让多个线程在一个安全点停下来,然后使用单线程的垃圾回收器去进行垃圾回收,垃圾回收完成后,再让其他线程运行。
2-3 吞吐量优先的垃圾回收器
开启吞吐量优先的垃圾回收器的 JVM 参数
开启/关闭的参数
默认的多线程垃圾回收器,前者是开启新生代回收器,采用复制算法,后者是开启老年代回收器,采用标记+拷贝算法。下面选项只要开启一个,那么另外一个也会开启。
-XX:+UseParallelGC , -XX:+UseParallelOldGC
开启自适应动态调整新生代的大小,晋升阈值
-XX:+UseAdaptiveSizePolicy
二个指标调整的参数(ParallelGC 会根据设定的指标去调整堆的大小到达下面期望设定的目标)
指标 1)1/(1+ratio) = 垃圾回收的时间/总的运行时间
ratio 默认值时 99,即垃圾回收的时间不超过总时间 1%。但一般设为 19。
如果达不到目标,ParallelGC 会调整堆内存大小来达到这个目标,通常是调大,这样垃圾回收的次数会减少,从而提高吞吐量
-XX:GCTimeRatio=ratio
指标 2)每次垃圾回收的时间限制( 最大暂停的毫秒数)
默认值是 200ms 显然将堆内存空间变小有助于减少每次垃圾回收的时间
-XX:MaxGCPauseMillis=ms
总结:显然指标 1)与指标 2)是有冲突的。
-XX:ParallelGCThreads=n //垃圾回收并行的线程数目
总结:采用多线程方式进行垃圾回收,垃圾回收的线程数目通常根据 CPU 的核数进行设置。在垃圾回收阶段,并行的垃圾回收线程会充分占用 CPU。在非垃圾回收阶段,用户线程会充分利用 CPU 资源。
2-4 响应时间优先的垃圾回收器(CMS 垃圾回收器)
缺点:采用的标记清除算法产生内存碎片需要退化成单线程的垃圾整理回收器,造成响应时间变长。
开启的 JVM 参数
注意这个是并发的采用标记清除算法的垃圾回收,这里区别于之前的垃圾回收器,该垃圾回收器能够在进行垃圾回收的同时运行其他非垃圾回收线程(也存在时间阶段需要停止,但不是所有阶段停止)。
老年代并发的垃圾回收器会出现失败的情况,这时老年代垃圾回收器会退化成单线程的垃圾回收器(SerialOld)
-XX:+UseConcMarkSweepGC // use concurrent mark sweep(会产生垃圾碎片) 工作在老年代的垃圾回收器
-XX:+UseParNewGC // 工作在新生代的垃圾回收器
重要的初始参数
-XX:ParallelGCThreads=n // 并行的垃圾回收线程数,通常等于 CPU 的核心数(垃圾回收并行阶段)
-XX:ConcGCThreads=threads // 并发的线程数目,通常设为并行垃圾回收线程数的 1/4(垃圾回收并发阶段)
其他参数
-XX:CMSInitiatingOccupancyFraction=percent // 执行垃圾回收的内存占比,预留空间给浮动垃圾
-XX:+CMSScavengeBeforeRemark
// 在重新标记前,对新生代进行垃圾回收,减少并发清理的垃圾对象,+开启,-关闭
浮动垃圾是指并发清理过程中用户线程新产生的垃圾,需要等待下次并发清理。
并发工作流程概述:
step1:老年代发生内存不存的现象。
step2:ConcMarkSweepGC 会进行一个初始标记动作(初始标记需要 STW 即阻塞非垃圾回收线程),初始标记只标记根对象,所以速度非常快,暂停时间也非常短。
step3:完成初始标记后,之前阻塞的线程又可以运行了,这个时候垃圾回收线程进行并发标记。
step4:并发标记结束后,需要再次阻塞非垃圾回收线程,进行一个所谓的重新标记,
step5:重新标记完成后,阻塞的线程又可以运行了。垃圾回收线程也并发的清理垃圾对象。
总结:初始标记与重新标记需要阻塞线程。 在并发阶段,由于垃圾回收线程占用资源,所以系统的吞吐量会受到一定的影响,但是系统的响应速度由于并发执行不会受到垃圾回收的明显影响(相比较其他垃圾回收器,STW 时间只需要进行初始标记与重新标记,并且能够不阻塞其他线程进行垃圾的标记与清除)。
今日份分享已结束,请大家多多包涵和指点!
评论 (1 条评论)