JVM 中的垃圾回收以及垃圾回收器

  • Post author:
  • Post category:其他


上篇讲了 Java 运行时内存的各个区域,对于程序计数器、虚拟机栈、本地方法栈这三部分,其生命周期与相关线程有关,随线程生而生,随线程灭而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。因此本篇主要介绍 java 堆和方法区这两个区域的垃圾回收。



1. 什么是垃圾回收

在 Java 中,所有的对象都是要存在内存中的,因此我们将内存回收,也可以叫做死亡对象的回收,也可以叫做垃圾回收。



2. 如何判断垃圾回收的对象(死亡对象的判断算法)



2.1 引用计数算法

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器就减一;任何时刻计数器为0的对象就是不能再被使用的,即对象已死。

使用场景:在 Python 语言中采用引用计数法进行内存管理。

缺点:在主流的 JVM 中没有选用引用计数器来管理内存,最主要的原因时引用计数器无法解决对象的循环引用问题。也就是两个对象互相引用导致没有线程使用对象,但是 JVM 却不能回收对象。总的来说就是 JVM 不适用引用计数法来判断对象是否存活。



2.2 可达性分析算法

可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集。

相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。这里的可达性分析就是Java、C# 选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集。

基本思路:通过一系列称为”GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时也就是从 GC Roots 到这个对象不可达时,证明此对象是不可用的,就是死亡对象。如下图:

在这里插入图片描述

作为 GC Roots 的对象:

  1. 虚拟机栈中的局部变量
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中 JNI 引用的对象



3. 如何回收垃圾(垃圾回收算法)



3.1 标记-清除算法

“标记-清除”算法是最基础的收集算法。算法分为:标记和清除两个阶段。首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点:

  1. 效率问题:标记和清除的效率都不高;
  2. 空间问题:标记清楚后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

过程图:

在这里插入图片描述



3.2 复制算法

复制算法是在标记-清楚算法的基础上,解决了它的效率问题。复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将次区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。

优点:性能高,内存分配时不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。

缺点:内存利用率低,通常只利用百分之五十。

使用场景:HotSpot 使用复制算法回收新生代。

过程图:

在这里插入图片描述



3.3 标记-整理算法

由于复制算法在对象存活率较高时会进行比较多的复制操做,效率会变低,所以在老年代一般不使用复制算法,从而提出了标记-整理算法。

标记-整理算法的过程与标记-清除过程一致,但它标记完对象后不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。

使用场景:老生代

过程图:

在这里插入图片描述



3.4 分代算法

分代算法不是一种具体的算法,它是通过区域划分的,在不同的区域实现不同的策略。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用标记-清理或者标记-整理算法。



4.垃圾回收的具体实现(垃圾收集器)

以上的收集算法是内存回收的方法论,那么垃圾收集器就是垃圾回收的具体实现。垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。



4.1 Serial 收集器

Serial 收集器是最基本、发展历史最悠久的收集器。

特性:Serial 收集器是一个单线程的收集器,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World,简称STW)。

优势:简单而高效,对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互,所以可以获得最高的单线程收集效率。

使用场景:Serial 收集器时虚拟机运行在 Client 模式下的默认新生代收集器。

在这里插入图片描述



4.2 ParNew收集器

ParNew 收集器除了使用多条线程进行垃圾收集之外,其余都与Serial 收集器完全一样。

特性:ParNew 收集器是Serial 收集器的多线程版本。

使用场景:ParNew收集器是许多运行在

Server

模式下的虚拟机中首选的新生代收集器。之所以是Server首选的新生代收集器最重要的原因是目前只有它能与CMS收集器配合工作。

在这里插入图片描述



4.3 Parallel Scavenge收集器

特性:Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。Parallel Scavenge收集器与其他收集器关注点不同,它是达到一个可控制的吞吐量,垃圾收集停顿时间越小,吞吐量越高。

Parallel Scavenge收集器可以通过两个参数控制吞吐量:

XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间

XX:GCRatio 直接设置吞吐量的大小

使用场景:停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高

效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。



4.4 Serial Old收集器

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

应用场景:Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

在这里插入图片描述



4.5 Parallel Old收集器

特性:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

应用场景:在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

在这里插入图片描述



4.6 CMS收集器

CMS收集器是基于“标记—清除”算法实现的。

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

优点:并发收集、低停顿。

缺点:

  • CMS收集器对CPU资源非常敏感
  • 无法处理浮动垃圾
  • 因为基于标记清除算法,所以会有大量的垃圾碎片产生

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

CMS 的运作过程:

  • 初始标记

    仅仅只是标记一下GC Roots能直接关联到的对象,需要 STW。
  • 并发标记

    进行 GC Roots Tracing 的过程。
  • 重新标记

    重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。
  • 并发清除

    清除对象。

    在这里插入图片描述



4.7 G1收集器

G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的 region 块,然后并行的对其进行垃圾回收。G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。它是唯一一款全区域的垃圾回收器。



版权声明:本文为AlinaQ05原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。