likes
comments
collection
share

ART虚拟机GC 全貌

作者站长头像
站长
· 阅读数 8

关于GC碎碎念

目前大部分Android开发者能接触到的GC资料局限在八股文/关于Hotspot虚拟机的实现,大部分不适用ART虚拟机关于GC的实现,或者大部分资料只局限在GC的某个点,管中窥豹的方式很难有一个全貌。不了解ART虚拟机的过程也意味着当遇到GC相关的系统问题其实也很难解决。

即使ART虚拟机发展了这么多年,GC收集这一块代码依旧在不断迭代当中,其中特殊情况下依旧有不少段错误的出现,相信各位APP开发者也会遇到。

看完本文你将会学习到

  1. ART虚拟机中涉及GC算法流程
  2. ART虚拟机是如何根据不同的内存区域制定不同的GC策略
  3. GC的整体大纲,以及后续如何深入学习

学习完之后,这些知识可以被运用到:

  1. android性能优化策略
  2. 系统知识理解以及运用在自定义OS当中

阅读本文前,希望你已经掌握基本的GC算法的大致介绍,比如标记清除,复制等

ART中关于GC的实现

ART虚拟机的GC流程,从整体上看,其实主要围绕着以下几个点展开:

  1. GC策略:对接ART内存模型,GC策略如何与ART内存模型对接
  2. GC范围:如何确定GC的范围,全量扫描还是部分扫描
  3. GC选择:如何根据场景选择具体的垃圾回收器
  4. GC流程:标记流程与清除流程,如何采取更加快捷的方式让GC完成

GC策略

ART虚拟机是适用于Android这种手机的特殊定制化虚拟机,相信大家都知道这点,而ART的内存模型,其实决定着GC的策略。

这里说的是一个策略的问题,比如某一块内存区域里面内容不容易/不产生垃圾对象,或者回收效益不大(GC的目的就是要释放更多的可用内存),其实我们就可以不用加入GC的流程。如果某一块区域经常产生gc,那么自然,我们就需要针对这一块区域进行GC,这样才能达到效益最大化。ART虚拟机其实就是依靠这种思想,把进程堆的可用内存,分为了好几块。

这里的内存块,由SpaceType表示

enum SpaceType {
  kSpaceTypeImageSpace,
  kSpaceTypeMallocSpace,
  kSpaceTypeZygoteSpace,
  kSpaceTypeBumpPointerSpace,
  kSpaceTypeLargeObjectSpace,
  kSpaceTypeRegionSpace,
};

它有以上几种,其中每种SpaceType的具体实现,也可以有多种,比如LargeObjectSpace的实现类根据32/64位架构不同,具体的实现分别为LargeObjectMapSpace/FreeListSpace等。无论这些实现类如何,其实他们代表的space永远都是按照SpaceType表示。

ART虚拟机把内存根据不同的特征,抽象出一个个Space,它是一个抽象类,在创建每一个space的时候,会由具体的子类去根据自己的定义,通过GetType返回返回SpaceType,表示自己属于哪种内存类型

ART虚拟机GC 全貌

在这里我们先打住,我们知道了ART虚拟机会把堆划分为不同的Space这一个事实,具体的Space承载着什么,可以阅读笔者之前关于ART虚拟机内存模型的解析。本章重点是GC模型,因此我们只需要知道虚拟机有不同的内存块类型即可

在有了这些前置知识之后,我们引出来第一个GC相关的概念,GC的策略

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分别对应起来,就会得到下面表格。

ART虚拟机GC 全貌

ImageSpacekGcRetentionPolicyNeverCollect
MallocSpacekGcRetentionPolicyAlwaysCollect
ZygoteSpacekGcRetentionPolicyFullCollect
BumpPointerSpacekGcRetentionPolicyAlwaysCollect
LargeObjectSpacekGcRetentionPolicyAlwaysCollect
RegionSpacekGcRetentionPolicyAlwaysCollect

通过对Space设定不同的GC策略,可以十分高效确定GC的扫描范围,让每一次GC都根据自身的特性去扫描特定的Space,比如ZygoteSpace本身垃圾对象不多,因此只需要等到内存不太充足的FullGC条件下,才会被纳入扫描范围。让GC本身的策略更加高效

GC范围/类型

找到垃圾对象这个流程,其实算是一个较为耗时的过程,所以ART虚拟机的设计其实也跟我们常见的思想一样。如果在内存较为充裕的情况下,我们其实可以只回收部分区域的垃圾对象即可,这样更快同时也能够较为有效的回收内存。在内存不足情况下,我们回收策略就是尽可能的回收更多的内存,不然就会导致OOM对吧。这也是ART虚拟机的通用策略,只不过在一些情况下会有小部分的特殊处理罢了

GcType对象就承担着GC的扫描范围范围这么一个过程

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这个垃圾回收器的基类决定。

GarbageCollector

virtual GcType GetGcType() const = 0;

这里我们其实就可以知道,GarbageCollector有着自己的GC范围,不同的GarbageCollector的实现,也决定了上述讲到的哪些Space内存会被加入扫描。

同样的,ART虚拟机也为GarbageCollector划分了不同的类型,即CollectorType,当然,这里的type就描述着GarbageCollector是基于哪种垃圾回收算法实现的,比如标记清除,标记整理,又或者是复制算法等

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的实现,可以通过阅读对应子类的实现进行了解

ART虚拟机GC 全貌

垃圾回收器中会根据具体的实现,定义好具体的GC范围,同时GC范围也间接影响了哪些Space会被加入垃圾回收。值得一提,垃圾回收器也决定了具体分配器的实现,不同的垃圾回收也对应着不同的内存分配器,这里简单提一下,Heap::ChangeCollector

GC选择

我们认识到了垃圾回收器的相关特征,那么ART虚拟机是如何选择哪种垃圾回收器的呢?总不可能都选择所有垃圾回收器吧。

影响垃圾回收器的选择可以主要有三个大的因素

  1. 垃圾回收器的默认选择:性能突破,比如在早期Android版本选择CMS进行垃圾回收,而后续高版本中默认采取CC分配方式,因为它性能更好,暂停时间更短等。
  2. 厂商自定义:比如OS厂商,会有部分厂商尝试自研垃圾回收实现,贴合自研的系统,因此可以忽略默认选项,通过自定义“-Xgc”类型选中特定的垃圾回收器,比如在一些车企OS中会指定即使android高版本的系统通用采取CMS(毕竟CMS经过内部迭代多版)
  3. 前后台:应用前后台不同(这里指ProcessState)区分,比如前台因为直接与用户进行交互,就会选择垃圾回收更快的方式,而后台因为用户看不到,就可以选择把内存整理一下,腾出更多连续内存(防止内存抖动)

垃圾回收器的决定会在Heap初始化的时候,选择垃圾回收器,需要指定前台垃圾回收器与后台垃圾回收器

ART虚拟机GC 全貌

其中会根据是否使用读屏障选中默认的CC还是Xgc参数制定的垃圾回收器类型(目前手机AndroidOS大部分都是使用读屏障(其实它的是一个mutex,会在编译被嵌入相关的指令)) ART虚拟机GC 全貌

ART虚拟机GC 全貌 自定义gctype会更具-Xgc参数赋值

ART虚拟机GC 全貌

当然,当发现前后台切换时,会通过UpdateProcessState函数,改变前后台Collecter的切换

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触发进行调用链分析)

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.
};

进行一系列的矫正

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方法

ART虚拟机GC 全貌

垃圾回收器主要做的其实就是两件事:

  1. 标记可达的内存
  2. 删除不可达的内存

因为标记可达内存这一步,可以采取多线程的方式进行标记,所以一些Concurrent前缀的策略,其实就是采取多线程的方式加快标记。

标记这里会涉及两个处理:

  1. 去增:不要让新的内存进行分配,因为新内存分配容易改变引用链,让其更加复杂,因此大多数采取读锁获取的方式,能让分配器可以读取已有的内存,不至于让gc卡顿,注意,写锁获取仍然被阻塞,比如分配内存。
  2. 减存:通过获取写锁,把堆中的垃圾对象清除,获取写锁就是避免让无效的被释放的内存还能被读取或者使用。这一步往往是ART GC的最难的一步,如果产生异常,那么很容易产生段错误SIGSEGV (signal segmentation violation)比如jni操纵数组越界后,会破坏jvm的引用链,造成gc时SIGSEGV。

所以垃圾回收器的本质都是在这两点基础上做出差异优化,我们拿手机os中默认也是主流的ConcurrentCopying垃圾回收器进行解析

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**();

循环标记,尽可能减少分配线程中存留对象可能引用到了其他对象

最后还是要上写锁,把内存迁移

ART虚拟机GC 全貌

当然,这里涉及的gc算法,比如染色、读屏障就不具体分析了,这块资料还是有很多的。

GC流程本质就是根据内存情况进行内存删减的流程,这里面就是调用具体的垃圾回收器进行收集,整个垃圾回收器的设计核心都是两部,如何更快且更精确做出去增减存

总结

ART中GC还有不少的内容,不过相信读完这篇文章,我们对GC有了一个更加宏观的全貌,接下来只需要根据需要进行细节的阅读即可。当然,了解后,我们可以在这基础上,开发出各种各样的gc黑科技,从而让APP性能有多的可能。

我是Pika,如果你喜欢我的文章,请不要忘记点赞关注!后面还有一系列鸿蒙/Android文章等你!

转载自:https://juejin.cn/post/7341652882112872511
评论
请登录