BAT面试题,Java GC(垃圾回收机制),一线大厂面试必问

Java
216
0
0
2024-01-06
标签   Java面试

1.什么是GC?

GC是 垃圾收集 的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃, Java 提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。 Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。

2.需要GC的区域

定义 :Java 虚拟机 在执行java程序的时候,会将它管理的内存划分为若干个数据区域

类型 :分为 线程 共享区和线程私有区

线程共享区 :所有线程都可使用,且只有一份

线程私有区 :每一个线程都有单独属于自己的内存区域

在 JVM 中,主要内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈

直接内存 :直接内存就是没有被虚拟机化的操作系统上的其他内存,所以直接内存不属于运行时数据区的一部分,但是会被频繁使用,就好比,我们的系统有8个G的内存,其中有3个G被JVM虚化了,所以还剩下5个G,而这5个G的内存是由JVM借助了一些工具来使用的,这个内存部分称之为直接内存。

如下图

BAT面试题,Java GC(垃圾回收机制),一线大厂面试必问

3. 堆内存 的结构

Java堆划分

为更好回收内存,或更快分配内存,需要对Java堆进行划分:

(I)从垃圾收集器的角度来看

JVM规范没有规定JVM如何实现垃圾收集器;

由于很多JVM采用分代收集 算法 ,所以Java堆还可以细分为:新生代、老年代和永久代;

BAT面试题,Java GC(垃圾回收机制),一线大厂面试必问

(II)从内存分配角度来看

为解决分配内存线程不安全问题,需要同步处理;

Java堆可能划分出 每个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB) ,减少线程同步;

HotSpot VM通过”-XX:+/-UseTLAB”指定是否使用TLAB;

4.堆内存上对象的分配与回收:


堆内存上的对象回收也叫做垃圾回收,那么垃圾回收什么时候开始呢?

垃圾回收主要是完成 清理对象,整理内存 的工作。上面说到GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代。新生代还分为一个Eden区和两个Survivor区。垃圾回收分为年轻代区域发生的Minor GC和老年代区域发生的Full GC,分别介绍如下。

Minor GC(年轻代GC):

对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。

Full GC(老年代GC):

Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。

动态对象年龄判定:

如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么 年龄大于等于该对象年龄的对象即可晋升 到老年代,不必要等到-XX:MaxTenuringThreshold。

空间分配担保:

发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则进行一次 Full GC(老年代GC) ,如果小于,则查看 HandlePromotionFailure设置是否允许担保失败 ,如果允许,那只会进行一次 Minor GC ,如果不允许,则改为进行一次 Full GC

5.目前会问到的问题

1.年轻代三个区比例

Eden,S0,S1比例8:1:1

2.为什么要有Survivor区

我们知道,目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代;新生代又被划分成Eden空间、 From Survivor 和 To Survivor 三块区域。

看书的时候有个疑问,为什么非得是两个Survivor空间呢?要回答这个问题,其实等价于:为什么不是0个或1个 Survivor 空间?为什么2个 Survivor 空间可以达到要求?

为什么不是0个 Survivor 空间?

这个问题等价于:为什么需要Survivor空间。我们看看如果没有 Survivor 空间的话,垃圾收集将会怎样进行:一遍新生代 gc 过后,不管三七二十一,活着的对象全部进入老年代,即便它在接下来的几次 gc 过程中极有可能被回收掉。这样的话老年代很快被填满, Full GC 的频率大大增加。我们知道,老年代一般都会被规划成比新生代大很多,对它进行垃圾收集会消耗比较长的时间;如果收集的频率又很快的话,那就更糟糕了。基于这种考虑,虚拟机引进了“幸存区”的概念:如果对象在某次新生代 gc 之后仍然存活,让它暂时进入幸存区;以后每熬过一次 gc ,让对象的年龄+1,直到其年龄达到某个设定的值(比如15岁), JVM 认为它很有可能是个“老不死的”对象,再呆在幸存区没有必要(而且老是在两个幸存区之间反复地复制也需要消耗资源),才会把它转移到老年代。

总之,设置Survivor空间的目的是让那些中等寿命的对象尽量在 Minor GC 时被干掉,最终在总体上减少虚拟机的垃圾收集过程对用户程序的影响。

为什么不是1个 Survivor 空间?

回答这个问题有一个前提,就是新生代一般都采用复制算法进行垃圾收集。原始的复制算法是把一块内存一分为二,gc是把存活的对象从一块空间(From space)复制到另外一块空间(To space),再把原先的那块内存(From space)清理干净,最后调换 From space 和 To space 的逻辑角色(这样下一次 gc 的时候还可以按这样的方式进行)。

我们知道,在HotSpot虚拟机里, Eden 空间和 Survivor 空间默认的比例是 8:1 。我们来看看在只有一个 Survivor 空间的情况下,这个 8:1 会有什么问题。此处为了方便说明,我们假设新生代一共为 9 MB 。对象优先在 Eden 区分配,当 Eden 空间满 8 MB 时,触发第一次 Minor GC 。比如说有 0.5 MB 的对象存活,那这 0.5 MB 的对象将由 Eden 区向 Survivor 区复制。这次 Minor GC 过后, Eden 区被清理干净, Survivor 区被占用了 0.5 MB ,还剩 0.5 MB 。到这里一切都很美好,但问题马上就来了:从现在开始所有对象将会在这剩下的 0.5 MB 的空间上被分配,很快就会发现空间不足,于是只好触发下一次 Minor GC 。可以看出在这种情况下,当 Survivor 空间作为对象“出生地”的时候,很容易触发 Minor GC ,这种 8:1 的不对称分配不但没能在总体上降低 Minor GC 的频率,还会把 gc 的时间间隔搞得很不平均。把 Eden : Survivor 设成 1 : 1 也一样,每当对象总大小满 5 MB 的时候都必须触发一次 Minor GC ,唯一的变化是 gc 的时间间隔相对平均了。

上面的论述都是以“新生代使用复制算法”这个既定事实作为前提来讨论的。如果不是这样,比如说新生代采用“标记-清除”或者“标记-整理”算法来实现幸存对象的移动,好像确实是只需要一个 Survivor 就够了。至于主流的虚拟机实现为什么不考虑采用这种方式,我也不是很清楚,或许有实现难度、内存碎片或者执行效率方面的考虑吧。

为什么2个 Survivor 空间可以达到要求?

问题很清楚了,无论Eden和 Survivor 的比例怎么设置,在只有一个 Survivor 的情况下,总体上看在新生代空间满一半的时候就会触发一次 Minor GC 。那有没有提升的空间呢?比如说永远在新生代空间满 80% 的时候才触发 Minor GC ?

事实上是可以做到的:我们可以设两个Survivor空间( From Survivor 和 To Survivor )。比如,我们把 Eden : From Survivor : To Survivor 空间大小设成 8 : 1 : 1 ,对象总是在 Eden 区出生, From Survivor 保存当前的幸存对象, To Survivor 为空。一次 gc 发生后:

1)Eden 去活着的对象 + From Survivor 存储的对象被复制到 To Survivor ;

2)清空 Eden 和 From Survivor ;

3)颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。

可以看出,只有在Eden空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以降低。当然,使用两个 Survivor 这种方式我们也付出了一定的代价,如 10% 的空间浪费、复制对象的开销等。

总结

根据分析得知,当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的,所以这也是为什么 Survivor 分区是 2 个的原因了。

6. JVM如何判定一个对象是否应该被回收?(重点掌握)

垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。

一般有两种方法来判断:

引用计数器法 :为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;

可达性分析算法 :从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

以下对象会被认为是root对象:

  • 栈内存中引用的对象
  • 方法区中静态引用和常量引用指向的对象
  • 被启动类(bootstrap加载器)加载的类和创建的对象
  • Native方法中JNI引用的对象。

7. JVM垃圾回收算法有哪些?

HotSpot 虚拟机采用了 可达性分析 来进行内存回收,

标记-清除算法 :标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。

复制算法 :按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。

标记-整理算法 :标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

分代算法 :根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

标记-清除算法(Mark-Sweep):

标记无用对象,然后进行清除回收。

标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:

  • 标记阶段:标记出可以回收的对象。
  • 清除阶段:回收被标记的对象所占用的空间。

标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。

优点 :实现简单,不需要对象进行移动。

缺点 :标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。

标记-清除算法的执行的过程如下图所示

BAT面试题,Java GC(垃圾回收机制),一线大厂面试必问

复制算法

复制算法把内存空间分成两个同样的区域,每次就用他们中的一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象copy到另外一个区域中。复制算法每次只处理正在使用中的对象,所以复制成本比较小,而且复制过去依旧能继续进行相应的内存整理,不会出现“碎片”问题。不过,该方法问题比较突出,就是需要两倍内存空间。

优点 :按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。

缺点 可用内存缩成了一半,浪费空间

BAT面试题,Java GC(垃圾回收机制),一线大厂面试必问

标记-整理算法:

标记-整理算法结合了 “标记-清除”和“复制” 两个算法的优点。也是分两阶段,

第一阶段从根节点开始标记所有被引用对象,

第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

BAT面试题,Java GC(垃圾回收机制),一线大厂面试必问

分代收集算法

当前商业虚拟机都采用 分代收集 的垃圾收集算法。分代收集算法,顾名思义是根据对象的 存活周期 将内存划分 为几块。 一般包括年轻代、老年代 和 永久代 如图所示

BAT面试题,Java GC(垃圾回收机制),一线大厂面试必问

8.垃圾收集器(掌握CMS和G1)

JVM中的垃圾收集器主要包括7种,即 Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old以及CMS,G1收集器 。如下图所示:


BAT面试题,Java GC(垃圾回收机制),一线大厂面试必问

1、Serial收集器:

Serial收集器是 一个单线程的垃圾收集器 ,并且在执行垃圾回收的时候需要 Stop The World 。虚拟机运行在 Client模式 下的默认新生代收集器。Serial收集器的优点是简单高效,对于限定在单个CPU环境来说,Serial收集器没有 多线程 交互的开销。

2、Serial Old收集器:

Serial Old是Serial收集器的老年代版本,也是 一个单线程收集器 。主要也是给在Client模式下的虚拟机使用。在Server模式下存在主要是作为CMS垃圾收集器的后备预案, 当CMS并发收集发生Concurrent Mode Failure时使用。

3、ParNew收集器:

ParNew是Serial收集器的 多线程 版本,新生代是并行的(多线程的),老年代是串行的(单线程的),新生代采用复制算法,老年代采用标记整理算法。可以使用参数: -XX:UseParNewGC使用该收集器,使用 -XX:ParallelGCThreads可以限制线程数量。

4、Parallel Scavenge垃圾收集器:

Parallel Scavenge是一种新生代收集器,使用复制算法的收集器,而且是 并行的多线程收集器。 Paralle收集器特点是更加关注吞吐量(吞吐量就是cpu用于运行用户代码的时间与cpu总消耗时间的比值)。可以通过 -XX:MaxGCPauseMillis参数控制最大垃圾收集停顿时间;通过-XX:GCTimeRatio参数直接设置吞吐量大小;通过-XX:+UseAdaptiveSizePolicy参数可以打开GC自适应调节策略, 该参数打开之后虚拟机会根据系统的运行情况收集性能监控信息,动态调整虚拟机参数以提供最合适的停顿时间或者最大的吞吐量。 自适应调节策略 是Parallel Scavenge收集器和ParNew的主要区别之一。

5、Parallel Old收集器:

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

6、CMS(Concurrent Mark Sweep)收集器(并发标记清除)

CMS收集器是 一种以获取最短回收停顿时间为目标 的收集器。CMS收集器是基于 标记-清除算法 实现的,是一种老年代收集器,通常与 ParNew 一起使用。

CMS的垃圾收集过程分为4步:

初始标记 :需要“Stop the World”,初始标记仅仅只是标记一下GC Root能直接关联到的对象,速度很快。

并发标记 :是主要标记过程,这个标记过程是和用户线程并发执行的。

重新标记 :需要“Stop the World”,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(停顿时间比初始标记长,但比并发标记短得多)。

并发清除 :和用户线程并发执行的,基于标记结果来清理对象。

BAT面试题,Java GC(垃圾回收机制),一线大厂面试必问

那么问题来了, 如果在重新标记之前刚好发生了一次MinorGC,会不会导致重新标记阶段Stop the World时间太长?

答:不会的,在并发标记阶段其实还包括了一次并发的 预清理阶段 ,虚拟机会主动 等待年轻代发生垃圾回收 ,这样可以将重新标记对象引用关系的步骤放在并发标记阶段,有效降低重新标记阶段Stop The World的时间。

CMS垃圾回收器的优缺点分析:

CMS以降低垃圾回收的停顿时间为目的,很显然其具有并发收集,停顿时间低的优点。

缺点主要包括如下:

对CPU资源非常敏感 ,因为并发标记和并发清理阶段和用户线程一起运行,当CPU数变小时,性能容易出现问题。

收集过程中会产生 浮动垃圾 ,所以不可以在老年代内存不够用了才进行垃圾回收,必须提前进行垃圾收集。通过参数 -XX:CMSInitiatingOccupancyFraction 的值来控制内存使用百分比。如果该值设置得太高,那么在CMS运行期间预留的内存可能无法满足程序所需,会出现 Concurrent Mode Failure失败,之后会临时使用Serial Old收集器作为老年代收集器 ,会产生更长时间的停顿。

标记-清除方式会产生内存碎片 ,可以使用参数 -XX:UseCMSCompactAtFullCollection 来控制是否开启内存整理(无法并发,默认是开启的)。参数 -XX:CMSFullGCsBeforeCompaction 用于设置执行多少次不压缩的Full GC后进行一次带压缩的内存碎片整理(默认值是0)。

接下来,我们先看下上边介绍的浮动垃圾是怎么产生的吧。

浮动垃圾:

由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了 “Floating Garbage” ,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以, 并发收集器一般需要20%的预留空间 用于这些浮动垃圾。

7、G1(Garbage-First)收集器:

G1收集器将新生代和老年代取消了,取而代之的是 将堆划分为若干的区域 ,每个区域都可以根据需要扮演新生代的Eden和Survivor区或者老年代空间,仍然属于分代收集器,区域的一部分包含新生代,新生代采用复制算法,老年代采用标记-整理算法。

通过 将JVM堆分为一个个的区域(region) ,G1收集器可以避免在Java堆中进行全区域的垃圾收集。G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次 根据回收时间来优先回收价值最大的region。

G1收集器的特点:

并行与并发 :G1能充分利用多CPU,多核环境下的硬件优势,来缩短Stop the World,是并发的收集器。

分代收集 :G1不需要其他收集器就能独立管理整个GC堆,能够采用不同的方式去处理新建对象、存活一段时间的对象和熬过多次GC的对象。

空间整合 :G1从整体来看是基于标记-整理算法,从局部(两个Region)上看基于复制算法实现,G1运作期间不会产生内存空间碎片。

可预测的停顿 :能够建立可以预测的停顿时间模型,预测停顿时间。

和CMS收集器类似,G1收集器的垃圾回收工作也分为了四个阶段:

初始标记

并发标记

最终标记

筛选回收

其中,筛选回收阶段首先对各个Region的回收价值和成本进行计算,根据用户期望的GC停顿时间来制定回收计划。