华为二面:你真的明白Java垃圾回收器吗?我用这份笔记狂怼面试官

286 阅读10分钟

今日分享开始啦,请大家多多指教~

第一阶段:串行垃圾回收器:jdk1.3.1之前Java虚拟机仅仅只支持Serial收集器。

第二阶段:并行垃圾回收器:随着多核的出现,Java引入了并行垃圾回收器,充分利用多核性能提升垃圾回收效率。

第三阶段:并发标记清理回收器CMS:垃圾回收器可以和应用程序同时运行,降低暂停用户线程执行的时间。

第四阶段:G1(并发)回收器:初衷是在清理非常大的堆空间的时候能满足特定的暂停应用程序的时间,与CMS相比会有更少的内存碎片。

1 垃圾回收算法

1-1 标记清除算法

算法概述

优点:回收速度快

缺点:造成内存碎片,无法分配大的连续空间。

算法思想

在Java9之前,Java默认使用的垃圾回收器是ParallelGC,从Java9开始G1作为了默认的垃圾回收器

image.png

step1: 第一次扫描,通过GC root对象判断堆内存中哪些对象可以进行垃圾回收,进行标记。

step2: 第二次扫描, 将那些标记的GC root对象进行垃圾回收,只需要将起始内存地址与终止内存地址放入空闲内存区就行。

1-2 标记整理算法

第一个依旧是标记,第二步会进行一个空间整理,从而不产生碎片。

优点:避免了内存碎片

缺点:对空间的整理使得效率比较低下。

image.png

1-3 复制算法

特点:

将管理的内存分为2块区域,from区域与to区域,将那些不需要回收的对象从from区域拷贝到to区域。复制的过程中完成内存区域的整理。之后交换from和to的指向。

优点:不会产生内存碎片

缺点:需要双倍的内存空间,内存利用率不高,而且拷贝也需要时间。

image.png

image.png

image.png

1-4 三种垃圾回收算法总结

image.png

注意:实际的JVM垃圾回收算法中上面的三种算法是综合使用的。

2 JVM分代回收算法

2-1 概述

Garden of Eden:伊甸园 garbage:垃圾

image.png

新生代主要由三部分内容组成,分别是Eden区,幸存区from,幸存区to。 通常情况下只有Eden区与幸存区from会存放数目,幸存区to只有垃圾回收时,复制对象会用到。堆内存的新生代进行一次垃圾回收(Minor GC),大部分对象都会都会被回收。

老年代通常存放一些经常被使用的对象,一个对象如果经历多次垃圾回收仍然幸存,那么该对象会从新生代放入老年代。只有新生代内存不足并且老年代内存也不足的时候才会触发full GC对老年代的对象进行垃圾回收。

为什么需要进行划分?

实际环境中,对象的生命周期是不同的,老年代的对象生命周期比较长,可能很长时间才进行一次垃圾回收。新生代的对象生命周期比较短,垃圾回收比较频繁。这种分区法方便采用不同的垃圾回收算法更加有效地进行垃圾回收。

2-2 分代垃圾回收示例

step1:程序刚刚开始运行,产生的对象先放入Eden区,当Eden区放不下的时候。

step2:对Eden区进行Minor GC,并将没有被垃圾回收的对象复制的幸存区To,然后交换幸存区To和幸存区From,第一次垃圾回收的最终的效果如下图所示:

image.png

step3: 第一次Minor GC, Eden区又有空间可以分配给新的对象使用,经过一段时间Eden又不够用了,触发第二次Minor GC, 这次垃圾会检查Eden区以及幸存区From哪些对象可以存活,并将这些对象复制到幸存区To,然后交换幸存区To和幸存区From,这个时候Eden区又空了出来,可以放置新的对象。

image.png

实际垃圾回收过程中,JVM会对每个对象经过垃圾回收幸存下来的次数进行记录,比如上图中,幸存区的2个对象经过垃圾回收的次数分别是1和2。

step4: 当一些对象经过垃圾回收的次数仍然幸存的次数达到一个阈值(说明这个对象价值比较高),那么这个对象会被移动到老年代。

image.png

极端情况考虑:Eden区,from区,老年区都已经满了?

image.png

此时会触发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 垃圾回收相关的虚拟机参数

垃圾回收器概述

image.png

2-5 垃圾回收案例分析

情况1:什么都不放的情况

new generation:新生代 tenured generation:老年代

image.png

情况1执行结果

可以看到即使用户没有创建对象,系统对象也要占据一部分堆内存空间。

image.png

Java的内存对象都是分配在堆上吗

情况2:新生代堆空间放满,触发GC

image.png

情况2执行结果

image.png

情况3: 新生代内存随着对象的增多放不下了

image.png

执行结果

新生代放不下,将新生代的对象放置到老年代。

image.png

情况4:一开始直接分配大于新生代的内存,如果老年代放得下,则直接放到老年代

image.png

执行结果

image.png

当内存比较紧张的时候,即新生代内存放不下的时候,有时候会直接将对象分配到老年代,或者直接在回收次数较少(未达到15次)的情况下,直接将新生代对象弄到老年代。

2 垃圾回收器

2-1 垃圾回收器概述

image.png

CMS垃圾回收器后来被G1垃圾回收器取代。

2-2 串行垃圾回收器

开启串行垃圾回收器的JVM参数

-XX:+UseSerialGC = Serial + SerialOld

// Serial:工作在新生代,采用复制的垃圾回收算法

// SerialOld:工作在老生代,采用标记+整理的垃圾回收算法

image.png

总结:触发垃圾回收时,让多个线程在一个安全点停下来,然后使用单线程的垃圾回收器去进行垃圾回收,垃圾回收完成后,再让其他线程运行。

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 //垃圾回收并行的线程数目

image.png

总结:采用多线程方式进行垃圾回收,垃圾回收的线程数目通常根据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

// 在重新标记前,对新生代进行垃圾回收,减少并发清理的垃圾对象,+开启,-关闭

image.png

浮动垃圾是指并发清理过程中用户线程新产生的垃圾,需要等待下次并发清理。

并发工作流程概述:

step1:老年代发生内存不存的现象。

step2:ConcMarkSweepGC会进行一个初始标记动作(初始标记需要STW即阻塞非垃圾回收线程),初始标记只标记根对象,所以速度非常快,暂停时间也非常短。

step3:完成初始标记后,之前阻塞的线程又可以运行了,这个时候垃圾回收线程进行并发标记。

step4:并发标记结束后,需要再次阻塞非垃圾回收线程,进行一个所谓的重新标记,

step5:重新标记完成后,阻塞的线程又可以运行了。垃圾回收线程也并发的清理垃圾对象。

总结:初始标记与重新标记需要阻塞线程。 在并发阶段,由于垃圾回收线程占用资源,所以系统的吞吐量会受到一定的影响,但是系统的响应速度由于并发执行不会受到垃圾回收的明显影响(相比较其他垃圾回收器,STW时间只需要进行初始标记与重新标记,并且能够不阻塞其他线程进行垃圾的标记与清除)。

今日份分享已结束,请大家多多包涵和指点!