前言
在作者的上一篇文章《Android R常见GC类型与问题案例》中,对Demo应用的Heap堆结构与Space类型及相对应内存分配算法做了简要的探究,同时对Android R机器运行中常见的GC类型和具体卡顿案例进行了细致的梳理,对Android系统和应用开发人员了解学习ART GC运行策略和优化具体GC类问题具有一定的借鉴参考意义。
承接上一篇文章,本文将对JVM垃圾回收和CC回收器简单介绍,因文章篇幅原因,对CC回收器的细节本文不过多展开,敬请期待作者下一篇对CC回收器具体实现分析的文章,本文重点介绍GC的任务和工程化的挑战,以及CC回收器的特点和优势。
术语
为了后面叙述方便,首先明确以下名词的含义:
u Concurrent: 并发,指回收线程和应用线程可同时运行
u Parallel: 并行,指多个回收线程同时进行垃圾回收工作
u Collector: 进行垃圾回收的线程
u Mutator: 修改器线程,指任何可以修改Heap的线程,一般指应用业务线程
u RootSet: 根集对象集合(全局类、线程上下文类)
u RememberSet: 记忆集,一种数据结构记录堆内跨代、跨区引用
u CardTable: 记忆集的一种实现,一块连续内存地址段对应一个Card,当内存段内的对象存在跨区、跨代引用,标记为dirty_card。
u Tracing: 可达性分析的追踪过程,从根对象标记引用链上的所有对象
u PauseTime: GC时挂起Mutator线程的时间,CC只有一次Pause,在FlipThreadRoots中
u CMS: Concurrent Mark Sweep--并发标记清除回收器
u CC: Concurrent Copying--并发复制回收器
u Partial GC: 部分回收,CC中指回收所有region和其关联的space
u Sticky\Young\Minor GC: 新生代回收,CC中指只回收新生代region
u Full GC: 全堆回收
u 起始、原始快照:追踪过程中,记录被删除的引用,将被删除的引用指向的对象加入标记栈,最后再追踪下此对象(并发阶段死亡的对象也会标记为存活,即漂浮垃圾,下次GC再回收)-G1、CC采用
u 增量更新:追踪过程中,记录新增引用,将引用被修改过的对象加入到标记栈,最终再追踪下此对象,并发阶段死亡对象能被回收,漂浮垃圾少,但需要一个额外的STW最终标记-CMS采用
一、GC的难题(挑战)
在李晓峰的《虚拟机设计与实现:以JVM为例》一书中,提出合格的回收器必须做到的三个方面:
u 正确性:GC不会丢失任何存活对象。任何需要的对象都不能被当成垃圾对象被回收,否则可能出现逻辑错误和进程崩溃。
u 进步性:GC不会保留任何垃圾对象太长时间。即垃圾对象最终都将被清除,短暂地保留一些漂浮垃圾不是问题。
u 可结束:确保Tracing阶段可结束。Tracing阶段的遍历或递归不会出现死循环。
在GC算法的业界研究和工程化应用中,重点是如何做到:
u 更小的PauseTime,对应的提出并发回收、并行回收和分代回收
u 更高的吞吐量,吞吐量=回收的内存/回收耗时
u 更低的内存分配耗时,常见分配算法BumpPointer\Dlmalloc\Rosmalloc\FreeList
u 更少的内存碎片(高效内存整理),如后台进程进行内存整理,对拆分为多个region管理等
u 更高的内存利用率,如可达分析与引用计数混合实现,精准的垃圾回收区域策略等
u 更小的GC数据内存占用开销,如CardTable\MarkBitmap的实现
u 漂浮垃圾回收策略和对象消失的处理手段,对应增量更新、起始快照的实现
二、GC的任务
自动回收进程运行过程中不再需要的垃圾对象(也称孤立对象)。在这个任务中有几个重点:
l 垃圾对象的判定
l 垃圾回收实现方式
l 降低GC对业务的影响(对应上一条:GC的难题)
1)垃圾对象的判定
垃圾对象也称为死亡或孤立对象,相反的则是存活或活跃对象。要寻找垃圾对象,首先的前提:所有对象只有两种状态,不是存活就是死亡。所有对象的合集为存活对象合集和垃圾对象合集组成,没有第三种集合。
主流的有两种算法来判定垃圾对象:
引用关系 图示
A) 引用计数法(RC:Reference counting)
引用计数法的思想是确定哪些对象是死的,在对象从存活到死亡的过程中回收内存,因对象死亡时间不定,所以引用计数类GC具有时刻回收、内存利用率高的特点。
如上图示,引用计数法类的GC会记录每个对象的引用次数,当引用次数为0时,代表此对象没有被任何对象引用,即认为是垃圾对象。引用计数类GC有以下优势:
u 实现简单
u 引用减少至0时,实时回收内存,内存利用率高
u 回收操作可并发运行,无需暂停应用线程
除了优势,在实现运用上也有以下主要弊端:
u 对象环状循环引用(见上图右侧),此类型的对象无法被回收(开源社区有一些复杂方案能解决此场景)
u Space overhead: 每个对象需要格外空间存储引用次数
u Speed overhead: 每次引用修改需要增加指令,且多线程情况下需要加锁保证原子操作
因上诉弊端,尤其是环状引用和overhead方面,主流工程化应用的GC都没有选择引用计数法,而是选择可达性分析法。
B) 可达性分析法
可达性分析法的思想是找出哪些对象是活的,活的对象确定后,其他的都是死亡对象(垃圾对象),通过一次批处理完整的清除垃圾对象,回收垃圾对象占用的内存。因此在一次GC的垃圾对象回收前,可以认为所有对象都是存活对象。
如上图示,左边已确定的根集对象出发直接引用和间接引用到的对象都是存活对象,因为根集中的根对象是系统运行和业务逻辑必须的对象,被根对象直接或间接引用的对象当然需要保留。
确认到所有从根集对象直接和间接的对象合集(引用关系图\对象邻接图)后,其他与根对象无关联的对象则被判定为垃圾对象,可以直接进行回收。
可达性分析类GC有以下优势:
u 不存在循环引用问题
u 不存在引用计数法的空间和运行时开销
当然,可达性分析法也有弊端:
u 内存利用率低(在GC回收内存前,垃圾对象占用的内存无法复用)
u 设计复杂,若需要支持并发回收需要额外数据结构支撑
u PauseTime: 根集枚举与引用Tracing过程一般需要暂停应用线程至少一次,大部分回收器要暂停2-3次
虽然引用计数法和可达性分析法的思想和实现完全不同,但两者其实是互补关系,在业界不乏一些优秀的GC回收器实现整合了两个算法的优点,来提高内存利用率和减小暂停时间。
*这篇文章分析的CC回收器使用的是可达性分析法。
2)垃圾回收的实现方式
在周志明的《深入理解Java虚拟机》一书中,展示了下面几种经典垃圾回收算法:
u 分代收集
建立在以下三个假说之上
弱分代假说:绝大多数对象都是朝生夕死
强分代假说:熬过越多次GC过程的对象越难以死亡
跨代引用假说:跨代引用相对于同代引用来说仅占极少数
因此,商业虚拟机中一般都会将Java Heap堆划分成不同的区域,按照年龄大小将对象聚集在一起,匹配有差异化的回收策略,以提高GC吞吐量和提高内存利用率。
u 标记-清除算法
特点:实现简单,GC耗时短,但会产生大量内存碎片
u 标记-复制算法
特点:解决内存碎片问题,有对象复制的开销,且牺牲一半可用内存
u 标记-整理算法
特点:不存在内存碎片和内存浪费,对象复制与引用更新开销巨大,整理阶段一般需要挂起应用线程
a. ART中各回收器的算法体现
在Android 7(N)及以前的几个Android版本中,应用在前台时使用CMS(并发标记清除)回收器,在前台时重点关注内存回收速度。应用在后台时使用HSC(同构空间压缩)回收器,实质是标记-整理算法的一种实现。
在Android 8(O)之后,应用在前后台都是用CC(并发复制)回收器,应用在前台也能整理内存,减少内存碎片化。应用在后台时,可以最大程度地进行内存整理。在Android 10(Q)版本重新引入了分代回收,每个的Region可能是老年代或者新生代。
Android各版本回收器性能对比(数据源自Google I/O 19)
表中对比发现,CC回收器在内存分配速度、内存整理等方面都有全面的提升,本文后续章节将探究CC回收器的实现原理和策略。
3)降低GC对业务的影响
基于可达性分析实现的GC无法避免的是PauseTime(STW:stop the world),主流的回收器都在致力于减少PauseTime。不支持并发的回收器进行垃圾回收时,需要先暂停进程的所有线程,等待GC线程进行根集遍历(Root visit)、Tracing标记和回收后,进程才能恢复运行。如此长的PauseTime对于用户体验是非常致命的。
为了优化PauseTime,商用回收器基本都采用并发(Concurrent)实现,比如CMS\G1\CC等。
并发机制可以极大的减少PauseTime,一般只需要在枚举线程上下文的根对象才需要暂停线程,比如ART虚拟机的CC回收器只在枚举线程根对象时暂停一次。
并发也带来了实现逻辑的复杂化,需要增加多个关键数据和机制来维护并发期间的数据准确。主要是解决以下的难题:
u 并发期间引用更新的处理,Collector Tracing过程中Mutator持续修改引用
u 存在跨代引用和跨区引用,并持续被Mutator修改
在CC回收器实现中,采用并发机制,极大地减少了PauseTime,依据google官方数据,PauseTime平均为0.4ms,且不随堆大小而变化,只与Roots的数量有关。
CC回收器GC过程 图示
三、CC的实现原理与特点
CC回收器并无独创性的理论突破,借鉴的是开源社区成熟的GC理论和应用成果,结合Android设备和堆内存特点进行的实现应用。主要优化内存分配速度与内存碎片,核心理论突破是将整个堆分为多个同等大小的内存段,化整为零再分而治之。
先回顾下采用CC回收器的app堆结构图。
1)回收流程概述
u InitializePhase,初始化统计量,设置标记等。
u MarkingPhase,遍历所有Root根集,将根集对象直接引用对象压入mark_stack,并发标记存活对象记录在mark_bitmap中(此阶段只用于确定回收区域,StickyGC和ExplicitGC不需要)。
u FlipThreadRoots,依据上一阶段的标记信息,确定回收区域(倾向于回收垃圾对象多的region),回收区域设置为from_space。
u CopyingPhase,处理跨代、跨区引用的dirty_card,将from_space中的存活对象拷贝至to_space,更新引用等。
u ReclaimPhase,释放from_space的空间,重置为to_space。
2)PartialGC与StickyGC流程图示
u Sticky GC
u Partial GC
3)涉及算法
u 内存分配
整个堆划分为多个同样大小的Region(256KB),Region内分配内存使用最高效的BumpPointer指针碰撞算法,缺点是无法单独释放某个对象的内存(漂浮垃圾无法避免),只能整个释放Region占用的空间。
u 回收算法
全局层面为标记-整理算法,两个Region间为标记-复制算法。有效地减少内存碎片。
u 跨区引用
写屏障支持,用CardTable实现RememberSet,记录跨代/特殊Space与RegionSpace间的引用。
u 并发
由CardTable,转发指针,起始快照,目标空间不变(只访问to_space对象)等特性支持。
4)参考设计
借鉴G1/Shenandoah回收器思想,化整为零,分代收集。
5)基础原理
u 写屏障(两种类型:写前\写后)
修改对象的引用型成员变量时执行一段特殊代码。CC中引用修改会将对象关联的Card标记为Dirty,用于记录自上次GC之后和GC并发期间的引用变化。
u 读屏障
读取对象的引用型成员变量访问目标对象时执行一段特殊代码。CC中通过目标对象的monitor_成员变量关联的LockWord对象判断目标对象是否被拷贝。未拷贝则Mutator将目标对象进行拷贝并修改原对象的LockWord使其指向新对象,且将对象的引用修正为新的目标对象。
u 转发指针
被拷贝后的from_space原对象LockWord配置转发指针,之后所有访问原对象都会自动指向to_space新对象。
6)优势
u 指针碰撞分配,贡献最高的内存分配速度。
u 前后台内存整理,更少的内存碎片,可控的漂浮垃圾数量。
u 更灵活的内存管理单位,提升内存利用率,相比CMS降低平均32%的堆占用(google官方数据)。
u 更精准的分代垃圾回收策略,提升吞吐量(回收效率),减少PauseTime。
7)不足
u 预留两倍堆空间的内存地址(32位应用影响明显,增加虚拟内存地址不足OOM概率)。
u 并发复制过程耗时随堆大小、对象数递增,耗时较长。
u Mutator线程参与对象拷贝和Tracing,存在对象移动风暴。
u 读写互斥锁与锁堆特殊场景阻塞应用线程。
u 无法单独释放某个对象的内存,并发过程中新增对象默认为存活,存在一定的漂浮垃圾。
四、总结
本文重点着力于阐述GC并发所面对的问题和对应的优化手段,结合Android ART虚拟机中CC回收器的实现原理和回收流程,简短的概述文章,希望读者看完能增加对GC的认识。GC理论发展几十年,依靠CPU算力提升和以空间换时间等经典思想,逐步地将GC对应用线程的影响一步步降低。有个说法JAVA开发者无需过多关注GC,但系统开发者和应用架构师掌握虚拟机内存管理机制是很有必要的,对于指导业务优化和实现高级功能都有助益。
作者下一篇文章将CC回收器具体代码进一步剖析,Stay Tuned!
参考资料
1.《虚拟机设计与实现:以JVM为例》--李晓峰
2.《深入理解Java虚拟机》--周志明
3.Android ART并行拷贝垃圾回收--王允臣
4.Android GC简史--掘金:二手认知
5.Understanding Android Runtime (ART) for faster apps (GoogleI/O'19)--youtube
6.ConcurrentCopying Google I/O '17--youtube
7.Model Checking Copy Phases of Concurrent Copying Garbage Collection withVarious Memory Models - YouTube
8.Java 垃圾回收权威指北--刘家财
9.深入浅出垃圾回收--刘家财
10.JVM 三色标记 增量更新 原始快照
长按关注内核工匠微信
Linux 内核黑科技 | 技术文章 | 精选教程