当前位置: 首页 > news >正文

Java垃圾回收机制和三色标记算法

一、对象内存回收

对于对象回收,需要先判断垃圾对象,然后收集垃圾。

收集垃圾采用垃圾收集算法和垃圾收集器。

判断垃圾对象,通常采用可达性分析算法。

引用计数法

每个对象设置一个引用计数器。每被引用一次,计数器就加1,取消引用,计数器就减1,任何时候计数器为0的对象就不可能在被使用了,可被回收。

实现简单、效率高,但不能解决循环引用的问题,A引用B、B引用A,但不被其他对象引用,计数器都为1,虽不会在被使用,但不会被回收。

可达性分析算法

从GC Root起搜索引用的对象,并进行标记,被标记的对象即为存活对象,其他未被标记的对象为垃圾对象。

GC Root对象:线程栈的本地对象、静态变量、本地方法栈的变量等。

二、垃圾收集算法

分代收集理论

当代虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分新生代、老年代,根据各个年代的特点使用合适的垃圾收集算法。

理论依据:99%的对象都是朝生夕死

新生代:每次收集会回收大量对象,所以选择复制算法,复制少量的对象即可完成垃圾收集(其余对象直接清理掉)。

老年代:对象存活几率高,同时没有额外的空间进行分配担保,所以选择标记-清除算法标记-整理算法(比复制算法慢10倍以上)。

1、标记-复制算法

将内存分为相同的两块,每次使用一块。垃圾回收时将存活的对象复制到另一块,并清理掉该块全部的内容。

**优点:**回收效率高,每次只需要复制极少一部分对象,清除对象的时候直接情况当前内存块的全部内容;

**缺点:**只能在两块内存空间中的一块上进行分配内存。

结合以上优缺点,在hotspot的实现中,设计了eden:s1:s2=8:1:1,即利用了回收效率高的优点,又一定程度上避免了内存使用率低的问题(仅有十分之1的内存块不能同时使用)。

3、标记-清除算法

算法分为标记清除阶段

两种实现方式:

  • 标记存活的对象,回收其他所有未被标记的对象(一般采用这种);
  • 标记出垃圾对象,标记完成后统一回收所有被标记的对象。

缺点:

  • 空间碎片问题,标记清除后会产生大量不连续的碎片;
  • 标记效率不高,如果需要标记的对象太多,效率会低。
4、标记-整理算法

算法分为标记和移动两个阶段,标记阶段同标记-清除算法,标记之后存活的对象移动到内存空间的一端,然后清理掉端边界以外的内存。

最终使对象连续存储,空闲空间在另一侧,不在有内存碎片。

三、垃圾收集器

  • 垃圾收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。
  • 没有最好和通用的垃圾收集器,结合垃圾收集器的特点,根据应用场景选择合适的垃圾收集器。

在这里插入图片描述

1、Serial串行垃圾收集器
  • 年轻代使用Serial,jvm开启参数:-XX:+UseSerialGC,采用复制算法

  • 老年代使用Serial Old,jvm开启参数:-XX:+UseSerialOldGC,采用标记-整理算法

使用单线程执行垃圾收集工作,进行垃圾收集工作时暂停所有的工作线程

**优点:**简单、没有线程交互的开销

**缺点:**比较多线程的垃圾收集器,回收效率自然就慢

在这里插入图片描述

由图可知:始终仅有一个GC线程,同时不会贺应用程序线程并行。

2、Parallel Scavenge收集器

Parallel垃圾收集器实际就是Serial收集器的多线程版本,相比Serial垃圾收集器使用了多线程进行垃圾收集,其余类似。

是JDK8默认的新生代贺老年代收集器。

默认的收集线程与CPU核数相同,可通过参数-XX:ParallelGCThreads指定收集线程数,但一般不建议修改。

  • 年轻代使用Parallel,JVM开启参数:-XX:+UseParallelGC,采用复制算法

  • 老年代使用Parallel Old收集器,JVM开启参数:-XX:+UseParallelOldGC,采用标记-整理算法

优点:Parallel Scavenge收集器关注的是吞吐量(高效率利用CPU)

​ 提供了很多参数供用户找到最合适的停顿时间或最大的吞吐量(可将内存管理优化由JVM自行完成)。

在这里插入图片描述

由图可知:Parallel收集器GC线程有多个,可并发进行垃圾收集,但不会应用程序线程并行运行。

3、ParNew收集器

用于新生代的垃圾收集器,采用复制算法,开启参数:-XX:+UseParNewGC

跟Parallel收集器类似,区别主要是ParNew可配合CMS使用。

只有它能与CMS收集器配合工作。

在这里插入图片描述

4、CMS收集器

Concurrent Mark Sweep:并发 标记 清除

用于老年代的垃圾收集器,采用标记-清除算法,JVM开启参数:-XX:+UseConcMarkSweepGC

优点:CMS收集器关注获取最短回收停顿时间,注重用户体验。

​ 真正意义的并发收集器,第一次实现了垃圾收集线程与用户线程同时工作。

工作过程

  1. 初始标记:暂停工作线程,只标记由GC Root直接引用的对象,速度很快。
  2. 并发标记:与工作线程并发运行,从GC Root直接引用的对象开始遍历整个对象图。
    • 耗时较长但不需要停顿用户工作线程;
    • 由于用户线程继续运行,可能会导致已标记的对象状态发生改变;
  3. 重新标记:暂停工作线程,做重新标记,修正并发标记期间用户线程运行导致标记产生变动的一部分对象,主要处理漏标问题。用三色标记法的增量更新算法
    • 漏标:说明没有把需要存活的对象标记为存活状态,如果执行清理,程序会崩溃;
    • 多标:本可以清理的对象标记为了存活**(浮动垃圾)**,不影响系统运行,最多占点空间,可在下一次垃圾收集过程中清理掉。
  4. 并发清理:与工作线程并发运行,GC线程对未标记为存活的对象做清理。
    • 对于该过程新增的对象标记为黑色,本过程不做任何处理;
  5. 并发重置:与工作线程并发运行,重置本次GC过程中标记的数据。
    在这里插入图片描述

由图可知:

​ CMS一次垃圾收集由5个步骤,分别是:初始标记、并发标记、重新标记、并发清理、并发重置

​ 其中初始标记和重新标记将暂停工作线程,不与工作线程并发运行

优点:并发收集、停顿低,用户体验好;

缺点

  • 会与工作线程抢占CPU资源;

  • 无法处理浮动垃圾,因为在并发标记和并发清理过程中可能会产生垃圾,只能等到下一次GC清理;

  • 回收算法“标记-清理算法”会产生大量内存碎片

    • 可通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
  • 会出现"concurrent mode failure",此时CMS会退化成serial old垃圾收集器。

    • 在并发标记和并发清理过程中,由于工作线程在同时运行,可能又导致内存不足触发full GC

CMS的相关核心参数

  1. -XX:+UseConcMarkSweepGC:启用cms

  2. -XX:ConcGCThreads:并发的GC线程数

  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)

  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次

  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比):尽早执行垃圾收集,降低出现"concurrent mode failure"发生的可能。

  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整。

  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段**(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)**时的开销,一般CMS的GC耗时 80%都在标记阶段。

  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW。

  9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW。

四、优化

很多优化无非就是让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc

只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值。

五、垃圾收集底层算法实现-三色标记

在并发标记过程中,因为期间应用线程还在继续跑,对象的引用可能发生变化,会存在多标和漏标的情况。

**多标:**把本该是垃圾的对象标记为存活,产生浮动垃圾。

  • 产生情况:在并发标记过程中,由于方法结束导致部分局部变量(GC Root)被销毁,这个GC Root之前又被标记为非垃圾对象,那么本轮GC不会回收这部分对象。
  • 不会影响垃圾回收的正确性,只需要等到下一轮垃圾回收。
  • 解决办法:当前不用解决,可由下一次进行收集。

**漏标:**把本该存活的对象没有标记为存活,导致本该存活的对象被删除,导致严重bug,必须解决。解决办法:主要引入了三色标记算法解决。

🔺三色标记算法:

三色标记算法把GC Roots可达性分析遍历对象过程中遇到的对象,按照

“是否访问过”标记为三种颜色:

  • **黑色:**表示对象已经被垃圾收集器访问过,且这个对象的所有引用的对象都已经扫描。
    • 黑色的对象代表已经访问过,且是安全存活的。
    • 如果其他对象引用指向了黑色对象,无须重新扫描一遍。
    • 黑色的对象不会不经过灰色对象直接引用白色对象。
  • **灰色:**表示对象已经被垃圾收集器访问过,但这个对象的所引用的对象至少还有一个未被扫描。
  • **白色:**表示对象尚未被垃圾收集器访问过。
    • 在可达性分析刚开始阶段所有对象均为白色标记。
    • 若可达性分析结束对象仍为白色则表示对象不可达。
处理漏标的两种方案

增量更新**(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)**

增量更新:当黑色对象新增加了指向白色对象的引用关系时,将新插入的引用记录下来。等并发扫描结束之后将记录的引用以黑色对象为根重新扫描一次**(重新标记)**。

  • 因为黑色对象一旦插入了引用白色对象,就不能在是黑色了,该变成白色对象了,所以要记录并重新标记。
  • 为了使在并发标记过程中新加入的白色对象也被扫描到,否则会被误认为是非存活对象而被删除。

原始快照:当**删除灰色对象指向的白色对象的引用**时,将删除的引用记录下来。在并发扫描结束之后以灰色对象为根,重新扫描一次,这样便能扫描到白色对象。

  • 直接将白色对象标记为黑色。(该对象可能确实可以回收,而产生浮动垃圾)
  • 如果指向该白色对象的来自灰色对象的引用全部被删除,则该白色对象就不能被扫描到,但如果新增了来自黑色对象的引用,则该白色对象不该被删除。
    • 因为没有判断是否新增了来自黑色对象的引用,所以直接标记为黑色对象,让其在下一次GC时扫描判断。
读写屏障

虚拟机对新增引用关系和删除引用关系的记录,都是通过写屏障实现的。

写屏障:就是在赋值操作前后加入一些处理,把引用记录下来。

读屏障:当读取成员变量时,记录读取到的对象。

在HotSpot虚拟机中,并发标记时处理漏标的方案:

  • CMS:写屏障+增量更新
  • G1:写屏障+SATB

其他功能:

  1. 写屏障可用于记录跨代/区引用的变化;
  2. 读屏障可用于支持移动对象的并发执行
思考:并发标记处理漏标的方案中,为什么G1用SATB,CMS用增量更新?
SATB相对增量更新效率更高。
原始快照在重新标记阶段直接把记录的白色对象标记为黑色,不需要再次深度扫描背删除对应对象;增量更新会记录的黑色对象为根重新扫描一次。
而,CMS的老年代区域为一块内存,G1的对象分布在不同的region中,G1重新深度扫描代价会高于CMS。
所以G1选择SATB不做深度扫描,只是简单标记,浮动垃圾对象交给下一次GC。
记忆集和卡表

在新生代的垃圾收集进行GC可达性分析时,需要知道对象有没有被老年代的对象引用。

为了保证垃圾收集的效率,不能把所有老年代的对象扫描一遍,为此引用了**记忆集(Remember Set)**的数据结构,记录非收集区对象指向收集区对象的引用,避免直接扫描老年代。

在垃圾收集的扫描标记过程中,只需要通过记忆集判断对象是否有被非收集区对象引用,如果有标记对象不可回收。

此外,不只是新生代和老年代之间有跨代引用的问题,所有部分收集的垃圾收集器,如G1,都会涉及跨区引用的问题。

🔺hotspot虚拟机使用**卡表(cardtable)**的方式实现记忆集:

卡表使用一个字节数组实现,CARD_TABLE[],每个元素对应着其标识的内存区域一块特定大小的内存块,称为”卡页“。

一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表元素标识为1,表示该元素变脏,否则为0,GC时只要筛选本收集区的卡表中变脏的元素加入到GC ROOTs中。(不明白,卡页对应一批对象?一个为脏,全页为脏?)

Hotspot中使用写屏障维护卡表状态。
在这里插入图片描述

http://www.dtcms.com/a/263086.html

相关文章:

  • MySQL EXPLAIN 关键字详解
  • python学习打卡day58
  • 使用 C++ 和 OpenCV 构建驾驶员疲劳检测软件
  • Java设计模式之结构型模式(外观模式)介绍与说明
  • jenkins集成sonarqube(使用token进行远程调用)
  • 使用Python进行数据库交互:从SQL查询到ORM操作的安全实践指南
  • 【王阳明代数讲义】二十四史语料库与意气实体过程学说导引
  • 大学专业科普 | 云计算、大数据
  • 将 h264+g711a存为 mp4文件,记录
  • FreePDFv3.0.0:颠覆你的文献阅读习惯
  • 【RTSP从零实践】3、实现最简单的传输H264的RTSP服务器
  • GORM 高级查询实战:从性能优化到业务场景解决方案
  • 华为云Flexus+DeepSeek征文 | ​​接入华为云ModelArts Studio大模型 —— AI智能法务解决方案革新法律实践​
  • x86-64架构和aarch64架构的区别解读
  • 【ARM】解决ArmDS的工程没有生成Map文件的问题
  • 使用 Kafka 优化物流系统的实践与思考
  • 信息安全工程师考试架构相关说明
  • C语言之文件操作详解(文件打开关闭、顺序/随机读写)
  • Python Ai语音识别教程
  • 2 大语言模型基础-2.2 生成式预训练语言模型GPT-2.2.2 有监督下游任务微调-Instruct-GPT强化学习奖励模型的结构改造与维度转换解析
  • Spring生态:云原生与AI的革新突破
  • Linux 系统管理:高效运维与性能优化
  • FastAPI—学习1
  • 本地服务器部署后外网怎么访问不了?内网地址映射互联网上无法连接问题的排查
  • CppCon 2018 学习:A Little Order! Delving into the STL sorting algorithms
  • MySQL索引原理-主键索引与普通索引
  • 【软考高项论文】论信息系统项目的干系人管理
  • ACT-R 7.28
  • pbootcms程序运行异常: Modulo by zero,位置:/www/wwwroot/****/core/function/helper.php
  • 链表题解——设计链表【LeetCode】