关于java:java面试官最爱问的垃圾回收机制这位阿里P7大佬分析的属实到位

9次阅读

共计 6528 个字符,预计需要花费 17 分钟才能阅读完成。

前言

  • JVM 内存模型一共包含三个局部:

    • 堆 (Java 代码可及的 Java 堆 和 JVM 本身应用的办法区)、
    • 栈 (服务 Java 办法的虚拟机栈 和 服务 Native 办法的本地办法栈)
    • 保障程序在多线程环境下可能间断执行的程序计数器

特地地,咱们过后就提到 Java 堆是进行垃圾回收的次要区域,故其也被称为 GC 堆;而办法区也有一个不太谨严的表述,就是永恒代。总的来说,堆 (包含 Java 堆 和 办法区)是 垃圾回收的次要对象,特地是 Java 堆。

实际上,Java 技术体系中所提倡的 主动内存治理 最终能够归结为自动化地解决了两个问题:给对象分配内存 以及回收调配给对象的内存,而且这两个问题针对的内存区域就是 Java 内存模型中的堆区。对于对象分配内存问题,笔者的博文《JVM 内存模型概述》曾经论述了 如何划分可用空间及其波及到的线程平安问题,本文将联合垃圾回收策略进一步给出 内存调配规定。另外,咱们晓得垃圾回收机制是 Java 语言一个显著的特点,其能够无效的避免内存泄露、保障内存的无效应用,从而使得 Java 程序员在编写程序的时候不再须要思考内存治理问题。Java 垃圾回收机制要思考的问题很简单,本文论述了其三个外围问题

包含:

  • 那些内存须要回收?(对象是否能够被回收的两种经典算法: 援用计数法 和 可达性剖析算法)
  • 什么时候回收?(堆的新生代、老年代、永恒代的垃圾回收机会,MinorGC 和 FullGC)
  • 如何回收?(三种经典垃圾回收算法 (标记革除算法、复制算法、标记整顿算法) 及分代收集算法 和 七种垃圾收集器)

在探讨 Java 垃圾回收机制之前,咱们首先应该记住一个单词:Stop-the-World。Stop-the-world 意味着
JVM 因为要执行 GC 而进行了应用程序的执行,并且这种情景会在任何一种 GC 算法中产生。当 Stop-the-world 产生时,除了 GC 所需的线程以外,所有线程都处于期待状态直到 GC 工作实现。事实上,GC 优化很多时候就是指缩小 Stop-the-world 产生的工夫,从而使零碎具备
高吞吐、低进展 的特点
Ps: 内存泄露是指该内存空间应用结束之后未回收,在不波及简单数据结构的个别状况下,Java 的内存泄露体现为一个内存对象的生命周期超出了程序须要它的工夫长度。

如何确定一个对象是否能够被回收?

援用计数算法:判断对象的援用数量

  • 援用计数算法是通过判断对象的援用数量来决定对象是否能够被回收。

援用计数算法是垃圾收集器中的晚期策略。在这种办法中,堆中的每个对象实例都有一个援用计数。

  • 当一个对象被创立时,且将该对象实例调配给一个援用变量,该对象实例的援用计数设置为 1。
  • 当任何其它变量被赋值为这个对象的援用时,对象实例的援用计数加 1(a = b,则 b 援用的对象实例的计数器加 1),
  • 但当一个对象实例的某个援用超过了生命周期或者被设置为一个新值时,对象实例的援用计数减 1。
  • 特地地,当一个对象实例被垃圾收集时,它援用的任何对象实例的援用计数器均减 1。任何援用计数为 0 的对象实例能够被当作垃圾收集。

援用计数收集器能够很快的执行,并且交错在程序运行中,对程序须要不被长时间打断的实时环境比拟无利,但其很难解决对象之间互相循环援用的问题。如上面的程序和示意图所示,对象 objA 和 objB 之间的援用计数永远不可能为 0,那么这两个对象就永远不能被回收。

public class ReferenceCountingGC {

        public Object instance = null;

        public static void testGC(){ReferenceCountingGC objA = new ReferenceCountingGC ();
            ReferenceCountingGC objB = new ReferenceCountingGC ();

            // 对象之间互相循环援用,对象 objA 和 objB 之间的援用计数永远不可能为 0
            objB.instance = objA;
            objA.instance = objB;

            objA = null;
            objB = null;

            System.gc();}

上述代码最初面两句将 objA 和 objB 赋值为 null,也就是说 objA 和 objB 指向的对象曾经不可能再被拜访,然而因为它们相互援用对方,导致它们的援用计数器都不为 0,那么垃圾收集器就永远不会回收它们。

可达性剖析算法:判断对象的援用链是否可达

  • 可达性剖析算法是通过判断对象的援用链是否可达来决定对象是否能够被回收。

可达性剖析算法是从离散数学中的图论引入的,程序把所有的援用关系看作一张图,通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜寻,搜寻所走过的门路称为援用链(Reference Chain)。当一个对象到 GC Roots 没有任何援用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证实此对象是不可用的,如下图所示。在 Java 中,可作为 GC Root 的对象包含以下几种:

  • 虚拟机栈 (栈帧中的局部变量表) 中援用的对象;
  • 办法区中类动态属性援用的对象;
  • 办法区中常量援用的对象;
  • 本地办法栈中 Native 办法援用的对象;

垃圾收集算法

标记革除算法

标记 - 革除算法分为标记和革除两个阶段。该算法首先从根汇合进行扫描,对存活的对象对象标记,标记结束后,再扫描整个空间中未被标记的对象并进行回收,如下图所示。

标记 - 革除算法的次要有余有两个:

效率问题:标记和革除两个过程的效率都不高;

空间问题:标记 - 革除算法不须要进行对象的挪动,并且仅对不存活的对象进行解决,因而标记革除之后会产生大量不间断的内存碎片,空间碎片太多可能会导致当前在程序运行过程中须要调配较大对象时,无奈找到足够的间断内存而不得不提前触发另一次垃圾收集动作。

![](https://upload-images.jianshu.io/upload_images/23140115-f9a1ccdd3cc7bdcf?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块下面,而后再把已应用过的内存空间一次清理掉。这种算法实用于对象存活率低的场景,比方新生代。这样使得每次都是对整个半区进行内存回收,内存调配时也就不必思考内存碎片等简单状况,只有挪动堆顶指针,按程序分配内存即可,实现简略,运行高效。该算法示意图如下所示:

事实上,当初商用的虚拟机都采纳这种算法来回收新生代。因为钻研发现,新生代中的对象每次回收都基本上只有 10% 左右的对象存活,所以须要复制的对象很少,效率还不错。正如在博文《JVM 内存模型概述》中介绍的那样,实际中会将新生代内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间 (如下图所示),每次应用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次地复制到另外一块 Survivor 空间上,最初清理掉 Eden 和方才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90% (80%+10%),只有 10% 的内存会被“节约”。

标记整顿算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更要害的是,如果不想节约 50% 的空间,就须要有额定的空间进行调配担保,以应答被应用的内存中所有对象都 100% 存活的极其状况,所以在老年代个别不能间接选用这种算法。标记整顿算法的标记过程相似标记革除算法,但后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理掉端边界以外的内存,相似于磁盘整顿的过程,该垃圾回收算法实用于对象存活率高的场景(老年代),其作用原理如下图所示。

标记整顿算法与标记革除算法最显著的区别是:标记革除算法不进行对象的挪动,并且仅对不存活的对象进行解决;而标记整顿算法会将所有的存活对象挪动到一端,并对不存活对象进行解决,因而其不会产生内存碎片。标记整顿算法的作用示意图如下:

分代收集算法

对于一个大型的零碎,当创立的对象和办法变量比拟多时,堆内存中的对象也会比拟多,如果逐个剖析对象是否该回收,那么势必造成效率低下。
分代收集算法是基于这样一个事实:不同的对象的生命周期 (存活状况) 是不一样的,而不同生命周期的对象位于堆中不同的区域,因而对堆内存不同区域采纳不同的策略进行回收能够进步 JVM 的执行效率。
当代商用虚拟机应用的都是分代收集算法:新生代对象存活率低,就采纳复制算法;老年代存活率高,就用标记革除算法或者标记整顿算法。Java 堆内存个别能够分为新生代、老年代和永恒代三个模块,如下图所示:

新生代(Young Generation)

新生代的指标就是尽可能疾速的收集掉那些生命周期短的对象,个别状况下,所有新生成的对象首先都是放在新生代的
新生代内存依照 8:1:1 的比例分为一个 eden 区和两个 survivor(survivor0,survivor1) 区,大部分对象在 Eden 区中生成。在进行垃圾回收时,先将 eden 区存活对象复制到 survivor0 区,而后清空 eden 区,当这个 survivor0 区也满了时,则将 eden 区和 survivor0 区存活对象复制到 survivor1 区,而后清空 eden 和这个 survivor0 区,此时 survivor0 区是空的,而后替换 survivor0 区和 survivor1 区的角色(即下次垃圾回收时会扫描 Eden 区和 survivor1 区),即放弃 survivor0 区为空,如此往返。特地地,当 survivor1 区也不足以寄存 eden 区和 survivor0 区的存活对象时,就将存活对象间接寄存到老年代。
如果老年代也满了,就会触发一次 FullGC,也就是新生代、老年代都进行回收。留神,新生代产生的 GC 也叫做 MinorGC,MinorGC 产生频率比拟高,不肯定等 Eden 区满了才触发。

老年代(Old Generation)

老年代寄存的都是一些生命周期较长的对象,就像下面所叙述的那样,在新生代中经验了 N 次垃圾回收后依然存活的对象就会被放到老年代中。
此外,老年代的内存也比新生代大很多(大略比例是 1:2),当老年代满时会触发 Major GC(Full GC),老年代对象存活工夫比拟长,因而 FullGC 产生的频率比拟低。

永恒代(Permanent Generation)

永恒代次要用于寄存动态文件,如 Java 类、办法等。
永恒代对垃圾回收没有显著影响,然而有些利用可能动静生成或者调用一些 class,例如应用反射、动静代理、CGLib 等 bytecode 框架时,在这种时候须要设置一个比拟大的永恒代空间来寄存这些运行过程中新增的类。

小结

因为对象进行了分代解决,因而垃圾回收区域、工夫也不一样。垃圾回收有两种类型,Minor GC 和 Full GC

GC:

对新生代进行回收,不会影响到年轻代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 十分频繁,个别在这里应用速度快、效率高的算法,使垃圾回收能尽快实现。

Full GC:

也叫 Major GC,对整个堆进行回收,包含新生代、老年代和永恒代。因为 Full GC 须要对整个堆进行回收,所以比 Minor GC 要慢,因而应该尽可能减少 Full GC 的次数,导致 Full GC 的起因包含:老年代被写满、永恒代(Perm)被写满和 System.gc()被显式调用等。

垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展现了 7 种作用于不同分代的收集器,其中用于回收新生代的收集器包含 Serial、PraNew、Parallel Scavenge,回收老年代的收集器包含 Serial Old、Parallel Old、CMS,还有用于回收整个 Java 堆的 G1 收集器。不同收集器之间的连线示意它们能够搭配应用。

Serial 收集器(复制算法):

  • 新生代单线程收集器,标记和清理都是单线程,长处是简略高效;

Serial Old 收集器 (标记 - 整顿算法):

  • 老年代单线程收集器,Serial 收集器的老年代版本;

ParNew 收集器 (复制算法):

  • 新生代收并行集器,实际上是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的体现;

Parallel Scavenge 收集器 (复制算法):

  • 新生代并行收集器,谋求高吞吐量,高效利用 CPU。吞吐量 = 用户线程工夫 /(用户线程工夫 +GC 线程工夫),高吞吐量能够高效率的利用 CPU 工夫,尽快实现程序的运算工作,适宜后盾利用等对交互相应要求不高的场景;

Parallel Old 收集器 (标记 - 整顿算法):

  • 老年代并行收集器,吞吐量优先,Parallel Scavenge 收集器的老年代版本;

CMS(Concurrent Mark Sweep)收集器(标记 - 革除算法):

  • 老年代并行收集器,以获取最短回收进展工夫为指标的收集器,具备高并发、低进展的特点,谋求最短 GC 回收进展工夫。

G1(Garbage First)收集器 (标记 - 整顿算法):

  • Java 堆并行收集器,G1 收集器是 JDK1.7 提供的一个新收集器,G1 收集器基于“标记 - 整顿”算法实现,也就是说不会产生内存碎片。此外,G1 收集器不同于之前的收集器的一个重要特点是:G1 回收的范畴是整个 Java 堆(包含新生代,老年代),而前六种收集器回收的范畴仅限于新生代或老年代。

内存调配与回收策略

Java 技术体系中所提倡的主动内存治理最终能够归结为自动化地解决了两个问题:给对象分配内存 以及 回收调配给对象的内存。一般而言,对象次要调配在新生代的 Eden 区上,如果启动了本地线程调配缓存(TLAB),将按线程优先在 TLAB 上调配。多数状况下也可能间接调配在老年代中。总的来说,内存调配规定并不是一层不变的,其细节取决于以后应用的是哪一种垃圾收集器组合,还有虚拟机中与内存相干的参数的设置。

对象优先在 Eden 调配,当 Eden 区没有足够空间进行调配时,虚拟机将发动一次 MinorGC。

当初的商业虚拟机个别都采纳复制算法来回收新生代,将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次应用 Eden 和其中一块 Survivor。当进行垃圾回收时,将 Eden 和 Survivor 中还存活的对象一次性地复制到另外一块 Survivor 空间上,最初解决掉 Eden 和方才的 Survivor 空间。(HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1)当 Survivor 空间不够用时,须要依赖老年代进行调配担保。

大对象间接进入老年代。

所谓的大对象是指,须要大量间断内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。

长期存活的对象将进入老年代。

当对象在新生代中经验过肯定次数(默认为 15)的 Minor GC 后,就会被降职到老年代中。

动静对象年龄断定。

为了更好地适应不同程序的内存情况,虚拟机并不是永远地要求对象年龄必须达到了 MaxTenuringThreshold 能力降职老年代,如果在 Survivor 空间中雷同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就能够间接进入老年代,毋庸等到 MaxTenuringThreshold 中要求的年龄。

须要留神的是,Java 的垃圾回收机制是 Java 虚拟机提供的能力,用于在闲暇工夫以不定时的形式动静回收无任何援用的对象占据的内存空间。也就是说,垃圾收集器回收的是无任何援用的对象占据的内存空间而不是对象自身

总结

大家看完有什么不懂的能够在下方留言探讨,也能够关注我私信问我,我看到后都会答复的。也欢送大家关注我的公众号:前程有光,马上金九银十跳槽面试季,整顿了 1000 多道将近 500 多页 pdf 文档的 Java 面试题材料放在外面,助你圆梦 BAT!文章都会在外面更新,整顿的材料也会放在外面。谢谢你的观看,感觉文章对你有帮忙的话记得关注我点个赞反对一下!

正文完
 0