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

G1垃圾回收算法概述

垃圾回收都是基于分区进行的。G1在实现垃圾回收时一共提供了3种回收的方法,分别是新生代回收(称为Young GC, YGC),混合回收(称为Mixed GC),全回收(称为Full GC, FGC)。这3种垃圾回收触发的时机通常如下:

1. 应用程序分配对象时,发现内存不足,触发YGC;

2. 在YGC执行中,判断整体内存使用是否大于一定的阈值,如果大于启动并发标记;在并发标记完成后,当下一次启动垃圾回收称为Mixed GC,在Mixed GC执行过程中不仅回收新生代分区,同时也回收部分老生代分区;

3. 在用程序分配对象时,发现内存不足,触发YGC或者Mixed GC;垃圾回收结束后再次尝试分配对象,如果内存还不足,此时将触发FGC。

在整个JVM运行过程中,还可以通过外部命令或者代码触发垃圾回收。

1 Young GC概述

G1对于YGC和Mixed GC的回收采用的复制算法。简单可以总结为:从根出发,标记活跃对象,并将活跃对象复制到新的分区中。在实际实现中,G1为了提高回收效率,将整个回收过程进行了细分,大体上拆分为:并行执行部分和串行执行部分。首先G1针对多个根进行并行的标记复制,然后再进行串行的标记复制。最早G1把引用处理也放在串行执行部分中,后来G1对引用处理做了优化,也可以并行执行,但是引用处理并不能直接放入到并行执行部分(因为引用处理必须在多个根并行执行完成之后才能执行)。所以这里把串行执行部分称为其他部分,在其他部分执行中以串行执行为主,有个别的任务的可以并行执行。

YGC的执行顺序来看一下整个收集过程的主要步骤:

1. 进行收集之前需要执行暂停。

2. 选择分区回收集合(称为CSet),对于YGC来说整个新生代分区都会被放入到CSet。

3. 进入并行任务处理

1) 根扫描并处理;处理过程会把根直接引用的对象复制到新的Survivor区,然后把被引用对象的成员变量入栈等待后续的复制处理。

2) 处理老生代分区到新生代分区的引用;首先会把程序运行过程中需要记录的代际关系引用信息进行保存(代际引用关系通过数据结构RSet来存储,G1中还存在单代的引用处理线程),然后从RSet出发,把RSet所在卡表对应的分区内存块中所有的对象都认为是根,把这些根引用到的对象复制到新的Survivor区,然后把被引用对象的成员变量入栈等待后续的复制处理。

3) JIT代码扫描。

4) 根据栈中的对象,进行深度递归遍历复制对象。

4. 下面是其他任务处理,其他任务大部分都是串行执行

1) JIT代码位置更新,在并行任务中已经对代码进行了扫描和复制,这里会更新相关指针所指向的位置。

2) 引用处理,即把引用中使用的存活对象也要复制到新的分区。

3) 字符串去重优化回收,这个是JDK8 G1新引入的功能。是为了优化字符串使用的效率。

4) 清除卡表,就是把全局卡表中已经处理过的分区对应的卡表清空。

5) JIT代码回收,代码已经可以回收,实际上是删除相关的引用。

6) 转移失败处理,主要的工作就是恢复对象头;

7) 引用再处理,把引用中还活着的对象放入到引用队列中,这个是和引用特殊的设计有关。

8) 进行Redirty,这个主要工作就是重构分区对象的引用关系。

9) 释放回收集合CSet中分区对应的内存,即把这些分区放入到自由列表Free List,供后续使用,这里的后续指的是对象分配时如果需要新的分区,可以直接从Free List获取。

10) 尝试大对象回收。

11) 尝试扩展内存,根据内存的使用情况判断是否可以扩展。

12) 调整新生代分区的数目等;主要是根据GC的执行时间和目标停顿时间预测下次可能发生垃圾回收能接受的最大分区数。


2 Mixed GC概述

Mixed GC包括两个部分,并发标记和垃圾回收。当标记完成后,垃圾回收过程和YGC几乎完全相同,唯一的区别就是在回收的时候,不仅仅回收新生代分区,同时还回收一部分老生代分区(可能需要多次混合回收才能把老生代所有的分区回收完成)。所以着重了解一下并发标记的过程。

并发标记是指并发标记线程和应用程序线程同时运行,它有四个典型的子阶段:初始标记子阶段、并发标记子阶段、再标记子阶段和清理子阶段。

(1)初始标记子阶段

负责标记所有从根集合被直接可达的对象,根集合是对象图的起点,初始标记需要将应用程序线程暂停掉,也就是需要暂停一切。在混合回收中的初始标记子阶段和新生代的初始标记几乎一样。实际上混合回收的初始标记子阶段是借用了新生代回收的结果,即新生代垃圾回收后的新生代Survivor分区作为根,所以混合回收一定发生在新生代回收之后,且不需要再进行一次初始标记。这就是所谓的“借道”。

注意实际上除了YGC发生后的Survivor作为并发标记的根之外,并发标记还有一些根是在YGC中识别的。

(2)并发标记子阶段

当YGC执行结束之后,如果发现满足并发标记的条件之后,并发线程就开始进行并发标记。根据新生代的Survivor分区开始并发标记。并发标记的时机是在YGC后,只有内存消耗达到一定的阈值后,才会触发。在G1中这个阈值通过参数InitiatingHeapOccupancyPercent控制(在JDK8中默认值是45,表示的是当已经分配的内存加上本次将分配的内存超过内存总容量的45%就可以开始并发标记,在JDK 11中对这一参数的含义又做了调整)。多个并发标记线程启动,每个线程每次只扫描一个分区,从而标记出存活对象。在标记的时候还会计算存活对象的数量,同时会计算存活对象所占用的内存大小,并计入分区空间。

并发标记子阶段会对所有分区的对象进行标记。这个阶段并不需要STW,故标记线程和应用程序线程并发运行。为了保证并发标记算法的正确,G1使用Snapshot-At-The-Begining(SATB)算法进行并发标记。

(3)再标记子阶段

再标记是最后一个标记阶段。在该阶段中,G1需要暂停一切,找出所有未被访问的存活对象,同时完成存活内存数据计算。引入该阶段的目的,是为了能够达到结束标记的目标。要结束标记的过程,要满足三个条件:

  • 从根(Survivor)出发并发标记子阶段已经标记出所有的存活对象。

  • 标记栈是空的。

  • 所有的引用变更对象都被处理了;这里的引用变更对象包括新增空间分配的对象和引用变更对象,新增空间所有对象都认为是活跃的(即便是对象已经死亡也没有关系,在这种情况下只是增加了一些浮动垃圾),引用变更处理的对象通过一个队列记录,在该子阶段会处理这个队列中所有的对象。


(4)清理子阶段

再标记子阶段之后是清理子阶段,该子阶段也需要暂停一切。清理子阶段主要执行以下操作:

  • 统计存活对象,统计的结果将会用来排序分区,以用于下一次的垃圾回收时分区的选择。

  • 交换标记位图,为下次并发标记准备。

  • 把空闲分区放到空闲分区列表中;这里的空闲分区指的是全都是垃圾对象的分区;如果分区还有任何活跃对象都不会释放,真正释放的动作是在混合回收中。

该阶段比较容易引起误解的地方在于,清理子阶段并不会清理垃圾对象,也不会执行存活对象的拷贝。也就是说,在极端情况下,该阶段结束之后,空闲分区列表将毫无变化,JVM的内存使用情况也毫无变化。

YGC和Mixed GC是G1种最为常见的垃圾回收方式,整个G1垃圾回收的活动图如图所示。

 

3 Full GC概述

FGC是整个程序运行过程中需要尽量避免发生。FGC的发生将导致应用程序停顿时间不可控。所以这里简单的介绍一下FGC。

FGC采用的标记清除算法,在JDK10之前,G1的FGC采用的串行实现,从JDK10开始,FGC被优化成并行执行。串行和并行的实现基本类似,唯一的不同是,在串行执行时标记清除是针对整个内存,而在并行执行时,多个并行执行的线程按照分区来标记清除。

标记清除算法,主要分为4步:

1. 标记活跃对象。

2. 计算新对象的地址。

3. 把所有的引用都更新到新的地址上。

4. 移动对象。


4 G1中Top10参数

在JDK8中G1共有41个可配置的生成参数,可以在JVM启动时调整相关的参数从而保证JVM运行达到最优。这里提供工作中最常用的10个参数:

  • G1HeapRegionSize,设置分区大小。

  • G1ReservePercent,设置保留空间,用于YGC时对象晋升时使用。

  • ParallelGCThreads,设置垃圾回收时并行工作线程的个数。

  • G1ConcRefinementThreads,设置并发引用工作线程的个数。

  • ConcGCThreads,设置并发工作线程的个数。

  • InitiatingHeapOccupancyPercent,设置并发标记启动的条件。

  • G1MixedGCCountTarget,设置混合回收时,一次回收老生代分区的比例。

  • G1HeapWastePercent,判断是否启动混合回收,当回收分区集合中可用的内存大于该比例才会被启动。

  • G1UseConcMarkReferenceProcessing,是否允许并行的处理引用

  • G1MixedGCLiveThresholdPercent,设置混合回收时,当分区中可用的内存大于该比例才会被回收

关于初始标记中根的介绍、并发标记的SATB算法以及参数的具体介绍可以参考《JVM G1源码分析和性能调优》。

推荐语:资深Java工程师撰写,来自一线实践经验的结晶,详细分析G1的基本运行原理以及调优方法,讲解细腻,图示丰富,可帮助Java工程师深入理解垃圾回收技术 

点击链接了解详情并购买

关于作者: 彭成寒 高级Java工程师,目前主要从事风控系统设计、算法建模、大数据处理等工作。有超过10年的Java和C++开发经验。

文末彩蛋来袭

更多精彩回顾

 书单 | 5月书讯 | 华章IT图书上新啦!重磅新书在线投喂...

干货 | 73页PPT,教你从0到1构建用户画像系统(附下载)
榜单 | 423世界读书日 | 华章精品IT书单独家推荐

收藏 | 这10本书助你从容应对数字化转型中可能出现的各种挑战

相关文章:

  • 【第7期】云计算监控——Prometheus监控系统
  • 一行简单的管道命令快速创建、原型化复杂的功能
  • 数据分析师的案头工具书都在这了
  • 薅当当羊毛的机会又!双!!叒!!!叕!!!来了
  • 520 情人节 :属于Python 程序员的脱单攻略大合集(视频版)
  • 高端科普系列——领略前沿科学的魅力
  • 周志明虚拟机最新版,大厂面试必备宝典
  • 对话阿里云 MVP:跨界半生,不改赤子之心
  • 想一探Greenplum内核的奥秘?这场直播不容错过!
  • 创建字节跳动之前,张一鸣读过哪些硬核技术书?
  • 周志华领衔撰写,历时4年,宝箱书问世!
  • 【第6期】R语言是什么?怎么学?
  • 学数据分析/挖掘应该先学Python,还是R语言?
  • 未来已来,看智能制造如何改变世界 |《铸魂》线上新书发布会
  • 为什么腾讯监控的大数据平台选择了这款数据库?
  • #Java异常处理
  • 4. 路由到控制器 - Laravel从零开始教程
  • 5分钟即可掌握的前端高效利器:JavaScript 策略模式
  • leetcode388. Longest Absolute File Path
  • Sass 快速入门教程
  • SpringCloud(第 039 篇)链接Mysql数据库,通过JpaRepository编写数据库访问
  • STAR法则
  • TypeScript实现数据结构(一)栈,队列,链表
  • Yeoman_Bower_Grunt
  • 马上搞懂 GeoJSON
  • 使用putty远程连接linux
  • 微信小程序:实现悬浮返回和分享按钮
  • 蚂蚁金服CTO程立:真正的技术革命才刚刚开始
  • #Linux(权限管理)
  • (NO.00004)iOS实现打砖块游戏(九):游戏中小球与反弹棒的碰撞
  • (编程语言界的丐帮 C#).NET MD5 HASH 哈希 加密 与JAVA 互通
  • (力扣记录)235. 二叉搜索树的最近公共祖先
  • (四)模仿学习-完成后台管理页面查询
  • (心得)获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。
  • (新)网络工程师考点串讲与真题详解
  • (转)JVM内存分配 -Xms128m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=512m
  • (轉)JSON.stringify 语法实例讲解
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • .NET简谈设计模式之(单件模式)
  • .net连接oracle数据库
  • .net专家(张羿专栏)
  • /usr/local/nginx/logs/nginx.pid failed (2: No such file or directory)
  • @Controller和@RestController的区别?
  • [<MySQL优化总结>]
  • [1525]字符统计2 (哈希)SDUT
  • [BZOJ 3680]吊打XXX(模拟退火)
  • [EULAR文摘] 脊柱放射学持续进展是否显著影响关节功能
  • [go 反射] 进阶
  • [HNOI2010]BUS 公交线路
  • [Linux]history 显示命令的运行时间
  • [luogu2165 AHOI2009] 飞行棋 (枚举)
  • [LWC] Components Communication
  • [Python学习笔记][Python内置函数]
  • [RK3568 Android11] 时间同步机制
  • [spark] DataFrame 的 checkpoint