JVM垃圾收集器

Posted by Yezhiwei on March 9, 2021

对象搜索算法与回收算法

垃圾回收(GC)是JVM的一大杀器,它使程序员可以更高效地专注于程序的开发设计,而不用过多地考虑对象的创建销毁等操作。

但是,当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,如果对GC不了解,很可能会成为个人的发展瓶颈。

学习垃圾收集需要完成以下三件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

对象已死?——GC对象搜索算法

垃圾回收,第一件事就是要搞清楚哪些东西是垃圾,而后才能对这些垃圾进行回收。

那么有什么办法识别对象是否存活呢?

通常有以下两种算法去识别判断

  • 引用计数算法

    这个算法非常简单。给对象一个计数器,每当这个对象被引用了,计数器值加一;引用失效,则减一。但这个对象计数值为0的时候,证明是无用对象,可以被GC程序回收掉。这种算法比较广泛应用在一些脚本语言上。 但是引用计数算法无法解决对象间相互引用的问题。当F对象引用了P对象,P对象也引用了F对象,这样F、P两个对象的计数器值都不会为0,即使这两个对象不被其他对象所引用,最终导致这些对象一直无法被回收。

    image-20210309180905736

  • 可达性分析算法

    可达性分析算法(GC roots算法),广泛应用于主流的商用语言。这个算法的基本思路就是通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

    如下图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的, 因此它们将会被判定为可回收的对象。

    image-20210309182424296

通常地,固定可作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等;

  • 方法区中类静态属性引用的对象,如Java类的引用类型静态变量;

  • 方法区中常量引用的对象,如字符串常量池(String Table)里的引用;

  • 本地方法栈中JNI(即一般说的Native方法)引用的对象;

  • 所有被同步锁(synchronized关键字)持有的对象;

  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如

    NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器

垃圾回收算法

分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,它建立在两个分 代假说之上:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区 域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那 么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对 象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有 效利用。

在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域 ——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。

  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
    • 新生代收集(M inor GC/Young GC):指目标只是新生代的垃圾收集。
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

算法

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

标记-清除,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。它是GC最基础的算法,后续很多算法都是基于它上面去改进的。

它的主要缺点有两个:

  • 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
  • 第二个是内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。

image-20210309213550582

标记-复制算法

复制算法的存在,正是为了解决内存碎片问题。并且这个算法也是分代算法的基础。

将内存分为大小相等的两块,每次程序只使用其中一块,当GC发生的时候,把存活的对象复制到另外一块内存中,整齐的排列,然后清空原来的那块内存。

image-20210309213526853

缺点:

  1. 把内存可使用的空间减少了一半,造成空间的浪费。
  2. 对象存活数量较多的时候,复制性能比较差
标记-整理算法(Mark-Compact)

针对复制算法的两个缺点,在老年代一般会用这种标记-整理算法。

其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的把存活的对象移到内存的一端,然后直接清理掉边界以外的内存。

image-20210309221915151

总结:分代收集算法

分代算法并不是一个特定的算法,也没有什么新的内容。而是把内存分成多个区域,一般为新生代、老年代等。然后根据不同区域不同的特点,用不同回收算法去回收垃圾。

例如新生代,对象存活率低,比较适用复制算法。老年代存活率高,比较适用Mark-Compact算法。

目前几乎所有的商业虚拟机都是采用分代收集的。

垃圾收集器

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

常见的垃圾收集器有3类

1. 新生代的收集器包括

  1. Serial
  2. PraNew
  3. Parallel Scavenge

2. 老年代的收集器包括

  1. Serial Old
  2. Parallel Old
  3. CMS

3. 回收整个Java堆(新生代和老年代)

  1. G1收集器

新生代垃圾收集器

1.Serial 串行收集器-复制算法

Serial 收集器是新生代单线程收集器,优点是简单高效,算是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程(Stop The World),直到它收集完成。

Serial 收集器依然是虚拟机运行在 Client 模式下默认新生代收集器,对于运行在 Client 模式下的虚拟机来说是一个很好的选择。

2.ParNew 收集器-复制算法

ParNew 收集器是新生代并行收集器,其实就是 Serial 收集器的多线程版本。

preview

除了使用多线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The Worl、对象分配规则、回收策略等都与 Serial 收集器完全一样。

3.Parallel Scavenge(并行回收)收集器-复制算法

Parallel Scavenge 收集器是新生代并行收集器,追求高吞吐量,高效利用 CPU。

该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

老年代垃圾收集器

1.Serial Old 收集器-标记整理算法

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程(串行)收集器,使用标记整理算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用

如果在 Server 模式下,主要两大用途:

(1)在 JDK1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用

(2)作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用

2.Parallel Old 收集器-标记整理算法

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。

3.CMS 收集器-标记整理算法

CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。

目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

**CMS收集器是基于“标记-清除”算法实现的,**它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:

(1)初始标记;

(2)并发标记;

(3)重新标记;

(4)并发清除。

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”

image-20210309223844522

CMS收集器主要优点

  1. 并发收集;
  2. 低停顿。

CMS三个明显的缺点:

  1. 在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计 算能力)而导致应用程序变慢,降低总吞吐量
  2. CMS 收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活。
  3. CMS是基于“标记-清除”算法实现的收集器,手机结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。

新生代和老年代垃圾收集器

1.G1收集器-标记整理算法

JDK1.7后全新的回收器, 用于取代CMS收集器。

G1收集器的优势:

  1. 独特的分代垃圾回收器,分代GC:分代收集器, 同时兼顾年轻代和老年代;
  2. 使用分区算法,不要求eden,年轻代或老年代的空间都连续;
  3. 并行性:回收期间,可由多个线程同时工作, 有效利用多核cpu资源;
  4. 空间整理: 回收过程中,会进行适当对象移动, 减少空间碎片;
  5. 可预见性: G1可选取部分区域进行回收, 可以缩小回收范围, 减少全局停顿。

G1收集器的运作大致可划分为一下步骤:

image-20210309224052187

G1收集器的阶段分以下几个步骤:

1、初始标记(它标记了从GC Root开始直接可达的对象);

2、并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象);

3、最终标记(标记那些在并发标记阶段发生变化的对象,将被回收);

4、筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)。

如果觉得还有帮助的话,你的关注和转发是对我最大的支持,O(∩_∩)O: