IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Java垃圾收集机制 -> 正文阅读

[Java知识库]Java垃圾收集机制

1. JVM自动内存管理机制

Java虚拟机的自动内存管理机制使得我们不用像C、C++那样,需要在程序中考虑如何分配内存以及何时回收它们。这样做的好处是不容易出现内存泄漏和内存溢出问题,但由于Java把内存控制的权利交给了Java虚拟机,一旦出现内存泄露和溢出的问题就会比较难以排查。因此,了解Java的垃圾收集机制是十分有必要的。

总的来说,垃圾收集器需要关注以下三件事情:

  • 判断哪些内存是需要回收的;

  • 什么时候进行垃圾回收;

  • 如何进行回收。

第一件事情不难理解,在回收对象之前当然是要判断出有哪些对象是可以被回收的,否则后续的工作就无法继续进行。

由于垃圾回收是需要占据一定的计算机资源的,同时在一些步骤中还需要暂停用户线程,所以我们需要在合适的时间进行回收,以免对用户程序造成过大的影响。一般来说,对象实例化时如果没有足够的内存供其申请,就会发生垃圾回收。

要回收对象,就肯定需要具体的回收方法,针对此,不同的垃圾收集器给出了不同的解决方法。总体上来说它们并无“好坏”之分,可以根据具体的场景选择合适的垃圾收集器。

2. 可达性分析算法

当前主流的商用程序语言的内存管理系统都是基于可达性分析算法实现的,Java也不例外。可达性分析算法的功能是判断对象是否存活(即是否可以被回收),其基本思路就是从一些可以判定是存活着的对象出发,搜索它们的引用链,将引用链中涉及到的对象都进行标记,那么剩下的对象就是可以被回收的对象。

总结一下,我们可以知道,可达性分析的关键在于:

  • 根对象(GC Roots)的枚举:枚举出所有的可以判定存活的对象;

  • 引用链的搜索:从根对象出发,标记其所直接或间接引用的对象。

2.1 枚举 GC Roots

作为可达性分析的基础操作,我们首先需要枚举出所有的 GC Roots 对象。在 Java 中,固定可以作为 GC Roots 的对象包括:

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

  • 在本地方法栈中(即通常所说的Native方法)引用的对象。

  • 类中的静态变量所引用的对象;

  • 常量,如字符串常量等;

  • 所有被同步锁持有的对象;

  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还会有其它的对象被加入,共同构成完整GC Roots集合。例如基于分代回收或局部回收的垃圾收集器,每次相当于只对部分内存进行了垃圾回收,那么这时候其它内存区域的对象也需要被考虑进去。

2.2 引用链的搜索

引用链是指以GC Roots为起始节点集,根据引用关系向下进行搜索,搜索过程中所走过的路径就是引用链。也就是说,在引用链上的对象是可达的,而其他不在引用链上的对象就是不可达的。

最简单的方法就是根据当前内存的快照,从GC Roots开始进行引用链的搜索。早起的垃圾收集器(如Serial、ParNew等)就是这么干的。但这种方法有一个问题就是需要基于一个能保障一致性的快照中才能够进行搜索分析,即对象间的引用关系是不能被改变的,否则就可能会引起错误。这就导致在这个过程中需要暂停用户线程,以保持引用的“不变性”。

长时间的暂停用户线程在一些情况下是难以接受的,当然也有一些改进的方法,比如说在同时使用多条线程进行垃圾收集(ParNew、Parallel Scavenge等),但这并不能从根本上解决问题。

想要“解决”这个问题,我们首先需要了解下可达性分析的过程,了解在什么情况下对象间引用关系发现变化后会造成分析的错误。这里规定:

  • 白色的对象表示可达性分析过程中未被访问过的对象;

  • 灰色的对象表示已经被访问过的对象,但这个对象上至少有一个引用还没有被访问过

  • 黑色的对象表示已经被访问过的对象,且这个对象的所有引用都被访问过了

一个简单的所搜中的快照可以是下面这样的:

在这里插入图片描述

现在我们考虑这种情况:搜索到上图中灰色对象的时候,它下方的白色对象的引用关系突然改变了,“移动”到了黑色对象上,就像下面这样(红色虚线表示新的引用关系):

在这里插入图片描述

这时候就会产生错误,被移动的白色对象明明是“可达”的,但在引用链搜索标记的过程中却没有标记到它,也就是说它会被当做是“垃圾”被收集掉,这样就会产生“对象消失”的问题。

通过上面的分析可以知道,产生“对象消失”问题必须满足以下两个条件:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;

  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

因此,只要破坏了这两个条件其中的任意一个,就可以解决“对象消失”的问题。由此分别产生了两种解决方案:增量更新(破坏第一个条件)和原始快照(破坏第二个条件)。

  • 增量更新(CMS就是基于增量更新):当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等扫描结束之后,再将这些记录过的引用关系重新扫描一次。

  • 原始快照(G1就是基于原始快照):当灰色对象指向白色对象的引用关系被删除时,就将这个被删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系重新扫描一次。

以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的(写屏障类似于一个虚拟机层面的AOP操作)。在垃圾收集的过程中,从GC Roots开始搜索并标记的整个过程是耗时较长的,通过增量更新或原始快照,引用链搜索的过程中就无需暂停用户线程(一开始的枚举GC Roots和最后的通过增量更新或原始快照进行重新标记的过程还是需要暂停用户线程的,但这相对来说速度是很快的)。

3. 分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了分代收集理论进行设计,其基于两个符合大多数程序运行实际情况的经验法则:

  • 弱分代假说:绝大多数对象都是朝生夕灭的;

  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

基于收集理论,一般来说可以将Java堆分为新生代老年代。在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

正如一开始所说的,除了固定的GC Roots,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还会有其它的对象被加入。假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。

但遍历无疑会带来很大的性能负担,这时候就需要分代理论的第三条经验法则:

  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。

4. 垃圾收集算法

4.1 标记清除算法

标记清除算法主要分为标记清除两个阶段:

  • “标记”阶段需要标记出所有的存活对象(或者死亡的对象),关于如何标记可以参考前文的可达性分析部分;

  • 清除阶段就是对死亡的对象进行清除(如果标记的是存活的对象,就清除剩余的对象;如果标记的是死亡的对象,那就清除所有被标记的对象)。

标记清除算法是后两种(标记复制、标记整理)算法的基础,后面的算法都是基于此进行改进得到的。既然提到了改进,就说明这种算法存在其缺点:

  • 如果Java堆中包含大量需要清除的对象,就需要大量的标记和清除操作,造成执行效率的降低;

  • 标记清除算法会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

4.2 标记复制算法

标记复制算法基于标记清除算法改进得到,其基本流程如下:

  • 将内存分为两部分,每次只是用其中的一部分;

  • 当发生GC时,就将存活的对象复制到另一部分中去,然后将原来存放对象的那部分内存全部清除。

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。标记复制算法很好的解决了标记清除算法中内存碎片的问题,但它有一个缺点就是内存没有被充分的利用(总是有一部分内存是空闲的)。但基于之前提到的,新生代中的对象都是朝生夕灭的,我们可以控制“空闲内存”的大小以减轻内存资源的浪费。

Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略:

  • 将新生代分为一块较大的Eden空间和两块较小的Survivor空间;

  • 每次分配内存只是用Eden空间和一块Survivor空间;

  • 发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。

这种方案很好的控制了内存浪费的问题,每次只有一块较小的Survivor空间是闲置的。HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局,其默认的Eden和Survivor的大小比例是8∶1,即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%)。

当然,我们无法完全保证每次垃圾收集后存活的对象都是很少的(即一个Survivor中可以被放下)。因此,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。

4.3 标记整理算法

标记复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费大量的内存空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

针对老年代对象的存亡特征,1974年Edward Lueders提出了另外一种有针对性的标记整理算法:

  • 首先对存活的对象进行标记;

  • 将所有存活的对象都移动到内存空间的一端,然后直接清理掉“边界以外”的对象。

标记复制算法的好处是不会产生内存碎片,但它需要移动存活对象并更新其引用,这个过程是需要暂停用户线程的。

5. 常见的垃圾收集器

5.1 Serial & Serial Old

Serial(新生代)/ Serial Old(老年代):

  • Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。

  • 是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(Stop The World)。

  • 新生代:基于标记复制算法

  • 老年代:基于标记整理算法

5.2 ParNew

ParNew:

  • ParNew是Serial收集器的多线程版本

  • 收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致;

  • ParNew是不少运行在服务端模式下的HotSpot虚拟机首选的新生代收集器。

5.3 Parallel Scavenge & Parallel Old

Parallel Scavenge(新生代)/ Parallel Old(老年代):

  • 与ParNew非常相似,但其关注的重点是吞吐量,目标是达到一个可控制的吞吐量(吞吐量 = 用于运行用户代码的时间 / 总消耗时间);

  • 新生代:标记复制

  • 老年代:标记整理

5.4 CMS(Concurrent Mark Sweep)

CMS:

  • 基于标记清除算法

  • 是一种以获取最短回收停顿时间为目标的收集器;

基本步骤:

  • 初始标记:枚举 GC Roots,仅标记 GC Roots 直接关联的对象,速度很快;(Stop The World)

  • 并发标记:从 GC Roots 直接关联的对象出发遍历整个对象图,时间很长,但不需要停顿用户线程,且是并发的(具体原理在 2.2 节中有描述);

  • 重新标记:对并发标记进行修正,时间相对也很快;(Stop The World)

  • 并发清除:清理标记阶段被判定已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

  • 优点:“并发低停顿收集器”,并发收集、低停顿;

  • 缺点:

    • 对处理器资源非常敏感,虽然并发阶段不会停止用户线程,但是占用其一部分资源;

    • 基于“标记清除”算法,会产生大量的空间碎片(CMS收集器默认会在不得不进行Full GC时开启内存碎片的合并整理过程);

5.5 G1 (Garbage First)

G1:

  • 开创了收集器面向局部收集的设计思路和基于Region的内存布局形式,目标是建立可预测的停顿时间模型;

  • 之前的垃圾收集器收集的范围要么是新生代,要么是老年代,再要么就是整个Java堆;G1则不同,它可以面向堆内存任何部分来组成回收,衡量的标准不再是分代,而是看哪块内存垃圾最多,回收的收益最大;

  • 虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间;

  • Region中还有一类特殊的Humongous区域,专门用来存储大对象;

  • G1将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,因此能建立可预测的停顿时间模型。

参考书目

深入理解Java虚拟机(第3版)

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-03-11 21:59:56  更:2022-03-11 22:01:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 10:27:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码