关于GC碎碎念
目前大部分Android开发者能接触到的GC资料局限在八股文/关于Hotspot虚拟机的实现,大部分不适用ART虚拟机关于GC的实现,或者大部分资料只局限在GC的某个点,管中窥豹的方式很难有一个全貌。不了解ART虚拟机的过程也意味着当遇到GC相关的系统问题其实也很难解决。
即使ART虚拟机发展了这么多年,GC收集这一块代码依旧在不断迭代当中,其中特殊情况下依旧有不少段错误的出现,相信各位APP开发者也会遇到。
看完本文你将会学习到
- ART虚拟机中涉及GC算法流程
- ART虚拟机是如何根据不同的内存区域制定不同的GC策略
- GC的整体大纲,以及后续如何深入学习
学习完之后,这些知识可以被运用到:
- android性能优化策略
- 系统知识理解以及运用在自定义OS当中
阅读本文前,希望你已经掌握基本的GC算法的大致介绍,比如标记清除,复制等
ART中关于GC的实现
ART虚拟机的GC流程,从整体上看,其实主要围绕着以下几个点展开:
- GC策略:对接ART内存模型,GC策略如何与ART内存模型对接
- GC范围:如何确定GC的范围,全量扫描还是部分扫描
- GC选择:如何根据场景选择具体的垃圾回收器
- GC流程:标记流程与清除流程,如何采取更加快捷的方式让GC完成
GC策略
ART虚拟机是适用于Android这种手机的特殊定制化虚拟机,相信大家都知道这点,而ART的内存模型,其实决定着GC的策略。
这里说的是一个策略的问题,比如某一块内存区域里面内容不容易/不产生垃圾对象,或者回收效益不大(GC的目的就是要释放更多的可用内存),其实我们就可以不用加入GC的流程。如果某一块区域经常产生gc,那么自然,我们就需要针对这一块区域进行GC,这样才能达到效益最大化。ART虚拟机其实就是依靠这种思想,把进程堆的可用内存,分为了好几块。
这里的内存块,由SpaceType表示
arduino
enum SpaceType {
kSpaceTypeImageSpace,
kSpaceTypeMallocSpace,
kSpaceTypeZygoteSpace,
kSpaceTypeBumpPointerSpace,
kSpaceTypeLargeObjectSpace,
kSpaceTypeRegionSpace,
};
它有以上几种,其中每种SpaceType的具体实现,也可以有多种,比如LargeObjectSpace的实现类根据32/64位架构不同,具体的实现分别为LargeObjectMapSpace/FreeListSpace等。无论这些实现类如何,其实他们代表的space永远都是按照SpaceType表示。
ART虚拟机把内存根据不同的特征,抽象出一个个Space,它是一个抽象类,在创建每一个space的时候,会由具体的子类去根据自己的定义,通过GetType返回返回SpaceType,表示自己属于哪种内存类型
在这里我们先打住,我们知道了ART虚拟机会把堆划分为不同的Space这一个事实,具体的Space承载着什么,可以阅读笔者之前关于ART虚拟机内存模型的解析。本章重点是GC模型,因此我们只需要知道虚拟机有不同的内存块类型即可
在有了这些前置知识之后,我们引出来第一个GC相关的概念,GC的策略
arduino
enum GcRetentionPolicy {
// Objects are retained forever with this policy for a space.
永远不GC
kGcRetentionPolicyNeverCollect,
// Every GC cycle will attempt to collect objects in this space.
每次GC都会尝试清除该区域
kGcRetentionPolicyAlwaysCollect,
// Objects will be considered for collection only in "full" GC cycles, ie faster partial
// collections won't scan these areas such as the Zygote.
"full"gc条件下才会扫描该区域,跟GC的范围有关
kGcRetentionPolicyFullCollect,
};
它为后续GC的范围奠定了扫描基础,举个例子,比如当前内存比较充裕情况下,只需要对那些经常会产生垃圾对象的区域扫描即可,对于那些不怎么产生垃圾对象(有,但是相对少)的Space,就可以不同纳入扫描范围,这样能让GC的整个扫描过程更快
GC的策略约束的是Space,因此Space会在创建的时候,就会根据具体实现的差异,分别制定不同的GcRetentionPolicy
下面我把Space与GcRetentionPolicy分别对应起来,就会得到下面表格。
ImageSpace | kGcRetentionPolicyNeverCollect |
---|---|
MallocSpace | kGcRetentionPolicyAlwaysCollect |
ZygoteSpace | kGcRetentionPolicyFullCollect |
BumpPointerSpace | kGcRetentionPolicyAlwaysCollect |
LargeObjectSpace | kGcRetentionPolicyAlwaysCollect |
RegionSpace | kGcRetentionPolicyAlwaysCollect |
通过对Space设定不同的GC策略,可以十分高效确定GC的扫描范围,让每一次GC都根据自身的特性去扫描特定的Space,比如ZygoteSpace本身垃圾对象不多,因此只需要等到内存不太充足的FullGC条件下,才会被纳入扫描范围。让GC本身的策略更加高效
GC范围/类型
找到垃圾对象这个流程,其实算是一个较为耗时的过程,所以ART虚拟机的设计其实也跟我们常见的思想一样。如果在内存较为充裕的情况下,我们其实可以只回收部分区域的垃圾对象即可,这样更快同时也能够较为有效的回收内存。在内存不足情况下,我们回收策略就是尽可能的回收更多的内存,不然就会导致OOM对吧。这也是ART虚拟机的通用策略,只不过在一些情况下会有小部分的特殊处理罢了
GcType对象就承担着GC的扫描范围范围这么一个过程
arduino
enum GcType {
// Placeholder for when no GC has been performed.
不进行GC
kGcTypeNone,
// Sticky mark bits GC that attempts to only free objects allocated since the last GC.
针对上次存活的对象进行GC
kGcTypeSticky,
// Partial GC that marks the application heap but not the Zygote.
针对heap所有进行GC不包括ZygoteSpace
kGcTypePartial,
// Full GC that marks and frees in both the application and Zygote heap.
针对heap所有进行GC包括ZygoteSpace
kGcTypeFull,
// Number of different GC types.
目前是充当边界检查标记,即校验gctype是否在【kGcTypeNone,kGcTypeMax) 里面,因为gctype是外部传入
kGcTypeMax,
/*
{
比如heap 初始化时校验
DCHECK_LT(gc_type, collector::kGcTypeMax);
DCHECK_NE(gc_type, collector::kGcTypeNone);
}
*/
};
各个GcType 对应的回收范围也不一样,而GcType由GarbageCollector这个垃圾回收器的基类决定。
csharp
GarbageCollector
virtual GcType GetGcType() const = 0;
这里我们其实就可以知道,GarbageCollector有着自己的GC范围,不同的GarbageCollector的实现,也决定了上述讲到的哪些Space内存会被加入扫描。
同样的,ART虚拟机也为GarbageCollector划分了不同的类型,即CollectorType,当然,这里的type就描述着GarbageCollector是基于哪种垃圾回收算法实现的,比如标记清除,标记整理,又或者是复制算法等
arduino
enum CollectorType {
// No collector selected.
kCollectorTypeNone,
// Non concurrent mark-sweep.
kCollectorTypeMS,
// Concurrent mark-sweep.
kCollectorTypeCMS,
// Concurrent mark-compact.
kCollectorTypeCMC,
// The background compaction of the Concurrent mark-compact GC.
kCollectorTypeCMCBackground,
// Semi-space / mark-sweep hybrid, enables compaction.
kCollectorTypeSS,
// Heap trimming collector, doesn't do any actual collecting.
kCollectorTypeHeapTrim,
// A (mostly) concurrent copying collector.
kCollectorTypeCC,
// The background compaction of the concurrent copying collector.
kCollectorTypeCCBackground,
// Instrumentation critical section fake collector.
kCollectorTypeInstrumentation,
// Fake collector for adding or removing application image spaces.
kCollectorTypeAddRemoveAppImageSpace,
// Fake collector used to implement exclusion between GC and debugger.
kCollectorTypeDebugger,
// A homogeneous space compaction collector used in background transition
// when both foreground and background collector are CMS.
kCollectorTypeHomogeneousSpaceCompact,
// Class linker fake collector.
kCollectorTypeClassLinker,
// JIT Code cache fake collector.
kCollectorTypeJitCodeCache,
// Hprof fake collector.
kCollectorTypeHprof,
// Fake collector for installing/removing a system-weak holder.
kCollectorTypeAddRemoveSystemWeakHolder,
// Fake collector type for GetObjectsAllocated
kCollectorTypeGetObjectsAllocated,
// Fake collector type for ScopedGCCriticalSection
kCollectorTypeCriticalSection,
};
具体的垃圾回收器都是GarbageCollector的子类,如果我们想要研究某一类垃圾回收算法在ART的实现,可以通过阅读对应子类的实现进行了解
垃圾回收器中会根据具体的实现,定义好具体的GC范围,同时GC范围也间接影响了哪些Space会被加入垃圾回收。值得一提,垃圾回收器也决定了具体分配器的实现,不同的垃圾回收也对应着不同的内存分配器,这里简单提一下,Heap::ChangeCollector
GC选择
我们认识到了垃圾回收器的相关特征,那么ART虚拟机是如何选择哪种垃圾回收器的呢?总不可能都选择所有垃圾回收器吧。
影响垃圾回收器的选择可以主要有三个大的因素
- 垃圾回收器的默认选择:性能突破,比如在早期Android版本选择CMS进行垃圾回收,而后续高版本中默认采取CC分配方式,因为它性能更好,暂停时间更短等。
- 厂商自定义:比如OS厂商,会有部分厂商尝试自研垃圾回收实现,贴合自研的系统,因此可以忽略默认选项,通过自定义"-Xgc"类型选中特定的垃圾回收器,比如在一些车企OS中会指定即使android高版本的系统通用采取CMS(毕竟CMS经过内部迭代多版)
- 前后台:应用前后台不同(这里指ProcessState)区分,比如前台因为直接与用户进行交互,就会选择垃圾回收更快的方式,而后台因为用户看不到,就可以选择把内存整理一下,腾出更多连续内存(防止内存抖动)
垃圾回收器的决定会在Heap初始化的时候,选择垃圾回收器,需要指定前台垃圾回收器与后台垃圾回收器
其中会根据是否使用读屏障选中默认的CC还是Xgc参数制定的垃圾回收器类型(目前手机AndroidOS大部分都是使用读屏障(其实它的是一个mutex,会在编译被嵌入相关的指令))
自定义gctype会更具-Xgc参数赋值
当然,当发现前后台切换 时,会通过UpdateProcessState函数,改变前后台Collecter的切换
scss
void Heap::UpdateProcessState(ProcessState old_process_state, ProcessState new_process_state) {
if (old_process_state != new_process_state) {
前台jank_perceptible true
const bool jank_perceptible = new_process_state == kProcessStateJankPerceptible;
if (jank_perceptible) {
// Transition back to foreground right away to prevent jank.
RequestCollectorTransition(foreground_collector_type_, 0);
GrowHeapOnJankPerceptibleSwitch();
} else {
// If background_collector_type_ is kCollectorTypeHomogeneousSpaceCompact then we have
// special handling which does a homogenous space compaction once but then doesn't transition
// the collector. Similarly, we invoke a full compaction for kCollectorTypeCC but don't
// transition the collector.
RequestCollectorTransition(background_collector_type_, 0);
}
}
}
垃圾回收器需要指定前后台垃圾回收器,总的大纲就是,在前台环境下,用户对于卡顿会更加敏感,因此需要选择更快的垃圾回收,而后台环境下,卡顿不敏感,因此需要进行内存的整理,便于内存块的整合
GC流程
抛开System.gc引起的主动gc,大部分GC由ConcurrentGCTask与分配时AllocInternalWithGc触发,我们简单看一下ConcurrentGCTask,分配GC我们在内存海绵方案中介绍过了(对于GC流程的追踪,我们就可以通过这个Task触发进行调用链分析)
rust
class Heap::ConcurrentGCTask : public HeapTask {
public:
ConcurrentGCTask(uint64_t target_time, GcCause cause, bool force_full, uint32_t gc_num)
: HeapTask(target_time), cause_(cause), force_full_(force_full), my_gc_num_(gc_num) {}
void Run(Thread* self) override {
Runtime* runtime = Runtime::Current();
gc::Heap* heap = runtime->GetHeap();
DCHECK(GCNumberLt(my_gc_num_, heap->GetCurrentGcNum() + 2)); // <= current_gc_num + 1
heap->ConcurrentGC(self, cause_, force_full_, my_gc_num_);
CHECK_IMPLIES(GCNumberLt(heap->GetCurrentGcNum(), my_gc_num_), runtime->IsShuttingDown(self));
}
private:
const GcCause cause_;
const bool force_full_; // If true, force full (or partial) collection.
const uint32_t my_gc_num_; // Sequence number of requested GC.
};
进行一系列的矫正
less
void Heap::ConcurrentGC(Thread* self, GcCause cause, bool force_full, uint32_t requested_gc_num) {
if (!Runtime::Current()->IsShuttingDown(self)) {
// Wait for any GCs currently running to finish. If this incremented GC number, we're done.
WaitForGcToComplete(cause, self);
if (GCNumberLt(GetCurrentGcNum(), requested_gc_num)) {
collector::GcType next_gc_type = next_gc_type_;
// If forcing full and next gc type is sticky, override with a non-sticky type.
if (force_full && next_gc_type == collector::kGcTypeSticky) {
next_gc_type = NonStickyGcType();
}
// If we can't run the GC type we wanted to run, find the next appropriate one and try
// that instead. E.g. can't do partial, so do full instead.
// We must ensure that we run something that ends up incrementing gcs_completed_.
// In the kGcTypePartial case, the initial CollectGarbageInternal call may not have that
// effect, but the subsequent KGcTypeFull call will.
if (CollectGarbageInternal(next_gc_type, cause, false, requested_gc_num)
== collector::kGcTypeNone) {
for (collector::GcType gc_type : gc_plan_) {
if (!GCNumberLt(GetCurrentGcNum(), requested_gc_num)) {
// Somebody did it for us.
break;
}
// Attempt to run the collector, if we succeed, we are done.
if (gc_type > next_gc_type &&
CollectGarbageInternal(gc_type, cause, false, requested_gc_num)
!= collector::kGcTypeNone) {
break;
}
}
}
}
}
}
堆发起,并进行校验,判断是否真正发起一次GC,并更新下一次gc的类型(比如当前是全量gc且下次gc为sticky情况下,下次gc旧设置为null,节约效率)
force_full 对应的是fullgc 或者 partial gc 取决于垃圾回收器
这里面就是一个效率问题,尽可能触发短快的stick或者一定条件允许下不触发,发起后由对应collect决定。
最终调用CollectGarbageInternal方法,里面会由GarbageCollector的具体子类进行垃圾回收,调用其run方法
垃圾回收器主要做的其实就是两件事:
- 标记可达的内存
- 删除不可达的内存
因为标记可达内存这一步,可以采取多线程的方式进行标记,所以一些Concurrent前缀的策略,其实就是采取多线程的方式加快标记。
标记这里会涉及两个处理:
- 去增 :不要让新的内存进行分配,因为新内存分配容易改变引用链,让其更加复杂,因此大多数采取读锁获取的方式,能让分配器可以读取已有的内存,不至于让gc卡顿,注意,写锁获取仍然被阻塞,比如分配内存。
- 减存 :通过获取写锁,把堆中的垃圾对象清除,获取写锁就是避免让无效的被释放的内存还能被读取或者使用。这一步往往是ART GC的最难的一步,如果产生异常,那么很容易产生段错误SIGSEGV (signal segmentation violation)比如jni操纵数组越界后,会破坏jvm的引用链,造成gc时SIGSEGV。
所以垃圾回收器的本质都是在这两点基础上做出差异优化,我们拿手机os中默认也是主流的ConcurrentCopying垃圾回收器进行解析
scss
void ConcurrentCopying::RunPhases() {
CHECK(kUseBakerReadBarrier || kUseTableLookupReadBarrier);
CHECK(!is_active_);
is_active_ = true;
Thread* self = Thread::Current();
thread_running_gc_ = self;
Locks::mutator_lock_->AssertNotHeld(self);
{
这里获取读锁,也就是说,如果有分配的话才会阻塞,没有就不会阻塞正常流程
ReaderMutexLock mu(self, *Locks::mutator_lock_);
初始化标记,这里会把目标Space与非目标Space进行标记与分离,这里标记的是上一次存活的对象
InitializePhase();
// In case of forced evacuation, all regions are evacuated and hence no
// need to compute live_bytes.
if (use_generational_cc_ && !young_gen_ && !force_evacuate_all_) {
如果启用了分代并且不是年轻代且force_evacuate_all_标记位为false,再次启动标记
MarkingPhase();
}
}
if (kUseBakerReadBarrier && kGrayDirtyImmuneObjects) {
// Switch to read barrier mark entrypoints before we gray the objects. This is required in case
// a mutator sees a gray bit and dispatches on the entrypoint. (b/37876887).
ActivateReadBarrierEntrypoints();
// Gray dirty immune objects concurrently to reduce GC pause times. We re-process gray cards in
// the pause.
ReaderMutexLock mu(self, *Locks::mutator_lock_);
GrayAllDirtyImmuneObjects();
}
FlipThreadRoots();
{
ReaderMutexLock mu(self, *Locks::mutator_lock_);
CopyingPhase();
}
// Verify no from space refs. This causes a pause.
if (kEnableNoFromSpaceRefsVerification) {
TimingLogger::ScopedTiming split("(Paused)VerifyNoFromSpaceReferences", GetTimings());
ScopedPause pause(this, false);
CheckEmptyMarkStack();
if (kVerboseMode) {
LOG(INFO) << "Verifying no from-space refs";
}
VerifyNoFromSpaceReferences();
if (kVerboseMode) {
LOG(INFO) << "Done verifying no from-space refs";
}
CheckEmptyMarkStack();
}
{
ReaderMutexLock mu(self, *Locks::mutator_lock_);
ReclaimPhase();
}
FinishPhase();
CHECK(is_active_);
is_active_ = false;
thread_running_gc_ = nullptr;
}
通过**CheckEmptyMarkStack**();
循环标记,尽可能减少分配线程中存留对象可能引用到了其他对象
最后还是要上写锁,把内存迁移
当然,这里涉及的gc算法,比如染色、读屏障就不具体分析了,这块资料还是有很多的。
GC流程本质就是根据内存情况进行内存删减的流程,这里面就是调用具体的垃圾回收器进行收集,整个垃圾回收器的设计核心都是两部,如何更快且更精确做出去增减存
总结
ART中GC还有不少的内容,不过相信读完这篇文章,我们对GC有了一个更加宏观的全貌,接下来只需要根据需要进行细节的阅读即可。当然,了解后,我们可以在这基础上,开发出各种各样的gc黑科技,从而让APP性能有多的可能。
我是Pika,如果你喜欢我的文章,请不要忘记点赞关注!后面还有一系列鸿蒙/Android文章等你!