前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java后端面试学习知识总结——GC

Java后端面试学习知识总结——GC

作者头像
星如月勿忘初心
发布2020-08-01 15:46:16
3710
发布2020-08-01 15:46:16
举报
文章被收录于专栏:星月小站

垃圾回收算法

JVM之所以能够自动回收内存,是因为JVM的开发人员使用了一些垃圾回收算法,来让JVM自己判断哪些对象可以回收,哪些对象不可以回收。

实际上垃圾收集并不是Java语言或者JVM专属的,垃圾回收算法主要分为两个阶段的算法,分别是标记算法回收算法

标记算法一般用来判断对象是否为垃圾,是否可以回收。

回收算法一般用来回收被标记为垃圾的对象。

二者一般结合使用。

垃圾回收之标记算法

  • 对象被判定为垃圾的标准:没有被其他对象引用。
  • 判定对象是否为垃圾的算法主要有两种:引用计数算法可达性分析算法
  • 关于引用计数法,有一个误会,这个算法虽然效率高,比较简单,但是主流的Java虚拟机并没有采用这个算法,因为需要额外考虑的情况太多,维护成本高。读者可以自行创建两个互相引用对方属性的对象,然后将两个对象置空,会发现对象依然可以被GC回收。下面虽然会介绍引用计数法,但读者需要了解,这个算法并没有被JVM采用。
  • 引用计数算法(Java虚拟机并未采用)
    • 通过判断对象的引用数量来决定对象是否可以被回收。
    • 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1。
    • 任何引用计数为0的对象实例都可以被当做垃圾收集。
    • 优点:执行效率高,程序执行受垃圾回收影响较小。
    • 缺点:无法检测出循环引用的情况,如果像个对象指向互相引用,会造成无法回收,内存泄漏。
  • 可达性分析算法(Java虚拟机的主流标记算法)
    • 通过判断GCRoots根对象的引用链是否可达来决定对象是否可以被回收,如下图所示:
  • Java技术体系中,可以被当做GC Root的对象主要如下:
    • 虚拟机栈中引用的对象(栈帧中的本地变量表)。
    • 方法区中的常量引用的对象。
    • 方法区中类静态属性引用的对象。
    • 本地方法栈中JNI(Native方法)的引用对象。
    • 所有被同步锁(synchronized关键字)持有的对象。

垃圾回收之回收算法

  • 分代收集理论:在介绍垃圾收集算法之前,首先要先说明一下分代收集理论。因为目前的商业虚拟机中的垃圾收集器,大多数都遵循了“分代收集”理论来设计,有时也会被称为分代回收算法/分代收集算法。分代收集虽说是理论,其实是一种经验假说,其建立在两种假说之上:
    • 弱分代假说:绝大多数对象都是朝生夕灭的。
    • 强分代假说:熬过越多次垃圾收集的对象就越难以灭亡。

    根据这两种假说,现代的商业Java虚拟机一般至少将堆内存分成两个部分——新生代老年代两个区域。顾名思义,新生代的对象一般都是刚刚创建的,根据弱分代假说,大多数对象会在短时间内被新生代的垃圾收集器回收掉,而熬过了多次回收的对象,则会进入老年代。 于是Java的垃圾收集就可以每次只针对其中某个区域进行回收,提高回收效率,因而才有了"Minor GC""Major GC""Full GC"等回收类型的划分。也针对不同区域的对象特征发展除了不同的垃圾收集算法——"标记-清除算法""标记-复制算法""标记-整理算法"等针对性的垃圾收集算法。下面将会具体展开介绍,但是读者需要明白,这一切都始于分代收集理论。

  • 标记-清除算法
    • 标记-清除算法是出现最早也是最基础的垃圾收集算法,如同其名字一样,算法首先标记出所有需要回收的对象,然后统一回收掉所有被标记的对象,也可以反过来标记存活的对象,统一回收未被标记的对象。
    • 标记:从跟集合进行扫描,对存活的对象进行标记。
    • 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存。
    • 缺点:1.对象越多执行效率越低。2.标记清除算法会产生内存碎片。
  • 标记-复制算法
    • 标记复制算法将可用内存空间分成对象面空闲面,每次只使用对象面创建对象,当对象面内存用完了,就将对象面中存活的对象复制到控线面中,然后清空对象面,此时空闲面就变成了对象面,对象面就变成了空闲面。
    • 标记-复制算法解决了内存碎片化的问题,而且是顺序分配内存,简单高效。
    • 缺点:这种算法将可用内存减少了一半,空间比较浪费,如果对象存活率高的话就会发生频繁GC的情况,所以适用于对象存活率低的场景。也就是说,不适用于老年代,一般应用于新生代。
  • 标记-整理算法
    • 标记整理算法有些像标记清除算法,但是标记整理算法是移动式回收算法。标记整理算法也是分为两步,先标记存活的对象,然后对存活对象进行整理,移动所有存活对象按照内存地址从内存头部依次排列,最后将末端存活对象以后的内存全部回收。
    • 由于标记整理算法需要移动对象,特别是老年代这种每次回收都有大量存活对象的区域,移动存活对象并更新其引用地址就是一种极为负重的操作,这种更新过程必须暂停全部用户应用程序才能进行,这种停顿也有一个命名叫做"Stop The World"
    • 既然标记整理算法代价这么大,为什么还要用这种方式呢,这是权衡需求与利弊的产物。标记清除算法虽然有可能不需要频繁地停顿用户进程,但是其会产生内存碎片,虚拟机就需要维护一个“分区空闲分配链表”来对空闲内存进行记录,这也影响了系统的效率。根据计算,发现标记清除算法延迟低但是吞吐量低标记整理算法虽然延迟高,但是吞吐量大。根据不同的需求,就可以选择不同的回收算法。
    • 当然也有和稀泥式的解决办法,就是让虚拟机平时大多数时间使用标记清除算法,暂时容忍内存碎片,直到碎片比较严重,影响了对象的内存分配,此时就采用标记整理算法进行一次GC来规整内存空间。后文将要介绍的CMS收集器面对碎片过多时,就采用的这种解决办法。

垃圾回收算法的组合拳:分代收集理论/分代收集算法

  • 分代收集算法,其实就是按照前文所介绍的分代收集理论,将不同的基础收集算法应用到不同分代区域中的组合算法。是垃圾回收算法的组合拳,因地制宜的体现。
  • 主流的JVM中,是按照对象生命周期的不同,依据弱分代假说和强分代假说将内存划分为新生代和老年代区域,这种划分的目的是为了提高JVM的回收效率。本文以HotSpot这一最主流虚拟机为例来讲解其分代收集算法的实现。其堆空间划分如下图所示:
  • Minor GCMajor GCFull GC:前文已经说明分代收集理论是为了分区域针对不同对象特点来进行内存回收,于是就有了这三个概念,分别对应年轻代GC、老年代GC和整堆GC。这里提前明确概念以便后文阅读。
    • Minor GC:年轻代GC,指目标只是新生代的垃圾收集活动。
    • Major GC:老年代GC,指目标只是老年代的垃圾收集活动,目前只有CMS收集器会有这种行为。
    • Full GC:整堆收集,指手机整个Java堆和方法区的垃圾收集活动。
  • 新生代/年轻代
    • 划分出新生代区域,是为了尽可能快速地收集掉那些生命周期短的对象。在HotSpot虚拟机中,新生代又划分为一个Eden区和两个Survivor区。两个Survivor区分别以fromto命名,用来进行基于复制算法的垃圾回收,代表了复制算法中的空闲面和对象面。Eden区和两个Survivor区的内存比例是8:1:1,其内存结构如下图所示:
  • HotSpot虚拟机中,大多数情况下,对象在新生代Eden区中分配(特别大的对象可能直接放入老年代),当Eden区没有足够空间进行分配的时候,就会触发一次Minor GC,来释放新生代的空间。垃圾回收工作时,会将Eden区和一个Survivor区中存活的对象放进另外一个Survivor区,然后清除Eden和第一个Survivor区。当一个对象经历了足够多的Minor GC依然存活的话(默认是15次),就会被放入老年代。
  • 这个垃圾回收步骤和标记复制算法的步骤是一致的,其回收流程如下图:
  • 在新生代GC中,对象有两种可能会晋升到老年代中:
    • 经历一定次数的Minor GC依然存活(默认15次)。
    • 在GC过程中,Eden区存活对象尝试复制到Survivor区,但Survivor区放不下的对象会通过分配担保机制提前转移到老年代去。
  • 老年代
    • 老年代中存放的对象一般都是生命周期较长的对象,还有一部分特别大的对象,因新生代放不下而提前转移至老年代。
    • 由于老年代对象存活率高,对象大,所以一般不使用标记复制算法(过于浪费空间),一般使用标记清除算法和标记整理算法(实际上只有CMS才有只针对老年代的GC)。
  • 各种GC的触发条件:
    • Minor GC:新生代的GC主要发生在Eden区内存满的时候。
    • Major GC:Major GC主要发生在CMS垃圾收集器中,当老年代满的时候会先尝试进行Major GC。
    • Full GC:
      • 非CMS收集器,当老年代满的时候会触发Full GC。
      • 当CMS收集器尝试进行GC时发现有对象Eden区放不下,老年代也放不下,或者有对象想直接进入老年代,但老年代空间不足,此时会进行Full GC。
      • Minor GC之后晋升到老年代的对象平均大小大于老年代剩余的空间,就会触发Full GC。
      • 调用System.gc();函数,可以直接触发Full GC。
  • 常见调优参数:
    • -XX:SurvivorRation : Eden和Survivor的比值 (默认8:1)
    • -XX:NewRation : 老年代和年轻代的内存比例
    • -XX:MaxTenuringThreshold : 对象从年轻代晋升到老年代经过的GC最大次数。

经典的垃圾收集器

  • 前置知识:
    • StopTheWorld:JVM由于要执行GC而暂停用户进程。任何GC算法中都会发生。多数GC优化通过减少StopTheWorld发生的时间来提高程序的性能。
    • SafePoint:分析过程中对象引用关系不会发生变化的点,如方法调用,循环跳转,异常跳转等。GC一般就发生在安全点。
    • JVM的运行模式:JVM分为ServerClient两种模式,前者是重量级JVM,启动慢,但稳定后性能高;后者是轻量级JVM,启动速度快,但长时间运行没有Server模式性能高。
      • Service模式下默认的年轻代收集器是Parallel Scabenge收集器
      • Client模式下默认的年轻代收集器是Serial收集器。
    • 吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
  • HotSpot虚拟机中经典的垃圾收集器 这里介绍的经典收集器是在实践中千锤百炼已经成熟的经典收集器,虽然技术上不是最先进,但是可以在商用生产环境上放心使用的收集器。各经典收集器之间的关系如下图所示:

其中,连线表示两个收集器之间可以组合使用,而一些组合(Serial+CMS和ParNew+Parallel Old)在JDK8中被声明废弃,在JDK9中被取消支持。

年轻代的垃圾收集器:

  • Serial垃圾收集器:
    • Serial垃圾收集器是最基础、历史最悠久的垃圾收集器,是一种单线程的垃圾收集器,采用标记-复制算法进行垃圾收集工作的时候必须暂停所有工作线程,直到收集结束。是Client模式下的JVM默认年轻代垃圾收集器。其垃圾收集逻辑如下图:
    • 优点:简单高效,额外内存消耗最小,单/少核心处理器上运行效率高。
    • 缺点:StopTheWorld时间较长。
  • ParNew收集器:
    • ParNew收集器是Serial收集器的多线程版本,除了是多线程并发进行垃圾回收外,别的行为和Serial收集器完全一样。ParNew收集器的工作过程如下所示:
    • ParNew收集器在单核心处理器的环境下性能绝不会比Serial收集器更好,但是随着可以被使用的处理器核心数增多,ParNew收集器就会越来越高效。
  • Parallel Scavenge收集器:
    • Parallel Scavenge收集器也是一款新生代收集器,是Server模式下JVM默认的新生代收集器,而且是使用了标记-复制算法的多线程的收集器。这样看来与ParNew收集器似乎没有什么不同,实际上Parallel Scavenge收集器的特点是该收集器的性能关注点与其他收集器不一样。
    • 其他收集器的优化目标是放在尽可能缩短垃圾收集期间的用户线程停顿时间,Parallel Scavenge收集器的目标则是更关注系统的吞吐量,使其尽可能达到一个可控制的值。
    • 缩短用户线程停顿时间可以提高用户的使用体验,所以这种优化一般使用在需要与用户交互的架构中,而高吞吐量则可以更高效地利用处理器资源来尽快完成运算任务,主要用于后台运算等不需要太多交互的分析任务中。
    • Parallel Scavenge收集器独有的参数控制:
      • -XX:UseAdaptiveSizePolicy(用来把内存管理调优任务交给JVM自己完成)
      • -XX:MaxGCPauseMillis(用来设置最大垃圾收集停顿时间)
      • -XX:GCTimeRatio(用来直接精确设置吞吐量大小)

老年代的垃圾收集器:

  • Serial Old收集器:
    • Serial收集器的老年代版本,也是单线程收集,进行垃圾收集时,必须暂停所有工作线程。
    • 使用标记-整理算法,这个收集器的主要意义是在Client模式下是使用,是Client模式默认的老年代收集器。如果是在Server模式下,当CMS收集器失败时,Serial Old收集器将作为后备预案。
  • Parallel Old收集器:
    • Parallel Old收集器是Parallel Scavenge收集器的老年代版本,多线程并发收集,基于标记-整理算法实现,也是主要关注系统的吞吐量。
    • Parallel Old经常与Parallel Scavenge一起使用来保证系统的吞吐量。
  • 目标最短回收停顿时间——CMS收集器:
    • CMS收集器是一种以获取最短回收停顿时间为目标的收集器,全称为Concurrent Mark Sweep,从名字可以看出,该收集器基于标记-清除算法,一些官方文档中也称之为并发低停顿收集器。目前主要应用于B/S系统的服务端上,提高用户交互体验。
    • CMS收集器虽然基于标记-清除算法,但其运作过程相较于前面几种收集器来说要复杂一些,主要分为四个步骤:
      • 初始标记(需要StopTheWorld):单线程标记一下GC Roots能够直接关联到的对象,速度很快,但是需要暂停用户线程。
      • 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,此过程耗时较长,但是无需停顿用户线程。
      • 重新标记(需要StopTheWorld):该阶段是为了修正在并发标记期间因用户线程运行而导致标记产生变动的那一部分对象标记记录,重新标记会导致用户线程停顿,比初始标记停顿要长,总体耗时远远小于并发标记阶段。
      • 并发清除:清理删除标记阶段判断为已死亡的对象,由于是标记清除算法,不需要移动对象,所以该阶段也无需停顿用户线程。
      • CMS收集器最耗时的是并发标记和并发清除阶段,这两个阶段是和用户线程一起运行的,所以总体来看,CMS收集器可以认为是和用户线程并发执行的,停顿时间非常短。其运行流程如下图所示:
    • CMS收集器的缺点
      • 对处理器资源非常敏感,核心数较低的处理器使用CMS停顿感可能较强。
      • 无法处理“浮动垃圾”,浮动垃圾是指在并发标记和并发清理阶段,新的垃圾对象还是会一直出现,这一部分只能下一次垃圾收集时再处理,容易引发Full GC降低性能。
      • CMS采用标记清理算法,会产生内存碎片,如果没有足够的连续内存用来存放大对象,就会触发Full GC。
  • G1收集器(Garbage First):
    • G1收集器是一款主要面向服务端应用的垃圾收集器,被开发出来是为了替代CMS收集器,是一种覆盖了新生代和老年代的“全功能垃圾收集器”。JDK9后宣布取代Parallel Scavenge + CMS的垃圾收集器组合,成为了Server模式下默认的垃圾收集器。
    • G1与CMS有许多相似的地方,也是并发和并行一体的垃圾收集器,也是分代收集器,但相对于CMS,G1有了很多的新特点:
      • 可预测的停顿:G1收集器允许用户设定允许的收集停顿期望值(-XX:MaxGCPauseMillis),G1收集器会在期望值以内尽可能收集多的对象。
      • 基于Region的堆内存模型:G1收集器能够面向堆内存任何部分来组成回收集,而不是单独面向新生代或者老年代,因为G1收集器中虽然依然遵循分代理论,但是G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域“Region”。每一个Region都可以根据需求扮演新生代的Eden空间、Survivor空间和老年代空间。收集器根据每个Region的属性来针对性进行回收。其模型图如下:

先进的低延迟垃圾收集器

  • ZGC
    • ZGC全称Z Garbage Collector,是一款在JDK11中加入的具有实验性质的低延迟垃圾收集器,其最初的设计目标是:在尽可能对吞吐量影响不大的情况下,实现在任意堆内存大小下都可以把垃圾回收的停顿时间限制在十毫秒以内的低延迟。
    • ZGC也是基于Region堆内存模型,但是ZGC不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法,以低延迟为首要目标。
    • ZGC的Region模型与G1有一些区别,ZGC的Region具有动态性——动态创建与销毁以及动态的分配区域容量大小。
      • ZGCRegion分成三种区域类型容量:小型Region:容量固定为2MB,用于放置小于256KB的小对象。
      • 中型Region:容量固定为32MB,用于放置大于等于256KB,但是小于4MB的对象。
      • 大型Region:容量不固定,可以动态变化,但是必须是2MB的整数倍。用于放置大于等于4MB的对象。每个大型Region中只会放置一个大对象。

Java的四大引用类型

在Java的GC机制中,判断对象是否为垃圾,主要依靠的还是对象间的引用关系,为了更好地控制对象之间的引用关系强弱程度,所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)软引用(Soft Reference)弱引用(Weak Reference)虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。

Java中的一切都被视为对象,但是我们在编程时直接操作的却是变量,它实际上是对象的一个引用(reference),通过这个引用标识符来指向内存中的某块区域上存储的对象,就可以利用引用来操作对象了。

  • 强引用(Strong Reference):强引用是Java中默认的声明引用强度,当我们使用new关键字来创建对象的时候,变量和对象间就是强引用的关系,如Object o = new Object();这段代码就会在内存中创建一组强引用关系,内存结构如下图所示:
  • 强引用只要存在,就不会被垃圾回收器回收掉,哪怕内存不足的时候,会直接抛出OOM异常,也不会去回收。
  • 想中断强引用和对象之前的引用关系,可以将引用赋值为null即可,如o = null,此时引用和对象就会断开联系,对象在下次GC就会被回收掉。
  • 软引用(Soft Reference):软引用是用来描述一些非必须但仍有用的对象。
    • 软引用需要显示调用,一个变量只能通过强引用指向堆内存中,所以需要使用java.lang.ref.SoftReference类来中继,表示软引用,演示语句如下:

    SoftReference<Object> softRf = new SoftReference<>(new Object()); 在内存中的结构如下图所示:

  • 内存足够的时候,软引用对象不会被回收,只有在内存不足的时候,系统才会回收软引用对象,如果回收了软引用对象之后,依然没有足够的内存,才会抛出OOM异常。
  • 这种特性常常被用来做缓存技术,比如网页或者图片缓存等,服务器内存压力不大图片就一直放在内存中,用户读取体验很好,当服务器内存压力大的时候,图片缓存就被清理掉,将内存空间尽可能给核心业务使用。
  • 当一个对象唯一剩下的引用是软引用,那么该对象才是软可及的。想要拿到被软引用的那个对象,可以使用java.lang.ref.SoftReference提供的get()方法,比如softRfObj.get();
  • 弱引用(Weak Reference):弱引用的引用强度比软引用更弱。
    • 弱引用也需要显示调用,使用java.lang.ref.WeakReference类来中继,表示弱引用,演示语句如下:

    WeakReference<Object> weakRfObj = new WeakReference<>(new Object()); 在内存中的结构如下图:

  • 弱引用中,无论内存是否足够,只要JVM开始进行垃圾回收,被弱引用关联的对象都会被回收。
  • 弱引用看似没有作用,因为会直接被垃圾回收。但其实弱引用在ThreadLocal中被使用到,而且是降低了ThreadLocal内存泄漏风险的主要方式,具体分析见:ThreadLocal与弱引用
  • 虚引用(Phantom Reference):虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有引用一样,随时可能被回收。
    • 虚引用也需要显示调用,使用java.lang.ref.PhantomReference类来中继,表示虚引用。PhantomReference类源码中只有一个构造函数和一个get()方法,而且get()方法仅仅返回一个空,也就是说永远无法通过虚引用直接获取对象,虚引用必须和ReferenceQueue引用队列一起使用才可以。演示语句如下:

    ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantomRfObj = new PhantomReference<>(new Object(), queue);

    • 虚引用虽然无法直接获取,但是当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。其运作机制如下图:
    • 所以,当被虚引用关联的对象被回收时,我们可以通过引用队列得知这一情况的发生,然后可以定制化地对这个对象进行回收前的处理。
    • 最经典的应用:管理直接内存
      • JDK的NIO(New Input/OutPut)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以通过Native函数库直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象来对这块内存的引用进行操作,避免数据在Java堆与Native堆中数据的来回复制。也就是说通过一种特殊的指针将JVM中的对象与JVM外的内存连接了起来。
      • 由于JVM的垃圾回收器只能管理与回收JVM内存中的对象,那么如何管理堆外内存中的数据呢?其实就是用过虚引用,将DirectByteBuffer对象关联起来:DirectByteBuffer实例化的会创建一个Cleaner实例,Cleaner是PhantomReference的子类。
      • DirectByteBuffer对象本身其实是很小的,但是它后面可能关联了一个非常大的堆外内存,如果GC回收了DirectByteBuffer对象本身,说明DirectByteBuffer对象所指向的堆外内存也不需要了。Cleaner实例就会被加入到ReferenceQueue中,然后用户线程就可以从队列中收到这个通知,针对性地通过Native的操作来释放堆外内存。
  • 四大引用类型总结

垃圾回收常见面试题与真题

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/06/06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 垃圾回收算法
    • 垃圾回收之标记算法
      • 垃圾回收之回收算法
        • 垃圾回收算法的组合拳:分代收集理论/分代收集算法
        • 经典的垃圾收集器
          • 年轻代的垃圾收集器:
            • 老年代的垃圾收集器:
            • 先进的低延迟垃圾收集器
            • Java的四大引用类型
            • 垃圾回收常见面试题与真题
            相关产品与服务
            云服务器
            云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档