阅读 86

学习笔记 | Java 垃圾回收(面试必备)

Java 垃圾回收与垃圾收集器

垃圾回收(Garbage Collection,GC),就是通过垃圾收集器把内存中没用的对象清理掉。垃圾回收涉及到内容:

  • 判断对象是否已死
  • 选择垃圾收集算法
  • 选择垃圾收集的时间
  • 选择适当的垃圾收集器清理垃圾

判断对象是否已死

判断对象是否已死:找出哪些对象是已经死掉的,以后不会再用到的。

判断对象是否已死的方法:引用计数算法可达性分析算法

引用计数算法

给每一个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加 1;每当有一个地方不在引用它时,计数器值减 1,这样只要计数器的值不为 0,就说明还有地方引用它,它就不是无用的对象。

这种方法看起来非常简单,但是目前许多主流的虚拟机都没有选用这种算法来管理内存,原理就是当某些对象之间互相引用时,无法判断出这些对象是否已死。

可达性分析算法

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

在 Java 语言中,可作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI (即一般说的 Native 方法)引用的对象

常用垃圾回收算法

常用的垃圾回收算法有三种:

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法

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

分为 标记清除 两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。

不足之处:

  • 效率问题:标记和清除两个阶段的效率都不高;
  • 空间问题:标记清除之后会产生大量不连续的内存碎片,导致在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

复制算法(Copying)

为了解决 "标记-清除" 算法内存碎片化的缺陷而被提出的算法。根据内存容量将内存划分为相等大小的两块。每次只使用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点:效率要高于标记-清除算法,不会产生过多的碎片。在对象存活率较高时要进行较多的复制操作,效率会较低。

缺点:可用内存被压缩到原本的一半。且存储对象增多的话,Copying 算法的效率会大大降低。

标记-整理算法(Mark-Compact)

先对可用的对象进行标记,然后所有被标记的对象都向一端移动,最后直接清理掉端边界以外的内存。

优点:自带整理功能,这样不会产生大量不连续的内存空间,适合老年代的大对象存储。

分代收集算法(Generational Collection)

分代收集算法是目前大部分 JVM 所采用的方法,其核心思想就是根据对象存活的不同生命周期将内存划分为不同的域(把堆内存分为新生代和老年代)。老年代的特点是每次垃圾回收只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有垃圾需要被回收,因此可以根据不同区域选择不同的回收算法。

新生代与复制算法

目前大部分 JVM 的 GC 对于新生代都采用 Copying 算法,因为新生代中每次垃圾回收都会回收大部分对象,即要复制的对象比较少,通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 区和两个较小的 Survivor 区(From Survivor、To Survivor),每次使用 Eden 区和其中一块 Survivor 区,当进行回收的时候,将该两块空间中还存活的对象复制到另外一块 Survivor 空间中。

老年代与标记复制算法

老年代因为每次只会回收少量对象,因而采用 Mark-Compact 算法

  1. Java 虚拟机提到过的处于方法区的永生代,它用来存储 class 类、常量、方法描述等,对永生代的回收主要包括废弃常量和无用的类。
  2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Space 目前存放对象的那一块),少数情况会直接分配到老年代。
  3. 当新生代的 Eden 区和 From Survivor 区空间不足时会触发一次 MinorGC,进行 GC 后,Eden 区和 From Survivor 区的存活对象会被挪到 To Survivor,然后将 Eden 区和 From Survivor 进行清理。
  4. 如果 To Survivor 没有足够存储某个对象,则将这个对象存储到老年代。
  5. 在进行 GC 后,使用的便是 Eden 区和 To Survivor区了,如此反复循环。
  6. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会加 1。默认情况下年龄达到 15 的对象会被移到老年代中。

垃圾收集器

常见垃圾收集器

现在常见的垃圾收集器有如下几种:

  • 新生代收集器:Serial、ParNew、Parallel Scavenge
  • 老年代收集器:Serial Old、CMS、Parallel Old
  • 堆内存垃圾收集器:G1

HosSpot 虚拟机的垃圾收集器

Serial 收集器

Serial 收集器是最基本、发展历史最悠久的收集器。Serial 收集器是一个单线程的收集器,但是这个"单线程"的意义并不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)。

Serial 收集器是虚拟机运行在 Client 模式下的默认新生代收集器。

优点:简单而高效,对于限定单个 CPU 的环境来说,Serial 收集器没有线程交互的开销,专心做垃圾收集可以获得最高的单线程收集效率。

适用场景:适合运行在 Client 模式下的虚拟机。

ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本,可以使用多条线程进行垃圾收集。

ParNew 是运行在 Server 模式下的虚拟机中首选的新生代收集器,只有 ParNew 收集器能够与 CMS 收集器配合工作。

ParNew 默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多的环境下,可以使用 -XX:ParallelGCThreads参数来限制垃圾收集的线程数。

Parallel Scavenge

Parallel Scavenge 是一个新生代收集器,使用复制算法实现,并行的多线程收集器,吞吐量优先的收集器。

Parallel Scavenge 收集器的目标是 达到一个可控制的吞吐量(Throughput)。

Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数以及设置吞吐大小的 -XX:GCTimeRatio 参数。

MaxGCPauseMills:允许的值是一个大于 0 的毫秒数,收集器将尽可能地保证内存回收话费的时间不超过设定值。

GCTimeRatio:参数的值是一个大于 0 且小于 100 的证书,就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。

Serial Old 收集器

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

适合 Client 模式下的虚拟机使用。

在 Server 模式下,还有两大用途:

  • 在 JDK1.5 以及以前的版本中与Parallel Scavenge 收集器搭配使用
  • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和"标记-整理"算法。从 JDK 1.6 开始提供。 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

CSM 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器是基于"标记-清除"算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为 4 个步骤:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

CMS 是一款优秀的收集器,主要优点:并发收集、低停顿。但是 CMS 还存在以下 3 个缺点:

  • CMS 收集器对 CPU 资源非常敏感。
  • CMS 收集器无法处理浮动垃圾(Floating Garbage)。
  • CMS 基于"标记-清除"算法实现,会产生大量空间碎片。

G1 收集器

G1(Garbage First) 收集器是当今收集器技术发展的最前沿成果之一。

G1 收集器是基于 标记-整理 算法实现的收集器,它不会产生空间碎片,可以非常精确地控制停顿。

G1 是一款 面向服务端应用 的垃圾收集器。与其他 GC 收集器相比,G1 具备如下特点:

  • 并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-The-World 停顿时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
  • 空间整合:G1 从整体上来看是基于"标记-整理"算法实现,从局部上看是基于"复制"算法实现的,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利用程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前出发下一次 GC。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,这几乎是实时 Java(RTSJ)的垃圾收集器的特征了。

垃圾收集器参数总结

参数 描述
UseSerialGC 虚拟机运行在 Client 模式下的默认值,打开此开关后,使用 Serial + Serial Old 的收集器组合进行内存回收
UseParNewGC 使用 ParNew + Serial Old 收集器组合进行内存回收
UseConcMarkSweepGC 使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为CMS收集器出现 Concurrent Mode Failure 失败后的后备收集器使用
UseParallelGC 虚拟机运行在 Server 模式下的默认值,使用 Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收
UseParallelOldGC 使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
SurvivorRatio 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为8,代表Eden:From:To=8:1:1
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold 晋升到老年代的年龄。每个对象在坚持过一次 Minor GC 之后,年龄就加 1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应对新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况
ParallelGCThreads 设置并行 GC 时进行内存回收的线程数
GCTimeRatio GC 时间占总时间的比率,默认为99,即允许 1% 的 GC 时间,仅在使用 Parallel Scavenge 收集器时生效。
MaxGCPauseMillis 设置 GC 的最大停顿时间。仅在使用 Parallel Scavenge 收集器时生效
CMSInitiationOccupancyFraction 设置 CMS 收集器在老年代空间诶使用多少后触发垃圾收集。默认值为68%,仅在使用 CMS 收集器时生效
UseCMSCompactAtFullCollection 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用 CMS 收集器时生效
CMSFullGCsBeforeCompaction 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用 CMS 收集器时生效

内存分配与回收策略

  1. 对象优先在 Eden 分配:大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC。
  2. 大对象直接进入老年代:大对象是指需要大量连续内存空间的 Java 对象,虚拟机提供了一个 -XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在 Eden 区及两个 Survivor 区之前发生大量的内存拷贝。
  3. 长期存活的对象将进入老年代:如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设置为 1,对象在 Survivor 区中每熬过一次 Minor GC,年龄增加 1 岁,当它的年龄增加到一定程度(默认15岁)时,就会被晋升到老年代中。
  4. 动态对象年龄判定:为了更好的适应不同程序的内存情况,虚拟机不总是需要对象年龄必须达到 MaxTenuringThreshold 才能晋升到老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等待 MaxTenuringThreshold 中要求的年龄。
  5. 空间分配担保:在发生 Minor GC 时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的胜过于空间大小,如果大于,则改为直接进行一次 Full GC;如果小于,则查看 HandlePromotionFailure 设置是否允许担保失败,如果允许,那只会进行 Minor GC,如果不允许,则也要改为进行一次 Full GC。

Minor GC vs Major GC vs Full GC

Minor GC:指发生在新生代(包括 Eden 区和 Survivor 区)的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。

Major GC:指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。Minor GC 的速度一般会比 Minor GC 的速度慢 10 倍以上。

Full GC:针对新生代、老年代、元空间(Metaspace、Java8 以上版本取代 Perm gen)的全局范围的 GC。Full GC 不等于 Major GC,也不等于 Minor GC + Major GC,发生 Full GC 需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

关注获得更多分享


推荐推荐: