JVM GC

4 篇文章 0 订阅

GC 核心概述

在这里插入图片描述

JVM 运行时数据区(栈和堆) 我们详细的讲解了运行时数据区的内容,该篇文章会详细介绍执行引擎中的垃圾回收器 GC。

Java 自动化内存管理

Java 相比 C/C++ 等语言,最大的不同就在于对内存的开辟和释放都是自动化的,这样做的好处是能降低由程序编码疏漏导致的内存泄漏和内存溢出的风险。但自动化同样也带来了缺点,它在一定上弱化了开发人员在程序出现内存泄漏和内存溢出时定位问题和解决问题的能力。

虽然内存的开辟和释放都交由了堆和 GC,但同样的我们也得了解自动化背后的原理,学会如何去监控和调节。

什么是垃圾?

看到标题也许你会很奇怪:垃圾还需要定义吗?不就是不使用的对象吗?不被 GC Root 引用的对象吗?

那如果我再具体问:什么是不使用的对象?对象什么时候不被 GC Root 引用的?也许你可能就回答不出来了。

对于垃圾的定义如下:

An Object is considered garbage when it can no longer be reached from any pointer in the running program.

翻译过来就是,垃圾指的是在程序中没有被任何指针指向的对象,这个对象就是需要被回收的垃圾。

所以我们说 GC 回收垃圾,那回收的其实就是这些没有被任何指针指向的对象。

内存碎片的概念

在这里插入图片描述

我们都知道 堆的内存本质上就是连续的一串内存地址,然后堆分配内存时就是在这块内存地址范围内提供一个或多个内存地址分配给对象。当这块内存地址满了,就会尝试 GC 回收对象的内存,而此时会有一些对象存活,有一些对象内存被回收,而这些被回收的内存就是内存碎片。

展现的效果就如上图,一个或多个白色方块就是被回收的对象,蓝色是可回收的对象内存,红色方块是仍然存活的对象内存。

为什么需要 GC?

对于系统而言,内存迟早都会被消耗完,因为不断的分配内存空间而不进行回收,就好像不停的产生生活垃圾。

所以 GC 的作用 除了释放垃圾对象,还需要对内存空间进行碎片管理,例如堆中分配给对象的内存,在经过 GC 回收内存后,根据不同的垃圾回收算法,被回收的内存地址需要通过空闲列表管理记录,或者回收后需要整理内存碎片。没有 GC 就不能保证应用程序的正常进行。

GC 相关算法对比

垃圾回收相关算法

垃圾回收算法分为两个阶段,每个阶段也对应有相应的算法:

  • 垃圾确认算法(标记阶段算法):引用计数法(不采用)、GC Roots 可达性分析算法

  • 清除垃圾算法(清除阶段算法):标记-清除算法、复制算法、标记-压缩算法

上面两个阶段简单理解就是:我要丢垃圾,那肯定得先确定它是垃圾

特别要注意的是,清除阶段算法中,标记-清除算法、标记-压缩算法在理解时要把 “标记” 两个字去掉,因为标记就是用的可达性分析算法完成的(后面会讲到),所以在它们的实际名称你只要记住是清除算法、压缩算法即可

标记阶段:引用计数算法

引用计算算法的原理是,对每一个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况

例如,对象 A 只要有任何一个对象引用了,则 A 的引用计数器 +1;当引用失效时,引用计数器 -1;只要对象 A 的引用计数器的值为 0,即标识对象 A 不可能再被使用,可进行回收。

引用计数算法的优点是,实现简单,垃圾对象便于识别且判断效率高。

但同时它又有缺点:

  • 需要单独的字段存储计数器,增加存储的空间开销

  • 每次赋值需要额外的加减法计算,增加时间开销

  • 最大的问题是 无法处理循环引用情况

引用计数算法的循环引用问题用下图解释:

在这里插入图片描述

所以引用计数算法因为这个严重问题没有被采用。

标记阶段:可达性分析算法

可达性分析算法它以 GC Roots 为起点,Gc Roots 就是一个集合,按照从上至下的方式搜索被 GC Roots 所连接的对象目标是否可达;使用可达性分析算法后,内存中的存活对象会被 GC Roots 直接或者间接连接着,搜索所走过的路径称之为引用链;如果目标对象没有任何引用链,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象。

在这里插入图片描述

相对于引用计数算法,可达性分析算法有效的解决了引用计数算法中循环引用的问题,防止了内存泄漏的发生。

在 Java 中可作为 GC Roots 的对象包括:

  • 方法区:静态对象、常量对象

  • 虚拟机栈:局部变量表中的对象(方法内的对象参数、方法内创建的对象)

  • 本地方法栈:JNI 中的对象

  • 所有被 synchronized 持有的对象

  • Java 虚拟机内部引用的对象:基本数据类型对应的 Class 对象,一些常驻的异常对象(NullPointerException 等)、系统类加载器

总的来说,可以作为 GC Roots 的对象就是:这个对象不在堆中,又引用着堆里面的对象,那么它就是 GC Roots。在堆内部的对象引用另一个堆内部的对象,这不算 GC Roots。

在这里插入图片描述

清除阶段:标记-清除(Mark-Sweep)算法

在这里插入图片描述

标记-清除算法是一种非常基础和常见的垃圾收集算法,该算法被 J.McCarthy 等人在 1960 年提出并应用于 Lisp 语言。

标记-清除算法执行过程是,当堆空间中有效内存空间被耗尽时,会进行 STW(Stop The World)行为,然后进行标记清除。

标记即通过可达性分析算法遍历所有被引用的对象确认可回收的垃圾,清除则是对堆内存从头到尾进行线性遍历,如果发现某个对象在 GC Roots 没有被标记为可达,则将其回收

它的缺点也很明显:

  • 需要从头到尾线性遍历查找效率不高

  • GC 时要 STW,导致用户体验差,会产生大量的内存碎片

需要注意的是,这里的清除不是抹去内存中的数据,而是本身分配的是一组连续的内存地址给对象使用,清除就是在回收这些内存地址,将它们保存在空闲地址表中,下次有对象需要分配内存时可以从这里提供内存地址。

清除阶段:复制(Copying)算法

在这里插入图片描述

为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.LMinsky 于 1963 年发表了著名论文:使用双存储区的 Lisp 语言垃圾收集器,该论文中被描述的算法被人们称之为复制算法。

复制算法执行过程是,将内存空间分为两块,每次只使用其中一块,在垃圾回收的时候,将正在使用的内存中存活的对象复制到未被使用的内存块中,之后清除正在使用的内存块,交换两个内存角色。

复制算法简单理解就是,将一块内存对半分(假设分为左内存和右内存),真正在使用的只有一半内存,左内存要清除前,将存活的对象按右内存的地址顺序排放后再清除;等右内存满了,就反过来处理交换角色

它的缺点是:

  • 需要两倍的内存空间

  • GC 需要维护对象的引用关系(存活对象复制到另一半时内存地址会更改),时间开销加大

复制算法的方案适用于垃圾对象较少,量级不大的情况

针对复制算法的问题同样的也提供了优化方案:

在这里插入图片描述

复制算法从一开始的对半分内存只使用一半内存的情况,在年青代被优化成了两部分,一部分是 Eden 区,另一部分是 Survivor 区,比例从 1:1 优化为 8:2。这个比例分配是有科学依据的,在常规应用的垃圾回收中,一次通常可以回收 70%-99% 的内存空间,回收性价比高,所以使用了这个比例。

在 Eden 区产生的对象,经过 GC 后存活的对象就会被推到 Survivor 区,图中有两个 Survivor,其实就是 From(S0) 和 To(S1) 区,这两部分内存和复制算法一样交替角色,直到对象年龄到达了老年代的阈值,就会被推到 Old 区

这种方案也是目前虚拟机对年青代的内存分配方案。

清除阶段:标记-压缩/整理(Mark-Compact)算法

在这里插入图片描述

复制算法的高效是建立在存活对象少、垃圾对象多的前提下,这种情况在年青代中经常发生,但是在老年代更常见的情况是大部分对象都是存活的,如果依然使用复制算法,由于存活对象多,复制成本也会非常高,因此基于老年代使用复制算法并不适用。

标记-压缩/整理算法的执行过程是,标记阶段它和标记-清除算法一致,通过可达性分析算法标记要回收的内存;然后将所有存活的对象压缩/整理到内存的一段,按照顺序排放,然后清理边界外所有的空间。

简单理解就是,标记-压缩/整理算法相比标记-清除算法标记完后 GC 清除多做了一步,将存活对象的内存整理有序排放

标记-压缩/整理算法的优劣:

  • 它最终的效果等同于标记-清除算法执行完后再进行一次内存碎片的整理,因此也可以把它称之为标记-清除-压缩(Mark-Sweep-Compact)算法

  • 二者本质差异在于标记-清除算法是一直非移动式的回收算法,标记-压缩/整理算法是移动式的,是否移动回收后的存活对象是一项优缺点并存的风险决策

  • 标记的存活对象被整理后,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了很多开销

算法性能指标对比

在这里插入图片描述

指标标记-清除算法标记-压缩/整理算法复制算法
速度中等最慢最快
空间少(会堆积碎片)少(不堆积碎片)需要两倍大小(不堆积碎片)
移动对象

从效率上来说,复制算法最快,但是内存浪费最多。

而为了尽量兼顾上面三个指标,标记-压缩/整理算法相对平滑一些,但是效率上差一些,它比复制算法多了一个标记阶段,比标记-清除算法多了一个整理阶段。

分代收集算法

无论是标记-清除算法、标记-压缩/整理算法还是复制算法都有各自的优劣,那是否有最优的回收算法呢?为了满足垃圾回收的效率最优性,分代收集算法应运而生。

分代收集算法基于一个事实:不同的对象生命周期是不一样的,因此,不同生命周期的对象可以采用不同的收集方式,以便于提高回收效率。一般是把堆分为年青代和老年代,这样就可以根据各个年代的特点使用不同回收算法,相对提高效率。

在这里插入图片描述

目前所有 GC 都采用分代收集算法,对象的状态经过大量的调研研究划分为年青代和老年代两个类别:

  • 年青代:区域相对小,对象生命周期短、存活率低、产生频繁,使用复制算法回收整理速度是最快的,复制算法效率只与当前存活对象大小有关,因此很适用于年青代,而空间问题因为存活率问题,所以单独开辟 Survivor 的 From(S0)和 To(S1)两块空间处理

  • 老年代:区域较大,对象生命周期长、存活率高,回收不及年青代频繁,因为存在大量存活的对象所以复制算法不适用,一般是用标记-清除算法和标记-压缩/整理算法混合使用。标记阶段的开销与存活对象的数量成正比;清除阶段的开销与所管理的大小成正比;压缩/整理阶段的开销与存活对象的数据成正比

增量收集算法

上述所有的垃圾回收算法在垃圾回收过程中,软件都会处于 STW(Stop The World)状态,在 STW 状态下,应用程序所有线程都会挂起暂停一切正常工作等待垃圾回收完成,这种情况将严重影响用户体验或系统稳定,为了解决这个问题,催生出了增量收集算法。

增量收集算法的概念是,如果一次性将所有垃圾进行处理会造成系统长时间停顿,增量收集就是让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只手机一小片区域的内存空间,接着切换到应用程序线程,直到垃圾收集完成。

增量收集算法实际上就是对线程间冲突的妥善处理,允许垃圾收集线程分阶段的方式完成标记、清理、复制等工作

使用这种算法由于在垃圾回收过程中间断性的执行了应用程序代码,虽然能减少停顿时间,但线程切换和上下文切换的消耗会让垃圾回收的总体成本上升,系统吞吐量下降。

分区算法

在这里插入图片描述

分区算法就是,相同条件下,堆空间越大,一次 GC 时间越长,停顿时间也越长,为了更好的控制 GC 产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干小区间,而不是整个堆空间,从而减少一次 GC 所产生的停顿。

常用的垃圾回收器

垃圾回收的串行与并行&并发

在这里插入图片描述

串行(Serial)指的是只有一条垃圾收集线程工作,当在垃圾收集时会 STW,回收完再启动用户线程。

并行(Parallel)指的是多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

并行&并发:

  • 并发指的是 多个事情在同一时间段内同时发生,并发的 多个任务之间会相互抢占资源

  • 并行指的是 多个事情在同一时间点上同时发生,并行的 多个线程之间不相互抢占资源

常用的几种垃圾回收器

在这里插入图片描述

上图是垃圾回收中常用的几种垃圾回收器,可以发现实际的 GC 要复杂得多,垃圾回收器并不是单独运行的,而是多种垃圾回收器复合算法混合着处理。其中有 Serial 单词的是串行回收器,有 Parallel 单词的是并行回收器。

根据年青代和老年代两个区域的划分,又可以将上面各个垃圾回收器根据不同的 GC 算法做区分:

在这里插入图片描述
接下来简单说明下上图各个垃圾回收器的在对应区域的 GC 处理。
在这里插入图片描述

在年青代使用 Serial 串行回收器,采用复制算法;在老年代使用 Serial Old 回收器,采用标记-整理算法。两种回收器都是单个 GC 线程在工作,都会出现 STW 行为。

在这里插入图片描述

在年青代使用 ParNew 回收器,有多个 GC 线程并行工作,采用复制算法;在老年代采用的 Serial Old 回收器,采用标记-整理算法。都会出现 STW 行为。

在这里插入图片描述

在年青代使用 Parallel Scavenge 回收器,有多个 GC 线程并行工作,采用复制算法;在老年代采用 Parallel Old 回收器,采用标记-整理算法。都会出现 STW 行为。

CMS 回收器

CMS 是采用标记-清除算法实现以获取最短回收停顿时间为目标的回收器。它的特点是能够实现在某些阶段与用户线程同时运行一边标记回收。

在这里插入图片描述

  • 初始标记:主要工作内容是标记出 GC Root 能关联到的对象。注意,这里只有 GC Root 对象,不会涉及引用链,该步骤会出现 STW 现象

  • 并发标记:遍历 GC Root 整个引用链。这个工作耗时非常长,采取了与垃圾收集器线程一起运行的方案

  • 重新标记:因为并发标记步骤有用户线程在运行,所以此处再次 STW 重新标记,但只标记重新运行后那部分对象数据的变动

  • 并发清理:清除不能到达 GC Root 的对象,该步骤会用户线程同步进行

  • 并发重置:更新之前使用过的数据

关于 CMS 回收器的具体算法实现思路是采用的三色标记法,具体实现可以参考下图:

在这里插入图片描述
在这里插入图片描述

垃圾回收器对比

根据上面的常用垃圾回收器,下面整理了它们具体的适用场景:

在这里插入图片描述

评估 GC 的性能指标

上面介绍了多种 GC 回收器以及简单了解了它们的运行机制。那怎么判定 GC 的性能?有具体的哪些指标?

GC 的性能指标如下:

  • 吞吐量:运行用户代码的时间占总运行时间的比例。总运行时间=程序运行时间+内存回收时间

  • 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例

  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间

  • 收集频率:应用程序的执行,收集操作发生的次数

  • 内存占用:堆区所占的内存大小

GC 性能调优是空间换时间或时间换空间,不存在完美,一般情况下是抓住吞吐量和暂停时间来设计。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值