图解JVM垃圾内存回收算法

前言

首先,我们要讲的是JVM的垃圾回收机制,我默认准备阅读本篇的人都知道以下两点:

  • JVM是做什么的
  • Java堆是什么

因为我们即将要讲的就是发生在JVM的Java堆上的垃圾回收,为了突出核心,其他的一些与本篇不太相关的东西我就一笔略过了

众所周知,Java堆上保存着对象的实例,而Java堆的大小是有限的,所以我们只能把一些已经用完的,无法再使用的垃圾对象从内存中释放掉,就像JVM帮助我们手动在代码中添加一条类似于C++的free语句的行为

然而这些垃圾对象是怎么回收的,现在不知道没关系,我们马上就会讲到

怎么判断对象为垃圾对象

在了解具体的GC(垃圾回收)算法之前,我们先来了解一下JVM是怎么判断一个对象是垃圾对象的
顾名思义,垃圾对象,就是没有价值的对象,用更严谨的语句来说,就是没有被访问的对象,也就是说没有被其他对象引用,这就牵引出我们的第一个判断方案:引用计数法

引用计数法

这种算法的原理是,每有一个其他对象产生对A对象的引用,则A对象的引用计数值就+1,反之,每有一个对象对A对象的引用失效的时候,A对象的引用计数值就-1,当A对象的引用计数值为0的时候,其就被标明为垃圾对象

这种算法看起来很美好,了解C++的应该知道,C++的智能指针也有类似的引用计数,但是在这种看起来“简单”的方法,并不能用来判断一个对象为垃圾对象,我们来看以下场景:

在这个场景中,A对象有B对象的引用,B对象也有A对象的引用,所以这两个对象的引用计数值均不为0,但是,A、B两个对象明明就没有任何外部的对象引用,就像大海上两个紧挨着的孤岛,即使他们彼此依靠着,但仍然是孤岛,其他人过不去,而且由于引用计数不为0,也无法判断为垃圾对象,如果JVM中存在着大量的这样的垃圾对象,最终就会频繁抛出OOM异常,导致系统频繁崩溃

总而言之,如果有人问你为什么JVM不采用引用计数法来判断垃圾对象,只需要记住这一句话:引用计数法无法解决对象循环依赖的问题

可达性分析法

引用计数法已经很接近结果了,但是其问题是,为什么每有一个对象来引用就要给引用计数值+1,就好像有人来敲门就开一样,我们应该只给那些我们认识的、重要的人开门,也就是说,只有重要的对象来引用时,才给引用计数值+1

但是这样还不行,因为重要的对象来引用只要有一个就够了,并不需要每有一个引用就+1,所以我们可以将引用计数法优化为以下形式:

给对象设置一个标记,每有一个“重要的对象”来引用时,就将这个标记设为true,当没有任何“重要的对象”引用时,就将标记设为false,标记为false的对象为垃圾对象

这就是可达性分析法的雏形,我们可以继续进行修正,我们并不需要主动标记对象,而只需要等待垃圾回收时找到这些“重要的对象”,然后从它们出发,把我们能找到的对象都标记为非垃圾对象,其余的自然就是垃圾对象

我们将上文提到的“重要的对象”命名为GC Roots,这样就得到了最终的可达性分析算法的概念:

创建垃圾回收时的根节点,称为GC Roots,从GC Roots出发,不能到达的对象就被标记为垃圾对象

其中,可以作为GC Roots的区域有:

  • 虚拟机栈的栈帧中的局部变量表
  • 方法区的类属性和常量所引用的对象
  • 本地方法栈中引用的对象

换句话说,GC Roots就是方法中的局部变量、类属性,以及常量

垃圾回收算法

终于到本文的重点了,我们刚刚分析了如何判断一个对象属于垃圾对象,接下来我们就要重点分析如何将这些垃圾对象回收掉

标记-清除算法

标记-清除很容易理解,该算法有两个过程,标记过程和清除过程,标记过程中通过上文提到的可达性分析法来标记出所有的非垃圾对象,然后再通过清除过程进行清理

比方说,我们现在有下面的这样的一个Java堆,已经通过可达性分析法来标记出所有的垃圾对象(用橙色表明,蓝色的是普通对象):

然后我们通过清除阶段进行清理,结果是下图:

发现什么问题了吗,没错,清理完后的空间是不连续的,也就是说,整个算法最大的缺点就是:

  • 会出现大量的空间碎片,当需要分配大对象时,会触发FGC,非常消耗性能

这里引出一个FGC的概念,为了避免主题跑偏,本文中暂时不进行深入,只需要知道垃圾回收分为YGC(年轻代垃圾回收)和FGC(完全垃圾回收),可以把YGC理解为扫扫地,倒倒垃圾,把FGC理解为给家里来个大扫除

复制算法

复制算法将Java堆划分为两块区域,每次只使用其中的一块区域,当垃圾回收发生时,将所有被标记的对象(GC Roots可达,为非垃圾对象)复制到另一块区域,然后进行清理,清理完成后交换两块区域的可用性

这种方式因为每次只需要一整块一起删除即可,就不用一个个地删除了,同时还能保证另一块区域是连续的,也解决了空间碎片的问题

整个流程我们再来看一遍

1.首先我们有两块区域S1和S2,标记为灰色的区域为当前激活可用的区域:

2.对Java堆上的对象进行标记,其中蓝色的为GC Roots可达的对象,其余的均为垃圾对象:

3.接下来将所有可用的对象复制到另一块区域中:

4.将原区域中所有内容删除,并将另一块区域激活

这种方法的优缺点也很明显:

  • 优点:解决了空间不连续的问题
  • 缺点:空间利用率低(每次只使用一半)

为了解决这一缺点,就引出了下面这个算法

优化的复制算法

至于为什么不另起一个名字,其实是因为这个算法也叫做复制算法,更确切的说,刚才介绍的只是优化算法的雏形,没有虚拟机会使用上面的那种复制算法,所以接下来要讲的,就是真正的复制算法

这个算法的思路和刚才讲的一样,不过这个算法将内存分为3块区域:1块Eden区,和2块Survivor区,其中,Eden区要占到80%

这两块Survivor区就可以理解为我们刚才提到的S1和S2两块区域,我们每次只使用整个Eden区和其中一块Survivor区,整个算法的流程可以简要概括为:

1.当发生垃圾回收时,将Eden区+Survivor区中仍然存活的对象一次性复制到另一块Survivor区上

2.清理掉Eden区和使用的Survivor区中的所有对象

3.交换两块Survivor的激活状态

光看文字描述比较抽象,我们来看图像的形式:

1.我们有以下这样的一块Java堆,其中灰色的Survivor区为激活状态

2.标记所有的GC Roots可达对象(蓝色标记)

3.将标记对象全部复制到另一块Survivor区域中

4.清理掉Eden区和激活的Survivor区中的所有对象,然后交换两块区域的激活状态

以上就是整个复制算法的全过程了,有人可能会问了,为什么Survivor区这么小,就不怕放不下吗?其实平均来说,每次垃圾回收的时候基本都会回收98%左右的对象,也就是说,我们完全可以保证大部分情况下剩余的对象都小于10%,放在一块Survivor区中是没问题的。当然,也可能会发生Survivor区不够用的问题,这时候就需要依赖其他内存给我们提供后备了

这种算法较好地解决了内存利用率低的问题,但是复制算法的两个问题依然没有解决:

  • 对象复制采用深度优先的递归方式来实现,会消耗栈资源(Cheney改进的GC复制算法解决了该问题)
  • 复制算法无法处理长寿数据,只会频繁地将其复制来白白消耗资源(重点)

标记-整理算法

这种算法可以说是专门针对对象存活率高的程序,具体的流程如下:

1.GC发生时,将所有被标记的存活对象移动到内存的一端

2.移动完成后,清理掉所有移动后的边界以外的对象

我相信大家在理解了前面几个算法之后,这个算法也能很方便地理解,我就不画图了,用一个例子来解释:

问题:对于一个长度为n的数组,我们想要保留其中所有小于10的数字,其余的数字删掉
方案:可以遍历一遍数据,将所有小于10的数字全部放到数组的最左侧,最终,数组的0~m(0<=m<=n)位置全部都是小于10的数字,然后我们只需要删除m+1~n的所有数字即可

这种方法的优点也显而易见:

  • 实现简单,执行速度快
  • 针对复制算法处理不佳的长寿数据,标记-整理算法可以选择不去整理这些对象
  • 没有空间碎片的问题

但是依然还是有缺点的:

  • 如果堆内存较小,则该算法的速度会下降
  • 遍历时需要多次访问类型信息和对象的指针域,开销很大
  • 记录新的转发地址需要占用额外的空间,导致吞吐量下降
  • 不适合并发回收器

分代收集算法

别急,我们还没说完,还有最后一个分代收集算法,这个算法将Java堆划分为两块区域:

  • 年轻代:存放朝生夕灭的对象,即存活率低的对象,大部分对象在一次GC后都会被回收
  • 老年代:存放存活率高的对象

可以看出,分代收集算法按照对象在GC后的存活率将Java堆分为这样两块区域,针对不同区域采用不同的算法,就能尽可能地做到“扬长补短”,来提高垃圾回收的效率

  • 针对年轻代朝生夕灭的性质,我们采用复制算法
  • 针对老年代存活率高的性质,我们采用标记-整理算法

总结

最后,垃圾回收的几种常见算法已经为大家介绍完毕,接下来如果有机会我会再介绍一下几种常见的垃圾回收器

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 快速理解Java垃圾回收和jvm中的stw

    Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外).Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互:这些现象多半是由于gc引起. GC时的Stop the World(STW)是大家最大的敌人.但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象. JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation

  • 基于JVM 中常见垃圾收集算法介绍

    JVM 中常见的垃圾收集算法有四种: 标记-清除算法(Mark-Sweep): 复制算法(Copying): 标记-整理(Mark-Compact): 分代收集: 下面我们来一一介绍: 一.标记-清除算法(Mark-Sweep) 这是最基础的垃圾收集算法,算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象.它的主要缺点有两个:一个是效率问题,标记和清除效率都不高:另一个是空间问题,标记清除后会产生大量不连续的内存

  • 详解Java内存管理中的JVM垃圾回收

    一.概述 相比起C和C++的自己回收内存,JAVA要方便得多,因为JVM会为我们自动分配内存以及回收内存. 在之前的JVM 之内存管理 中,我们介绍了JVM内存管理的几个区域,其中程序计数器以及虚拟机栈是线程私有的,随线程而灭,故而它是不用考虑垃圾回收的,因为线程结束其内存空间即释放. 而JAVA堆和方法区则不一样,JAVA堆和方法区时存放的是对象的实例信息以及对象的其他信息,这部分是垃圾回收的主要地点. 二.JAVA堆垃圾回收 垃圾回收主要考虑的问题有两个:一个是效率问题,一个是空间碎片问题.

  • JVM的垃圾回收算法工作原理详解

    怎么判断对象是否可以被回收? 共有2种方法,引用计数法和可达性分析 1.引用计数法 所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一.当一个对象的引用计数器为零时,说明此对象没有被引用,也就是"死对象",将会被垃圾回收. 引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法

  • JVM垃圾收集器详解

    说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物.事实上,GC的历史远比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言.当List还在胚胎时期时,人们就在思考GC需要完成的3件事情: 哪些内存需要回收? 什么时候回收? 如何回收? 一.哪些内存需要回收? 从JVM区域结构看,可将这些区域划分为"静态内存"和"动态内存"两类.程序计数器.虚拟机栈.本地方法3个区域

  • JVM教程之内存管理和垃圾回收(三)

    JVM内存组成结构 JVM栈由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)堆 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制.堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Space和To Space组成,结构图如下所示: 新生代.新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代大小可以由-Xmn来控制,也可以用-XX:Surv

  • 浅谈jvm中的垃圾回收策略

    java和C#中的内存的分配和释放都是由虚拟机自动管理的,此前我已经介绍了CLR中GC的对象回收方式,是基于代的内存回收策略,其实在java中,JVM的对象回收策略也是基于分代的思想.这样做的目的就是为了提高垃圾 回收的性能,避免对堆中的所有对象进行检查时所带来的程序的响应的延迟,因为jvm执行GC时,会stop the word,即终止其它线程的运行,等回收完毕,才恢复其它线程的操作.基于分代的思想是:jvm在每一次执行垃圾收集器时,只是对一小部分内存 对象引用进行检查,这一小部分对象的生命周

  • 图解JVM垃圾内存回收算法

    前言 首先,我们要讲的是JVM的垃圾回收机制,我默认准备阅读本篇的人都知道以下两点: JVM是做什么的 Java堆是什么 因为我们即将要讲的就是发生在JVM的Java堆上的垃圾回收,为了突出核心,其他的一些与本篇不太相关的东西我就一笔略过了 众所周知,Java堆上保存着对象的实例,而Java堆的大小是有限的,所以我们只能把一些已经用完的,无法再使用的垃圾对象从内存中释放掉,就像JVM帮助我们手动在代码中添加一条类似于C++的free语句的行为 然而这些垃圾对象是怎么回收的,现在不知道没关系,我们

  • LINQ重写博客垃圾图片回收算法

    思路很简单,从所有Blog Model中解析出所有文章使用的图片文件名,排除站外引用,放入一个List<string> usedPicList.再遍历图片上传文件夹,把所有图片文件的结果加入FileInfo[] fiAllPicList.然后比较usedPicList和fiAllPicList,找出所有fiAllPicList中有,而usedPicList中木有的图片,就是未被任何文章引用的垃圾图片了. 原先这个比较算法是用传统方法写的,很蛋疼,用了两重循环,一个标志位才解决问题: 复制代码

  • 深入理解JVM垃圾回收算法

    目录 一.垃圾标记阶段 1.1.引用计数法 (java没有采用) 1.2.可达性分析算法 二.对象的finalization机制 2.1.对象是否"死亡" 三.使用(MAT与JProfiler)工具分析GCRoots 3.1.获取dump文件 3.2.GC Roots分析 四.垃圾清除阶段 4.1.标记-清除算法 4.2.复制算法 4.3.标记-压缩(整理,Mark-Compact)算法 4.4.以上三种垃圾回收算法对比 4.5.分代收集算法 4.6.增量收集算法 4.7.分区算法G1

  • JVM垃圾回收算法的概念与分析

    前言 在JVM内存模型中会将堆内存划分新生代.老年代两个区域,两块区域的主要区别在于新生代存放存活时间较短的对象,老年代存放存活时间较久的对象,除了存活时间不同外,还有垃圾回收策略的不同,在JVM中中有以下回收算法: 标记清除 标记整理 复制算法 分代收集算法 有了垃圾回收算法,那JVM是如果确定对象是垃圾对象的呢?判断对象是否存活JVM也会有几套自己判断算法了: 引用记数 可达性分析 有了垃圾回收和判断对象存在这两个概念后,再来逐步分析它们. JVM是如何判断对象是否存活的? 要是让开发人员来

  • 浅谈JVM垃圾回收有哪些常用算法

    一.前言: 垃圾回收: 在未来的JDK中可能G1会为ZGC所取代 先问自己几个问题: 什么是垃圾? 垃圾就是堆内存中(范指)没有任何指针指向的对象实体.不具有可达性. 为什么要回收垃圾? 因为我们的内存是有限的,内存长时间不清理就会导致内存溢出,OOM: 只要是程序正在跑,那么就不断生成新的对象,我们需要GC开辟新的空间分配给新的对象. 我们怎么回收垃圾? 依靠Java的自动内存回收机制,机制的优劣由算法决定: 或者说是机制的适配度由算法和应用场景共同决定. 什么时候回收垃圾? 当堆中的实体对象

  • 最新JVM垃圾回收算法详解

    目录 1.垃圾回收需要做什么 2.如何判断对象可被回收 2.1 引用计数算法 2.1.2 优点 2.1.2 缺点 2.2 可达性分析算法 2.2.1 算法思路 2.2.2 GC Roots对象(两栈两方法) 2.2.3 优点 2.2.4 缺点 3.判断对象生存还是死亡 3.1 两次标记过程 3.2 finalize()方法 4.HotSpot虚拟机中对象可达性分析的实现 4.1 枚举根节点 4.2 安全点 4.2.1 安全点是什么,为什么需要安全点 4.2.2 安全点的选定 4.2.3 如何在安

  • jvm垃圾回收算法详细解析

    目录 前言 几种常用的垃圾回收算法 1.引用计数法 2.根搜索算法 3.标记清除法(Mark-Sweep) 4.复制交换算法(Mark-Sweep) 5.标记压缩算法(Mark-Compact) JVM 分代收集算法 前言 相比C语言,JVM虚拟机一个优势体现在对对象的垃圾回收上,JVM有一套完整的垃圾回收算法,可以对程序运行时产生的垃圾对象进行及时的回收,以便释放JVM相应区域的内存空间,确保程序稳定高效的运行,但在真正了解垃圾回收算法之前,有必要对JVM的对象的引用做一个简单的铺垫 JVM对

  • 从JVM的内存管理角度分析Java的GC垃圾回收机制

    一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率 ,才能提高整个应用程序的性能.本篇文章首先简单介绍GC的工作原理之后,然后再对GC的几个关键问题进行深入探讨,最后提出一些Java程序设计建议,从GC角度提高Java程序的性能.     GC的基本原理     Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放.     对于程序员来说,分配对象使用

随机推荐