浅析鸿蒙的GC垃圾回收
本文介绍了GC(垃圾回收)算法的基本原理和实现方式,重点分析了ArkTS运行时采用的HPP GC机制。主要内容包括:1)GC的两种基本类型(引用计数和对象追踪)及其优缺点;2)对象追踪算法的三种实现方式(标记-清扫、标记-复制、标记-整理);3)HPP GC的分代模型设计、混合算法策略和流程优化;4)Heap内存结构及各空间功能;5)HPP GC的三种类型(Young GC、Old GC、Full
GC算法简述
GC的类型
引用计数
当对象B指向对象A时,A的引用计数加1;当该指向断开时,A的引用计数减1。如果A的引用计数为0,则回收对象A。
- 优点:引用计数算法设计简单,而且会在对象成为垃圾时及时回收该部分内存,因此无需引入单独的暂停业务代码(Stop The World,STW)阶段。
- 缺点:在对象操作时插入了计数环节,增加了内存分配和赋值的开销,影响性能。存在因循环引用而导致的内存泄漏问题。
class Parent {
constructor() {
this.child = null;
}
child: Child | null = null;
}
class Child {
constructor() {
this.parent = null;
}
parent: Parent | null = null;
}
function main() {
let parent: Parent = new Parent();
let child: Child = new Child();
parent.child = child;
child.parent = parent;
}
在上述代码中,对象parent被对象child持有,parent的引用计数加1。同时,child也被parent持有,child的引用计数也会加1。这形成了循环引用,导致直到main函数结束,parent和child都无法释放,从而引发内存泄漏。
对象追踪

根对象包括程序运行中的栈内对象和全局对象等当前时刻一定存活的对象。从根对象开始,通过引用链可以访问到的所有对象**(可达对象)也是存活的**。通过遍历可以找到所有存活对象。如图所示,从根对象开始遍历,所有可达对象标记为蓝色,即为活对象。剩下的不可达对象标记为黄色,即为垃圾。
- 优点:对象追踪算法可以解决循环引用问题,并且对内存的分配和赋值没有额外开销。
- 缺点:和引用计数算法相比,对象追踪算法较为复杂,有短暂的STW阶段。而且回收有延迟,会导致较多的浮动垃圾。
引用计数和对象追踪算法各有优劣。由于引用计数存在内存泄漏问题,ArkTS运行时选择基于对象追踪(即Tracing GC)算法设计GC。
对象追踪的三种类型
对象追踪算法通过遍历对象标记出垃圾,而根据垃圾回收方式的不同,对象追踪可以分为三种基本类型:标记-清扫回收、标记-复制回收、标记-整理回收。下图中蓝色标记为可达对象,黄色标记为不可达对象。
标记-清扫回收

完成对象图遍历后,删除不可达对象内容,并将其放入空闲队列,以便下次对象分配。
该回收方式不搬移对象,效率高。但回收对象内存地址不连续,导致内存碎片化,降低分配效率。极端情况下,即使有大量空闲内存,也可能无法放入较大对象。
标记-复制回收

遍历对象图时,将可达对象复制到新内存空间。遍历完成后,回收旧内存空间。
这种方式可以解决内存碎片问题,通过一次遍历完成整个GC过程,效率较高。但在极端情况下,需要预留一半内存空间以确保所有活动对象都可以被拷贝,这会导致空间利用率较低。
标记-整理回收

完成对象图遍历后,将可达对象(蓝色)复制到本区域或指定区域的头部空闲位置,然后将已复制的对象回收整理到空闲队列中。
- 优点:解决了“标记-清扫回收”导致的大量内存碎片问题,避免了“标记-复制回收”浪费一半内存空间。
- 缺点:和“标记-复制回收”相比,性能开销较高。
HPP GC
HPP GC(High Performance Partial Garbage Collection),即高性能部分垃圾回收,其中“High Performance”主要体现在分代模型、混合算法和GC流程优化这三个方面。HPP GC根据不同对象区域采取不同的回收方式。
分代模型
ArkTS运行时采用传统的分代模型,将对象进行分类。大多数新分配的对象会在一次GC后被回收,而大多数经过多次GC后依然存活的对象会继续存活。ArkTS运行时将对象划分为年轻代和老年代对象,并分配到不同空间。

ArkTS运行时将新分配的对象直接分配到年轻代(Young Space)的From空间。经过一次GC后依然存活的对象,会移动到To空间。经过再次GC后依然存活的对象,会被移动到老年代(Old Space)。
混合算法
HPP GC是部分复制、部分整理和部分清扫的混合算法。根据年轻代和老年代对象特点,采取不同的回收方式。
- 部分复制
- 考虑到年轻代对象****生命周期短、回收频繁且大小有限,ArkTS运行时对年轻代对象采用“标记-复制回收”算法。
- 部分整理+部分清扫
- 根据老年代对象的特点,引入启发式Collection Set(简称CSet)选择算法。该算法在标记阶段统计每个区域的存活对象大小,然后在回收阶段优先选择存活对象少、回收代价小的区域进行对象整理回收,再对剩余区域进行清扫回收。
回收策略如下:
- 根据设定的区域存活对象大小阈值,将满足条件的区域纳入初步的CSet队列,并根据存活率进行从低到高的排序(注:存活率=存活对象大小/区域大小)。
- 根据设定的释放区域个数阈值,选出最终的CSet队列,进行整理回收。
- 对未被选入CSet队列的区域进行清扫回收。
启发式CSet选择算法结合了“标记-整理回收”和“标记-清扫回收”算法的优点,避免了内存碎片问题,同时提升了性能。
流程优化
HPP GC流程中引入了大量的并发和并行优化,以减少对应用性能的影响。采用了并发+并行标记(Marking)、并发+并行清扫(Sweep)、并行复制/整理(Evacuation)、并行回改(Update)和并发清理(Clear)执行GC任务。
Heap结构及其配置参数
Heap结构

- Young Space:年轻代(Young Generation),存放新创建出来的对象,存活率低,主要使用复制算法进行内存回收。
- OldSpace:老年代(Old Generation),存放年轻代多次回收仍存活的对象会被移动到该空间,根据场景混合多种算法进行内存回收。
- HugeObjectSpace:大对象空间,使用单独的Region存放一个大对象的空间。
- ReadOnlySpace:只读空间,存放运行期间的只读数据。
- NonMovableSpace:不可移动空间,存放不可移动的对象。
- SnapshotSpace:快照空间,转储堆快照时使用的空间。
- MachineCodeSpace:机器码空间,存放程序机器码。
GC流程

HPP GC的类型
Young GC
只回收年轻代中的短命对象。
频繁、快速(耗时最短),对应用性能影响最小。
- 触发机制:年轻代GC触发阈值在2MB-16MB,根据分配速度和存活率变化。
- 说明:主要回收semi Space(半空间)复制算法新分配的年轻代对象。
- 场景:前台场景。
- 日志关键词:[ HPP YoungGC ]
Old GC
回收年轻代和部分老年代。
频率较低,但耗时比Young GC长(5ms~10ms),因为它会做“全量标记”。
- 触发机制:老年代GC触发阈值在20MB到300MB之间变化。通常,第一次Old GC的阈值约为20MB,之后会根据对象存活率和内存占用情况进行调整。
- 说明:对年轻代和部分老年代空间做整理压缩,其他空间做sweep清理。触发频率比年轻代GC低很多,由于会做全量mark,因此GC时间会比年轻代GC长,单次耗时约5ms~10ms。
- 场景:前台场景。
- 日志关键词:[ HPP OldGC ]
Full GC
全面回收年轻代和老年代,最大限度地释放内存。
耗时最长,对性能影响最大。不会在前台主动触发,只在后台或特定工具操作时触发。
- 触发机制:不会由内存阈值触发。应用切换到后台场景之后,若预测可回收对象大小超过2M,则会触发一次Full GC。DumpHeapSnapshot和AllocationTracker工具默认会触发Full GC。Native接口和ArkTS接口也可触发。
- 说明:对年轻代和老年代做全量压缩,主要用于性能不敏感场景,最大限度回收内存。
- 场景:后台场景。
- 日志关键词:[ CompressGC ]
此后,Smart GC或IDLE GC会从上述三种GC中选择。
触发策略
空间阈值触发GC
- 函数方法:AllocateYoungOrHugeObject,AllocateHugeObject等分配函数。
- 限制参数:对应的空间阈值。
- 说明:对象申请空间到达阈值时触发GC。
- 典型日志:日志可区分GCReason::ALLOCATION_LIMIT。
native绑定大小达到阈值触发GC
- 函数方法:GlobalNativeSizeLargerThanLimit
- 限制参数:globalSpaceNativeLimit
- 说明:影响是否进行全量mark以及是否开启并发mark。(不是直接触发一次GC**,而是 影响GC的行为(是进行全量标记还是并发标记)**)。
全量mark、并发mark
标记-清除(Mark-Sweep) 算法的大致流程,它通常分为两个阶段:
- 标记(Mark): 从一组“根对象”(如全局变量、活动线程栈上的变量等)开始,遍历对象图,将所有仍然存活的对象打上一个标记。
- 清除(Sweep): 遍历整个堆内存,将所有没有标记的对象(即垃圾)回收,并将其占用的空间放回空闲列表。
而 全量Mark 和 并发Mark 主要区别就发生在 标记(Mark) 这个阶段。
全量Mark
- 核心定义: 在GC执行标记阶段时,需要暂停所有应用程序线程(这个过程被称为 “Stop-The-World”),然后由GC线程独自、完整地遍历整个对象图,完成所有存活对象的标记。
并发Mark
- 核心定义: 在GC执行标记阶段时,不需要完全暂停应用程序线程,而是让GC线程与应用程序线程同时运行。
切换后台触发GC
- 函数方法:ChangeGCParams
- 说明:切换到后台场景后主动触发一次Full GC。
- 典型日志:app is inBackground 和 app is not inBackground。
- GC 日志中可区分GCReason::SWITCH_BACKGROUND。
执行策略
ConcurrentMark(并发标记)
减少主线程卡顿。
- 函数方法:TryTriggerConcurrentMarking
- 说明:尝试触发并发mark,将遍历对象进行标记的任务交由线程池中并发运行,减少UI主线程挂起时间。
- 典型日志:fullMarkRequested,trigger full mark,Trigger the first full mark,Trigger full mark,Trigger the first semi mark,Trigger semi mark。
new space GC前后的阈值调整
自适应优化年轻代。
-
函数方法:AdjustCapacity
-
说明:GC后,调整SemiSpace的触发水线,优化空间结构。
-
对比Semi Space(半空间复制算法)和标记-整理算法
Semi Space(半空间复制算法)
- 核心原理: 复制
- 内存布局: 将可用堆内存一分为二,每次只使用其中一半(From Space),另一半空闲(To Space)。
- 工作过程:
- 标记: 从根集合(如全局变量、活动线程栈等)开始,遍历并标记所有在 From Space 中存活的对象。
- 复制/清除: 将标记到的存活对象,全部复制到空的 To Space 中。在复制过程中,对象在 To Space 侧被紧凑地排列在一起。
- 角色互换: 复制完成后,整个 From Space 被一次性清空(因为所有存活对象都已移走),然后 From Space 和 To Space 角色互换。
- 关键特征:
- 通过复制来实现回收和紧凑。
- 总是需要一半的闲置内存。
- 原空间(From Space)在回收后被完全清空。
标记-整理算法
- 核心原理: 标记 + 滑动整理
- 内存布局: 使用一整块连续的内存空间。
- 工作过程:
- 标记: 与 Semi Space 和标记-清除算法一样,首先标记出所有存活的对象。
- 整理: 将所有存活的对象“滑动”到内存空间的一端。这个过程像是在整理书架,把散落的书都推到一边,紧凑地排列起来。
- 更新引用: 整理完成后,更新所有指向被移动对象的指针。
- 清除: 最后,将整理后剩余的另一端内存空间(现在全是空闲的)一次性回收。
- 关键特征:
- 通过在原内存空间内滑动对象来实现紧凑。
- 不需要闲置一半内存,内存利用率更高。
- 整理过程通常比复制要慢,因为它涉及更多内存地址的计算和更新。
-
-
典型日志:无直接日志。可以通过GC统计日志看出,GC前Young space的阈值有动态调整。
第一次OldGC后阈值的调整
为后续老年代计算更合理的触发阈值,从而给老年代设定一个合理的初始容量。
- 函数方法:AdjustOldSpaceLimit
- 说明:根据最小增长步长以及平均存活率调整OldSpace阈值限制。
- 日志关键词:AdjustOldSpaceLimit
第二次及以后的OldGC对old Space和global space阈值调整,以及增长因子的调整
持续优化所有内存空间的限制。
- 函数方法:RecomputeLimits
- 说明:根据当前 GC 统计数据的变化,重新计算并调整newOldSpaceLimit、newGlobalSpaceLimit、globalSpaceNativeLimit及增长因子。
- 日志关键词:RecomputeLimits
Partial Old GC的CSet 选择策略
核心概念解释
- Partial GC(部分垃圾回收): 不是回收整个老年代,而是只回收老年代中的一部分区域。这样做是为了减少单次GC的暂停时间。
- Region: 为了支持Partial GC,老年代的内存被划分成多个固定大小的块,每个块称为一个 Region。
- CSet(Collection Set,回收集合): 在一次Partial GC中,被选定要进行回收的那些Region的集合,就叫做CSet。
策略详解
- 执行函数:
OldSpace::SelectCSet() - 选择策略: 优先选择“性价比”最高的Region进行回收。具体标准是:
- 存活对象数量少
- 回收代价小
为什么这么选?
因为回收一个Region,需要将其中的存活对象复制到另一个地方(通常是其他Region)。如果一个Region里全是垃圾(存活对象为0),回收它代价为0,收益最大。如果一个Region里全是存活对象,回收它需要复制所有对象,代价很高,收益却很小(几乎回收不了内存)。所以,GC会像一个精明的经理,优先处理那些“投入少、产出高”的任务。
- 函数方法:OldSpace::SelectCSet()
- 说明:PartialGC执行时,优先选择存活对象数量少、回收代价小的Region进行回收。
- 典型日志:
- Select CSet failure: number is too few
- 选择CSet失败:符合条件的Region数量太少了
- Max evacuation size is 6_MB. The CSet Region number
- 最大疏散大小是6MB。CSet包含的Region数量是
- Select CSet success: number is
- 选择CSet成功:数量是
- Select CSet failure: number is too few
SharedHeap
SharedHeap结构

- SharedOldSpace:共享老年代空间(不区分年轻代老年代),存放一般的共享对象。
- SharedHugeObjectSpace:共享大对象空间,使用单独的Region存放一个大对象的空间。
- SharedReadOnlySpace:共享只读空间,存放运行期间的只读数据。
- SharedNonMovableSpace:共享不可移动空间,存放不可移动的对象。
注:SharedHeap用于线程间共享对象,提高效率并节省内存。共享堆不单独属于任何线程,保存具有共享价值的对象,提高对象的存活率,去除了SemiSpace类型。
特性:Smart GC
在应用性能敏感场景,通过将线程(SmartGC对worker线程和taskpool线程不生效)GC触发水线临时调整到线程的堆最大值(主线程默认448MB),尽量避免触发GC导致应用掉帧。如果敏感场景持续时间过久,对象分配已经达到了堆最大值,则还是会触发GC,且这次GC由于积累的对象太多,GC时间会相对较久。
支持敏感场景
- 应用冷启动(默认支持)。
- 应用滑动。
- 应用点击页面跳转。
- 超长帧。
该特性使能由系统侧进行管控,三方应用暂无接口直接调用。
日志关键词: SmartGC

标记性能敏感场景。在进入和退出性能敏感场景时,在堆上标记,避免不必要的GC,维持高性能表现。
在堆管理器的内部状态中,设置一个全局的标志位,用以指示当前是否处于性能敏感时期。这个标志位会直接改变内存分配器和垃圾回收器的行为逻辑,使其在敏感期内采用一套极度宽松的、以避免GC为最高目标的触发策略,从而保证应用的流畅度。
更多推荐



所有评论(0)