一站式搞定JVM的垃圾回收机制(最全版)

先提提要

首先在开始进行垃圾回收之前的总结,先看一张图片:
在这里插入图片描述
下面开始对其的每一个部分进行一个具体的讲解:

  • 程序计数器:是一块较小的内存单元,可以看做是当前的执行的线程所执行的字节码的行号指示器,字节码解释器就是通过这个改变计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转等都需要依赖这个计数器来完成。
  • Java虚拟机栈来说也是一个个的线程所私有的。描述的是java方法执行的内存模型,每一个方法在执行的同时都会创建一个栈帧,用于存储局部变量表,操作数,动态链接等,每一个方法在调用直到完成的过程,都对应着一个栈帧在虚拟机中从入栈到出栈的过程。
  • 本地方法栈与虚拟机栈不同的地方在于,虚拟机栈为虚拟机执行Java方法服务(也就是字节码)服务,但是本地方法栈则会虚拟机使用到 Native方法服务。
  • Java堆: 是虚拟所管理的内存最大的一块,对于Java堆来说,也是被线程所共享的,在虚拟机启动的时候,堆中存放的是所有实例化的对象,但是也是GC回收的主要对象。
  • 方法区和堆都是对于线程来说是共享的,存放的是一些已经被虚拟机加载的类的信息,常量,静态变量,既时编译器编译后的代码数据。 运行时常量池 是方法区的一部分,对于class文件来说 除了有类版本字段,方法,接口等信息来说,还有一个常量池,用于存放在编译器生成的各种字面量与符号引用。这部分的内容将在类加载后进入方法区的运行时常量池中存放。

JVM 垃圾回收模型

前景提要: 首先对于垃圾回收器而言,我们可能也早就听过,但是也仅仅限于听说过的层面上。其实对于垃圾回收的时候,哪些对象需要回收?什么时候回收? 如何回收都是问题,下面我们就开始进行具体的讲解。


在完成垃圾回收之前,我们需要判断哪些的对象是需要进行回收的–对于那些已经死去的对象我们需要进行回收。但是如何判断对象是都已经死去,有两种的方法。

引用计数算法(Reference Counting)

给对象添加一个引用计数器,当有一个地方引用它的时候,计数器加一。当引用失效的时候,计数器减一,任何时刻计数器为0的对象就是不可能再被引用的。此时就可以对其进行回收处理。此方法却不能解决对象循环引用的问题:
循环引用的栗子
开始有一个方法A和一个方法B,开始时候 有对象对A进行引用,也有一些栈方法什么的对B进行引用,且两则之间有相互引用的关系。后来其余的引用不再工作,这两个就互相引用,此时引用计数器的值也不是0,但是对于外部来说,这两个方法以及不具有任何的价值,但是就是不能够被回收掉,就是循环引用的问题。

根搜索算法。

在java中 使用根搜索算法判断是否存活。
基本思路: 就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到GC Roots 没有任何引用链相连,就证明此对象是不可用的。
但是问题来了什么是 GC Roots:

GC Roots

以下的引用我们可以称作其为:

  • 在VM (帧中的本地变量)中的引用
  • 方法区中的静态引用。
  • JNI(即一般说的Native方法)中的引用

在讲述完判断是否是垃圾的方法以后,下面我们开始要做的就是 对其进行回收。

垃圾回收算法(四种)

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

两个阶段: 标记- 清除
首先 标记所有需要回收的对象(就是在前面进行过判断的“已死的对象”),然后回收所有需要回收的对象。
缺点

  • 效率都不太高
  • 会产生大量的不连续内存碎片,空间碎片太多,会导致后续使用中由于无法找到足够的连续内存而提前触发另一次的垃圾搜集动作。
    GC的次数越多 碎片化情况就会越加严重。
    在这里插入图片描述

如上图所示: 此时对于 G,F,J,M 就无法进行相对应的回收。

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

标记过程仍然存在,但是后续的步骤不是进行直接清理,而是让所有存活的对象一端移动,然后直接清理掉这段边界以外的内存。
在这里插入图片描述
如上图所示,进行一次标记以后进行对应的整理操作。

复制算法(Copying):

复制算法简单介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EpRGHyVb-1582811209567)(en-resource://database/1338:1)]

复制算法具体实现过程

在这里插入图片描述

这里的复制算法在新生代中都会使用到的算法,至于什么是新生代,后面会具体介绍

复制算法的优点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NBW7xzoh-1582811209568)(en-resource://database/1342:1)]

分代算法(Generational)

分代简介

在这里插入图片描述

新生代

在这里插入图片描述

老年代

在这里插入图片描述
解惑时间
为什么:新生代要使用复制算法。

第一个原因:我们也都知道了对于垃圾回收回收的就是堆中的内容。虽然说也会回收方法区中的空间,但是效率很低下,这里我们不再进行考量。所以现在来说新创建的对象都是放在新生代中的,但是这些新的对象却是朝生息灭(就例如我们创建一个对象,但是很快我们就不会再使用,就会使用一次来说)。前面我们也讲到了复制算法的好处与不好的地方。 对于复制算法来说,是进行全部的复制,若是太多 效率会低 所以 就是新生代里面 很多都是没有用的,使用复制时候 可以被回收的不多也不会复制太多 就使用复制算法。
既然是老年代,就是经历了很多次的垃圾回收还存在的,所以就是说还有价值的,既然价值还在 下一次的回收也不一定会回收掉,使用复制算法时候 就会复制很多 这个时候 复制成本就会很多 。
对于复制算法 就会浪费很多空间。
第二个原因: 由于复制算法会消耗空间 一个 from 一个 to 新生代操作完成以后 但是还是会有很多的存活的对象,这个时候 从 fromto时候 若是空间不够用 可以转到老年代里面去,此时的老年代就起到了一个分配担保的作用。


前面的第一节和第二节我们讲到了 如何判断一个对象是否存活(即其是不是垃圾了已经。而是如何对这些垃圾进行收集的算法),但是想要实现这些算法,我们还需要更加严格的限制,才能正常运行以上所说的信息。

算法的实现

枚举根节点

前面提到了可以使用根节点来进行判断哪些是垃圾对象,但是就是需要逻辑性得使用引用链来进行查找来判断哪些的对象可以连成一个链,但是很多的应用仅仅方法区就有几百兆,所以一个个的查找引用是不现实的,所以虚拟机提供了一个可以直接得知哪些地方存放着引用:使用一组称为OopMap的数据结构来实现的,在完成了类的加载以后,HotSop就会把对象内什么的偏移量上是什么类型的数据计算出来,在Jit编译的过程中,也就会在特定的位置记录下栈和寄存器中哪些位置是引用。这样得到节点就会简单很多。

安全点

有了前面 OopMap的协助之下,对于 GC Roots的枚举就会简单很多,但是问题也应运而生,虽然这个OopMap可以帮助我们,但是为每一条指令都生成对应的OopMap,就需要很多的额外空间,这样 GC的空间成本就会很高。
所以虚拟机也不会为每条指令都会生成OopMap,而是只会在特定的位置记录下这些信息,称为安全点。即程序执行时候并非在所有地方都停顿下来开始GC,而是在特定的地方才会停止,开始GC。(这里需要进行解释一下,现代判断垃圾的方法是使用根搜素算法,所以对于一个根来说才会进行搜索,才会开始一次的GC过程,所以可以这样进行理解,在特定的地方停顿,在特定的地方进行根搜索)

多线程时候

虽然暂时解决了一个问题,但是还有一个问题是,如何让多线程也都能够跑到最近的安全点停下来呢。提供了两个方法:
方法一: 抢先式中断: 不需要线程的执行代码的主动配合,而是在发生GC的时候,将所有的线程进行中断,若是发现有的线程不再“安全点”上,就让其恢复,继续跑到安全点上去。对这个方法来说太过于繁琐也开销巨大,现在不再使用。
方法二: 主动式中断:就是说 不会对线程直接进行操作,而是说 设置一些标志点,各个线程在运行的过程中,去主动的轮询这些标志,若是发现为真,就将自己挂起。这里需要注意的是,轮询点和安全点也是重合的。为什么要重合呢: 就是说 当要进行GC时候,轮询点会为真,此时线程停在这里,也是安全点,进行根节点选定以后,就可以进行判断哪些是垃圾。

安全区域

前面是安全点的介绍,基础是说,线程在运行的时候,但是还有一种情况就是说不执行时候呢。所谓的不执行就是说,没有分配CPU,典型的例子是线程出现休眠状态,或者是说处于阻塞的状态呢?这个时候前面提到的轮询就不再适用。
safe Region : 安全区:是指在一段代码片段中,引用的关系不会发生变化。在这个区域中的任何地方开始GC都是安全的。我们也可以把这段区域看做是扩展的 安全点。


前面的介绍了垃圾收集的算法,是指的是内存回收的方法论,此时的垃圾回收器就是这些算法的具体实现。可以说 分代的模型是GC的宏观愿望。垃圾回收器是GC的具体实现,每种的垃圾回收器都有自己使用的场景。

垃圾收集器

前面笔者也说到了对于现在的垃圾收集器大都是基于分代算法下面先看一张的总览:

在这里插入图片描述
以上介绍了我们下面要介绍的一些垃圾收集器。

并行与并发

在介绍之前,我们先进行一个概念的了解:
并行:指多个垃圾回收器的线程同时工作,但是用户线程处于等待状态。
并发:指收集器在工作的同时,可以运行用户线程也工作。
注: 但是并发也不能说就是解决了GC停顿的问题,在关键的步骤,例如在收集器标记垃圾的时候还是要停顿的,但是在清除时候 回收器线程可以与用户线程并发执行。

Serial 收集器。

是一种单线程的收集器,在收集的时候 会暂停所有的工作线程(简称STW(stop the word)) 使用复制收集算法。在老年代使用 Mark-Compact。虚拟机运行在Client模式时候默认新生代收集器。

ParNew

是Serial收集器的多线程版本,除了使用多个收集线程外,其余都是一样的,且这种收集器是虚拟机运行在Server模式的默认新生代收集器。但是对于多CPU的环境中,这种收集器的效果不见得比Serial效果好。

Parallel Scavenge 收集器

在这里插入图片描述
对于用户来说 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户体验,对于 PS来说 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,适用于在后台运算但是交互不太多的任务。

Serial Old 收集器

是Serial收集器的老年代版本,是一个单线程的收集器,使用标记-整理算法,主要意义在于给Clinent模式下的虚拟机使用。

Parallel Old 收集器

是Parallel Scavenge 收集器的老年代版本,使用多线程和“标记整理”算法、

CMS 垃圾回收器

(Concurrent Mark Sweep)CMS垃圾回收器顾名思义是一种以获取最短回收停顿时间为目标的收集器。就是说 想要停顿的时间更少,也是因为很大一部分的Java应用集中在互联网或者B/S系统的服务器上,这样的服务器很需要响应速度,给用户较好的体验,其有四个步骤:

初始标记:

仅仅是标记与GC Roots能够直接关联到的对象,速度很快

并发标记

就是进行Gc RootsTracing的过程

重新标记

是为了修正在并发标记期间因用户程序的继续运作而导致标记参数变动的哪一步分对象的标记记录,也比并发标记的时间要短。

并发清除

在这里插入图片描述

缺点:

  1. 对CPU资源非常敏感,实际上面向并发设计的程序都对CPU资源比较敏感。在 并发执行的阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程,从而导致应用程序变慢,总吞吐量会降低。
  2. 无法处理浮动垃圾。因为CMS并发清理阶段用户的线程还在运行,就会导致有新的垃圾不断产生,CMS无法在当次处理掉,只好等待以一次的GC,这一部分的垃圾叫做浮动垃圾。
  3. 是基于标记-清楚,算法。所以就会产生大量的空间碎片,就会导致虽然老年代还有很大的空间,但是无法找到足够大的连续空间来分配当前对象,就会导致不得不 提前触发一次 FUll GC。

前面介绍到的几种垃圾回收器,或多或少的都是有延续之前的思想,复制,标记清除,整理,或是eden /to survivor/ from survivor 等,但是对于 G1 垃圾收集器来说 。对之前的禁锢都有了一定的推翻,使用了全新的模式,也是在未来的时间里,将要推到最前端的垃圾回收器。

G1 垃圾回收器

在这里插入图片描述
运行方式:
在这里插入图片描述
出现的初衷: 在未来可以替换掉CMS收集器
在进行G1的讲解之前,首先来了解G1与其相关的概念:

基础概念:

吞吐量:在一个指定的时间内,最大化一个应用的工作量
例如一个使用下面的方式来衡量一个系统吞吐量的好坏:

  • 在一个小时内同一个事务(或者说是任务)完成的次数(tps)
  • 数据库一个小时可以完成多少次查询。
    对于关注吞吐量的系统,对于我们来说卡顿在日常是可以接受的,因为对于我们来说,关注一个系统关注的是长时间的大量任务的执行能力,单词的响应并不值得我们去追求和考虑、

响应能力: 一个程序或者是系统能否及时响应,多久完成响应。比如:在解决到一个请求时候需要多久能够完成这个请求。

简介:设计目标

是一个面向服务端的垃圾收集器,适用于多核处理器,大内存容量的服务端系统。能够满足短时间GC停顿的同时达到一个较高的吞吐量。

  • 与应用线程同时工作,几乎不需要stop the world (与 CMS类似)
  • 整理剩余空间时候,不会产生内存碎片(前面我们讲到了是对于 CMS来说 只能再 Full GC 时候,用 stop the world)但是对于 G1来说,一是没有内存碎片,但是也不用等到 Full gc。对于CMS的full gs 时候,无论是新生代,还是老年代,都是会进行全部的垃圾回收,此时就需要线程进行等待,这个也是会抢占cpu的地方。(在所有的垃圾收集器的设计中,都会避免出现 full GC 的情况。)
  • 停顿时间更加可控 说的是对于 g1来说,我们可以在启动的时候 设置一个停顿的时间,例如和cms进行比较的时候,cms在进行full gc 时停顿的时间是不能够控制的,有时候就算是停顿很长的时间,也是没有办法的,但是对于g1 来说 我们设置一个时间,就算是有很多的垃圾需要回收时候,g1也会先进性一个评估,评估大概需要多久,最后 回收的也只是在对应时间差不多的空间大小,等到下一次的再度回收,可以控制。与堆的设计相关,看来也是比较重要的一部分知识啊。
  • 不牺牲系统吞吐量: 指的是说 前面我们也有讲到了 cms的不好的地方,就还是说 在并发执行阶段,虽然说 不会使用户线程停止但是也是会占用一部分的线程,会使得应用程序变慢。
  • gc 不要求额外的内存空间 (CMS需要预留空间 存储 浮动垃圾)这里说明什么是浮动垃圾,就是说 对于cms来说 ,在执行的时候,用户的线程还是在执行的,开始任务不是垃圾,不会进行回收,但是后来变成了垃圾,需要回收时候,此时的cms没有能够认为是垃圾。也是比较好的地方对于 cms来说

G1与CMS的优势。

  • 在压缩空间有优势:
  • 内存分区region 不再固定。
  • 各个代不需要指定大小
  • 控制时间 控制垃圾收集时间,避免雪崩
  • 在回收内存以后,会立马进行合并内存的操作,但是cms要进行stop the word
  • G1 可以在yong 但是cms只能老年代。
  • 一个是复制,一个是 标记整理,不会有内碎片产生。
  • 同比较 parallel Scavenge和 parallel Old 比较时候 parallel 会对整个区域做整理,此时的时间停顿比较长
  • 前面讲到了会根据用户设定的停顿时间,会智能评估,回收哪几个的时候可以满足用户的设定

收集集合(Cset)

是一组可被回收的分区的集合,在CSet中存活的数据会在GC的过程中被移动到另一个可用分区,CSet中的分区可以来自eden空间,survivor空间,或者老年代:说是一种准备要被回收的数据的集合。

已记忆集合(RSet)

记录了其他的Region对象引用本Region中对象的关系,属于points-inot (谁引用了我的对象)。其价值在于,使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可,Region就是前面图片中的一个个小的方格,其中存放的都是对象。
在这里插入图片描述
如图所示,1引用了2中的对象,3也是如此,此时2就会有一个内存空间用于记录谁引用了我
在这里插入图片描述
在这里插入图片描述
年轻代:
在这里插入图片描述

堆内存划分。

注:这里为什么要讲解堆的划分,也是让我们在后面的理解中更加深刻的理解G1垃圾收集器的内存结构,同时也是与CMS进行一个比较(因为G1设计的初衷就是作为并发标记收集器(CMS)的长期替代产品)
对于cms来说本质上是使用 mark-sweep 算法,但是对于 G1是从整体上看是基于“标记-整理”算法实现的,但是对于局部的(region)上来看是基于“复制”算法。但是无论如何都意为这G1在长期的运行期间都不会产生内存空间碎片,收集后也能够提供规整的空间。自然就可以高效整理剩余的内存,也就不需要管理内存碎片。。也不需要因为长期的运行时候会发生分配较大的对象而找不到连续的内存空间而触发 Full GC。

基础的模式:

在这里插入图片描述
传统的 新创建的对象位于eden 中 首先是在其中进行创建,然后进行回收时候,将 eden 和s0全部清空 放到 s1里面去 下一次 进行进行eden创建 将 s1 和eden 清空 放到 s0去 反复。

G1垃圾收集器的结构

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

并发执行时候的基础流程
  • 当执行垃圾收集时,G1以类似于CMS收集器的方式运行。
    G1执行并发全局标记阶段,以确定整个堆中对象的活动性。标记阶段完成后,G1知道哪些区域大部分为空。它首先收集在这些区域中,通常会产生大量的自由空间。这就是为什么这种垃圾收集方法称为“垃圾优先”的原因。顾名思义,G1将其收集和压缩活动集中在可能充满可回收对象(即垃圾)的堆区域。G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数。
  • 由G1标识为可回收的成熟区域是使用疏散收集的垃圾。G1将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩并释放内存。撤离是在多处理器上并行执行的,以减少暂停时间并增加吞吐量。因此,对于每个垃圾收集,G1都在用户定义的暂停时间内连续工作以减少碎片。这超出了前面两种方法的能力。CMS(并发标记扫描)垃圾收集器不进行压缩。ParallelOld垃圾回收仅执行整个堆压缩,这导致相当长的暂停时间。

运行的主要模式

young

young 在eden 充满时候触发,在回收之后 就变成了空白的
在这里插入图片描述
主要流程:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

如下主要步骤:

如我们讲到的,先进行一个标记的过程,然后将有生命的问题撤离(即复制移动)到一个或多个幸存区域,若是满足老化阙值,直接被升到老年代。
在这里插入图片描述

有的已经被疏散到幸存者地区或是老年代。
在这里插入图片描述

老年代

上面介绍到了G1在并发阶段,在新生代的情况,但是G1还有一个就是回合mixed,可以对新生代操作的同时也会对老年代进行操作:
在这里插入图片描述
在这里插入图片描述
这里进行一个回顾,前面我们有讲到的是对于G1来说会有两种机制一种是 young一种是mixed,其中 young只是会对young进行,但是 mixed就会是young和 老年代同时进行 ,此时对于mixed来说 其中有一个并发标记,作用是在老年代里面查询得出收益高的若干老年代,其中的并发标识使用到的就是 GCM。当mixed也不能够满足要求的时候 ,就会退化为full GC,导致stop the word

具体过程

阶段 描述
Initial Mark 初始化标记时候会导致stop the word ,在G1中,会在young GC 上进行,在标记幸存者区域(根区域的时候)可能会有对老年代对象的引用。
根区域的扫描 扫描幸存者区域以获取老年代的参考,在进行新生代的GC回收之前必须完成此阶段。
并发标记 在整个堆中查找活动的对象。并发过程运行时候会发生这样的情况,但是这个阶段可以被年轻代垃圾收集器中断
重新标记(STW) 完成的是堆中活动对象的标记。使用一种称为快照(SATB)算法,此算法交CMS收集器中使用到的要快的多,在后面会进行介绍
清理(STW) 1. 对活动对象和完全空闲区域进行记录 2. 进行RSet(会发生STW)
复制 在stop the word 之后,将之前进行标记过的赋值到全新的未使用的地区中

下面介绍一下STAB算法

STAB

在这里插入图片描述在并发阶段可能会遇到的两个问题
二是创建新的对象:
在这里插入图片描述
一个是 对象引用的变更:
在这里插入图片描述
并发标记是并发多线程的,但并发线程在同一个时刻也是只会扫描同一个分区


前面介绍到了使用 gcm 进行统计收益比较高的老年代,下面介绍GCM。

gcm

前面说到的是 G1 可以适用于 老年代和新生代,对于 mixed 就是老年代的 。但是怎么说呢 gcm的执行过程类始于cms 但是不同的是,他主要是为mix 提供一个标记服务的,并不是G1 的一定要有的一部分 。因为对于G1来说有时候没有Mixed GC 时候 ,就不会进行所谓的GCM

GCM四个步骤:

  • 初始标记: 会出现stop 会从Gc Root开始直接可达的对象
  • 并发标记: 从GC Root 开始对 heap 中的对象进行标记,这个标记的过程和应用程序线程并发执行,并且收集 位于 region中的对象的存活的情况。
  • 重新标记: 标记那些在并发阶段发生变化的对象,将被回收。
  • 清理 清空region,那些region中已经没有了存活的对象 加入 free list。
    在这里插入图片描述

gcm 结束之后

我们可以设计一些的参数进行基础控制
在这里插入图片描述
上面所说的进行GCM时候需要判断垃圾的占比就需要用到此参数。看到的是存活对象的占比在某一个值的下面,也就不是意味着垃圾占比在某一个值的上面才会入选CSet
在这里插入图片描述

G1 不提供 full gc 但是若是mixgc 无法根上内存的分配的速度 就会触发 serial old GC 收集整个GC 。

Humongous 区域

当然会与一些特殊的区域G1也会有对应的处理方法
在这里插入图片描述

G1的最佳实践

  • 不断调优暂停时间指标
    通过 -XX:MaxGCPauseMills=x 可以设置启动引用程序暂停的时间,G1在运行时候 会根据这个参数选择CSet来满足响应时间的设置。一般都设置在 100到 200 ms
    面试问 为什么设置这么久: 大概是因为若是设计的时间太短的话,会导致出现 G1跟不上垃圾产生的速度,就会导致最终的full gc 出现。
  • 不要设置 新生代 和老年代的大小
    不要设置这些 我们可以设置 整个堆内存大小 不设置 也会有自己的启动至
    在这里插入图片描述

后记

以上就是我在看《深入理解java虚拟机》和一些相关联的视频整理出来的自我认知有什么不是很合适的地方,还请大家指出来,一切学习进步。

发布了26 篇原创文章 · 获赞 5 · 访问量 715

猜你喜欢

转载自blog.csdn.net/weixin_44015043/article/details/104545873