写点什么

我服了,阿里挖过来的 leader 连垃圾回收都说不清楚

作者:钟奕礼
  • 2022-11-21
    湖南
  • 本文字数:5500 字

    阅读完需:约 18 分钟

前言


  作为一名合格程序猿,JVM 的知识已经是我们必知必会的知识了。我们都知道 Java 与 C/C++比较大的区别之一就是 Java 的自动内存管理,而 Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。今天我们就来聊聊 Java 的垃圾回收(简称 GC)。


一、总体框架



垃圾回收


随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常,JVM 会自动完成垃圾回收工作,主要包括:


Minor GC/Young GC:针对新生代的垃圾收集;


Major GC/Old GC:针对老年代的垃圾收集。


Full GC:针对整个 Java 堆以及方法区的垃圾收集


Java 堆区可以划分为新生代和老年代,新生代又可以进一步划分为 Eden 区、Survivor 1 区、Survivor 2 区。具体比例参数的话,可以看一下这张图。


垃圾回收原理



一般情况下,新创建的对象都会被分配到 Eden 区(一些大对象特殊处理),这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。

对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。

复制算法不会产生内存碎片。在 GC 开始的时候,对象只会存在于 Eden 区和名为“From”的 Survivor 区,Survivor 区“To”是空的。紧接着进行 GC,Eden 区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold 来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次 GC 前的“From”,新的“From”就是上次 GC 前的“To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。


二、知识介绍


1. 如何确定垃圾?


(1)引用计数法


Java 中,引用和对象是关联的,操作对象必须用引用进行。因此,对象如果没有任何与之相关联的引用,即他们的引用数为 0,则该对象不太可能用到,即可回收。


(2)可达性分析


为了解决引用计数法存在的循环引用问题(有点类似死锁),Java 使用可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在 “GC roots”和一个对象之间没有可达路径,则该对象是不可达。另外,不可达对象不等价于回收对象,不可达对象变为可回收对象,需要至少两次标记。


2.主要算法


3.分代收集算法


是目前大部分 JVM 所采用的方法。核心思想根据对象的存活的不同生命周期进行内存划分(新生代(新对象频繁创建)和老生代(少量垃圾回收)),然后不同区域选择不同算法进行回收。


新生代:复制算法。老生代:标记复制算法。


4.Java 中的四种引用类型


(1)强引用(把对象复制给一个引用变量,这个变量就是强引用)(2)软引用(SolfReference 类来实现)(3)弱引用(4)虚引用


5.分区收集算法


将整个堆区划分为连续的不同小区间(分代是根据对象的生命周期),每个区间独立使用,独立回收,好处是可以控制一次回收多少个小区间。


6.GC 垃圾回收器


新生代:Serail、ParNew、Parallel Scavenge.年老代:CMS、MSC、Parallel Old。


(1)Serial 垃圾回收器 (单线程、复制算法)


最基本的垃圾回收器,使用复制算法,JDK1.3.1 之前新生代唯一的垃圾回收器。单线程,没有线程交互的开销,简单而高效。JVM 运行在 Client 模式下默认的新生代垃圾回收器。


JVM 有两种运行模式 Server 与 Client。两种模式的区别在于,Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多。这是因为 Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;而 Client 模式启动的 JVM 采用的是轻量级的虚拟机。所以 Server 启动慢,但稳定后速度比 Client 远远要快。


(2)ParNew (Serial+多线程)


Serial 的多线程版。JVM 运行在 Server 模式下默认的新生代垃圾回收器。


(3)Parallel Scavenge (多线程复制算法、高效)


也是新生代的垃圾回收器,他重点关注程序达到一个可控制的吞吐量,主要适用于在后台运行不需要太多交互的任务。自适应调节策略是 Parallel Scavenge 与 ParNew 的重要区别。


(4)CMS 收集器(多线程标记清除算法)


年老代垃圾收集器,主要目标是获取最短垃圾回收停顿时间。采用多线程的标记-清除算法。


(5)G1 收集器


目前垃圾回收理论发展最前沿成果,相比较 CMS,有以下两优点:1.基于标记-整理算法,不产生内存碎片 2.可以精准控制停顿时间


JVM 的内存分配


Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 堆 内存中对象的分配与回收。


Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。


上图所示的 Eden 区、From Survivor0(“From”) 区、To Survivor1(“To”) 区都属于新生代,Old Memory 区属于老年代。


大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。


  1. 大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.

  2. 大对象直接进入老年代

  3. 长期存活对象进入老年代


判断需要回收的对象



2.1 引用计数法


给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。


这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题


2.2 可达性分析算法


这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。


可作为 GC Roots 的对象包括下面几种:


  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 本地方法栈(Native 方法)中引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

  • 所有被同步锁持有的对象


2.3 引用


无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。


JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。


JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)


  1. 强引用(StrongReference)以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

  2. 软引用(SoftReference)如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

  3. 弱引用(WeakReference)如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  4. 虚引用(PhantomReference)"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。


虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。


2.4 不可达对象并非“非死不可”


即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。


被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。


垃圾回收算法



1.标记-清除算法:该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:


  • 效率问题

  • 空间问题(标记清楚后会产生大量不连续的碎片)



2. 标记-复制算法(新生代)


为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。


3. 标记整理算法(老年代)


根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。​


垃圾收集器


如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。


虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。


Serial 收集器


Serial 收集器是最基础、历史最悠久的收集器,是一个单线程工作的收集器,使用 Serial 收集器,无论是进行 Minor gc 还是 Full GC ,清理堆空间时,所有的应用线程都会被暂停。


ParNew 收集器


ParNew 收集器实质上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一致。


Parallel Scavenge 收集器


Parallel Scavenge 收集器也是一款新生代收集器,基于标记——复制算法实现,能够并行收集的多线程收集器和 ParNew 非常相似。Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。


Serial Old 收集器


Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。


Parallel Old 收集器


Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。


CMS 收集器


CMS 收集器设计的初衷是为了消除 Parallel 收集器和 Serial 收集器 Full gc 周期中的长时间停顿。CMS 收集器在 Minor gc 时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。


G1 收集器


G1 垃圾回收器是在 Java7 update 4 之后引入的一个新的垃圾回收器。G1 是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。


一张图总结各种 GC 收集器的比较


总结


  以上就是有关 JVM 垃圾回收的相关内容了,​需要 Java 实战书籍、架构视频、面试文档的资料已整理成文档,需要获取的小伙伴可以+ VX: mxk6072

用户头像

钟奕礼

关注

还未添加个人签名 2021-03-24 加入

还未添加个人简介

评论

发布
暂无评论
我服了,阿里挖过来的leader连垃圾回收都说不清楚_Java_钟奕礼_InfoQ写作社区