ART运行时Compacting GC简要介绍和学习计划
最后更新于:2022-04-02 05:05:03
[原文出处--------------ART运行时Compacting GC简要介绍和学习计划](http://blog.csdn.net/luoshengyang/article/details/44513977)
在前面一个系列文章中,我们学习了Android 4.4 ART的Mark-Sweep(MS)GC。到了Android 5.0,ART增加了对Compacting GC的支持,包括Semi-Space(SS)、Generational Semi-Space(GSS)和Mark-Compact (MC)三种。本文对Android 5.0 ART的Compacting GC进行简要介绍以及制定学习计划。
总体来说,Compacting GC和Mark-Sweep GC各有优劣。所谓Compacting GC,就是在进行GC的时候,同时对堆空间进行压缩,以消除碎片,因此它的堆空间利用率就更高。但是也正因为要对堆空间进行压缩,导致Compacting GC的GC效率不如Mark-Sweep GC。不过,只要我们使用得到恰当,是能够同时发挥Compacting GC和Mark-Sweep GC的长处的。例如,当Android应用程序被激活在前台运行时,就使用Mark-Sweep GC,而当Android应用程序回退到后台运行时,就使用Compacting GC。
为了达到上述目的,ART运行时内部有Foreground和Background两种GC之分。在ART运行时启动的时候,可以通过-Xgc和-XX:BackgroundGC来指定Foreground GC和Background GC的类型,即具体是哪一种Mark-Sweep GC或者Compacting GC。由于Mark-Sweep GC和Compacting GC所需要的堆空间结构是不一样的,因此,当发生Foreground GC和Background GC切换时,ART运行时需要提供支持,以维护堆空间的正确性。
除了适合后台运行时之外,Compacting GC还适合用在内存分配时。在以往的Mark-Sweep GC时,由于碎片而产生的内存不足问题,是解决不了的,只能让应用程序OOM。但是有了Compacting GC之后,就可以在应用程序OOM之前,再作一次努力,那就是对原来的堆空间进行压缩一下,再尝试进行分配,这样就可以提高成功分配内存的概率。
从上面的分析可以看出,Compacting GC的最大特点就是会对堆空间进行压缩。这意味着对象在堆空间的位置是会发生变化的。但是对应用程序来说,这种对象位置的变化是要透明的。因此,Compacting GC的最核心挑战就是在保持应用程序逻辑不变和正确的前提下,在需要的时候对对象的位置进行移动。所以在这篇文章里面,我们第一个要介绍的技术就是对象移动技术,接着再在此基础之上,展开对其它技术的介绍。
一个对象被移动了,要保持它的使用者的正确性,无非就是两种方案。
第一种方案是使用者不是直接引用对象,而是间接引用。这就类似于操作系统里面的文件描述符(fd)。我们在调用操作系统接口open打开一个文件的时候,获得的是一个整型的文件描述符。这个文件描述符其实就是一个索引,它索引到内核为每一个进程都创建的一个打开文件表。这个打开文件表里面的每一个项保存的都是一个指向一个打开文件结构体的指针。我们可以把这个打文件结构体看作就一个对象。当这个对象移动时,也就是在另外一个地方重新分配时,只需要将新分配得到的地址重新填入对应的打开文件表的表项就可以了。这对应用程序来说,是完全透明的,因为它是通过打开文件表来间接访问得到对应的打开文件结构体的。
我们可以轻易看出,上述方案的最大缺点就是每次访问对象都需要有额外的开销,也就是影响效率。但是如果我们可以忽略在执行Compacting GC时的这个开销,是不是就可以使用了呢?答案是否定的。由于Foreground和Background两种GC的同时存在,ART内部可能同时存在着Mark-Sweep和Compacting两种类型的GC。如果我们在Compacting GC中使用了该方案,那么也意味着Mark-Sweep GC也必须是要间接地去访问对象。但是这完全是没有必要的,因此ART使用的是第二种对象移动技术,也就是修改对象使用者的引用,使得它无论何时何地,总是直接指向对象的真实地址。
在ART运行时中,对象使用者无非就是位于两个位置,一个是堆,一个栈。因此,当一个对象被移动时,我们只需要找到它在堆和栈上的使用者的位置,那就可以将它们的值修改为对象被移动后的新地址,那就达到目的了。这个对象移动及其使用者引用者修改的过程示意图如图1所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/a1b240e5aaea41d406d32474d80bd7f7_779x418.png)
图1 ART运行时Compacting GC对象移动技术
在图1中,被移动的对象是Object-1,它从Source Space移动到Destination Space,并且同时被堆上的对象Object-2和堆上的一个slot引用。这里我们需要解决两个问题。第一个问题是上面提到的,我们要修改Object-2中对Object-1的引用,实际上就是某一个成员变量的值,以及栈上显示的slot位置。第二个问题是要保证Object-1只被移动一次,也就是Object-2中对Object-1的引用和栈上的slot位置指向的是同一个地方。
这是如何实现的呢?我们需要先看一下Object类的定义。这个Object类是ART运行时里面的所有对象的基类,它有一个monitor_成员变量,如下所示:
~~~
// C++ mirror of java.lang.Object
class MANAGED LOCKABLE Object {
public:
......
// Monitor and hash code information.
uint32_t monitor_;
......
};
~~~
这个类定义在文件art/runtime/mirror/object.h。
这个32位的monitor_成员变量的责任重大,除了用来描述对象的Monitor和Hash Code信息之外,还包括对象的移动信息。这个monitor_成员变量通过封装成一个LockWord对象来描述,如下所示:
~~~
class LockWord {
public:
enum {
// Number of bits to encode the state, currently just fat or thin/unlocked or hash code.
kStateSize = 2,
// Number of bits to encode the thin lock owner.
kThinLockOwnerSize = 16,
// Remaining bits are the recursive lock count.
kThinLockCountSize = 32 - kThinLockOwnerSize - kStateSize,
// Thin lock bits. Owner in lowest bits.
kThinLockOwnerShift = 0,
kThinLockOwnerMask = (1 << kThinLockOwnerSize) - 1,
// Count in higher bits.
kThinLockCountShift = kThinLockOwnerSize + kThinLockOwnerShift,
kThinLockCountMask = (1 << kThinLockCountSize) - 1,
kThinLockMaxCount = kThinLockCountMask,
// State in the highest bits.
kStateShift = kThinLockCountSize + kThinLockCountShift,
kStateMask = (1 << kStateSize) - 1,
kStateThinOrUnlocked = 0,
kStateFat = 1,
kStateHash = 2,
kStateForwardingAddress = 3,
// When the state is kHashCode, the non-state bits hold the hashcode.
kHashShift = 0,
kHashSize = 32 - kStateSize,
kHashMask = (1 << kHashSize) - 1,
};
......
enum LockState {
kUnlocked, // No lock owners.
kThinLocked, // Single uncontended owner.
kFatLocked, // See associated monitor.
kHashCode, // Lock word contains an identity hash.
kForwardingAddress, // Lock word contains the forwarding address of an object.
};
......
private:
......
// The encoded value holding all the state.
uint32_t value_;
};
~~~
这个类定义在文件art/runtime/lock_word.h中。
通过这个LockWord类的定义我们就可以知道,Object对象的成员变量monitor_的高2位描述的是状态,包括kUnlocked、kThinLocked、kFatLocked、kHashCode和kForwardingAddress五种状态。处于不同状态时,低30位有不同的描述,这里我们只关心kForwardingAddress这种状态。
当一个对象处于kForwardingAddress状态时,即它的成员变量monitor_的高2位值等于0x11时,表示它已经被移动过了,这时候它的位30位描述的就是对象移动后的新地址。有同学可能会说,30位够表示一个对象的地址吗?因为32位的体系架构中,内存的地址是32位的啊。答案是够的,回忆前面[ART运行时垃圾收集机制简要介绍和学习计划](http://blog.csdn.net/luoshengyang/article/details/42072975)这个系列的文章,对象的分配都是8字节对齐的,这意味着低3位都是0,因此这里用30位来描述对象的地址是足够的。
有了这个背景知识后,我们来回过头来看图1。我们首先需要明确,对象移动是发生在GC的过程中的,并且只有可达对象才需要移动,而可达对象都是从根集对象开始遍历的。我们假设Object-1是根集对象,并且是被栈上的slot引用。因此,当遍历到栈上的slot时,需要移动对象Object-1。这时候除了要将栈上的slot位置修改为Object-1在Destination Space上的位置之外,还需要将旧的Object-1(位于Source Space上)的成员变量monitor_的高2位设置为0x11,以及将低30位设置为新的Object-1(位于Destination Space上)的地址。
我们再假设Object-2是一个可达对象,也就是说在栈上的slot被遍历之后的某一个时候,Object-2也会被遍历。在遍历Object-2的时候,我们会检查它的每一个引用类型的成员变量。当检查到其引用了Object-1的成员变量的时候,会发现它Object-1(位于Source Space上)的成员变量monitor_的高2位已经被设置为kForwardingAddress状态,因此就直接将其低30位的值取出来,并且设置为Object-2对应的成员变量的值。这样就可以保证Object-2和栈上的slot位置引用的都是新的Object-1了。
以此类推,不管还有多少个可达对象引用了Object-1,按照上面的算法,都能保证它们能够正确地引用移动后的Object-1。
接下来我们就以Semi-Space GC标记对象的过程来说明上述的对象移动过程。Semi-Space GC的执行过程我们在后面的文章中再详细分析,这里主要关注对象的移动过程。首先栈上的根集对象的移动过程,如下所示:
~~~
void SemiSpace::MarkRootCallback(Object** root, void* arg, uint32_t /*thread_id*/,
RootType /*root_type*/) {
auto ref = StackReference::FromMirrorPtr(*root);
reinterpret_cast(arg)->MarkObject(&ref);
if (*root != ref.AsMirrorPtr()) {
*root = ref.AsMirrorPtr();
}
}
// Marks all objects in the root set.
void SemiSpace::MarkRoots() {
TimingLogger::ScopedTiming t(__FUNCTION__, GetTimings());
Runtime::Current()->VisitRoots(MarkRootCallback, this);
}
~~~
这两个函数定义在文件art/runtime/gc/collector/semi_space.cc中。
SemiSpace类的成员函数MarkRoots调用Runtime类的成员函数VisitRoots来遍历根集对象。对于每一个根集对象,都会回调SemiSpace类的静态成员函数MarkRootCallback进行处理。
在SemiSpace类的静态成员函数MarkRootCallback中,参数root是一个指向栈上的位置,它所引用的对象的地址值即为*root。首先是将被引用对象的地址*root封装成一个StackReference对象。
StackReference类继承于ObjectReference类,后者有一个类型为uint32_t的reference_,如下所示:
~~~
template
class MANAGED ObjectReference {
public:
MirrorType* AsMirrorPtr() const SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
return UnCompress();
}
void Assign(MirrorType* other) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
reference_ = Compress(other);
}
void Clear() {
reference_ = 0;
}
uint32_t AsVRegValue() const {
return reference_;
}
protected:
ObjectReference(MirrorType* mirror_ptr)
SHARED_LOCKS_REQUIRED(Locks::mutator_lock_)
: reference_(Compress(mirror_ptr)) {
}
// Compress reference to its bit representation.
static uint32_t Compress(MirrorType* mirror_ptr) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
uintptr_t as_bits = reinterpret_cast(mirror_ptr);
return static_cast(kPoisonReferences ? -as_bits : as_bits);
}
// Uncompress an encoded reference from its bit representation.
MirrorType* UnCompress() const SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
uintptr_t as_bits = kPoisonReferences ? -reference_ : reference_;
return reinterpret_cast(as_bits);
}
friend class Object;
// The encoded reference to a mirror::Object.
uint32_t reference_;
};
~~~
这个类定义在文件art/runtime/mirror/object_reference.h中。
kPoisonReferences是一个模板参数,表示是否要对ObjectReference的成员变量reference_进行压缩。这里所谓的压缩,也只不过是将成员变量reference_的值进行取负而已。
结合ObjectReference类的定义,回到SemiSpace类的静态成员函数MarkRootCallback中,就意味着本地变量ref指向的StackReference对象的成员变量reference_的值就等于*root的值,也就是被引用对象的地址值。接下来再调用SemiSpace类的成员函数MarkObejct判断是否需要移动对象,如下所示:
~~~
template
inline void SemiSpace::MarkObject(
mirror::ObjectReference* obj_ptr) {
mirror::Object* obj = obj_ptr->AsMirrorPtr();
if (obj == nullptr) {
return;
}
......
if (from_space_->HasAddress(obj)) {
mirror::Object* forward_address = GetForwardingAddressInFromSpace(obj);
// If the object has already been moved, return the new forward address.
if (UNLIKELY(forward_address == nullptr)) {
forward_address = MarkNonForwardedObject(obj);
DCHECK(forward_address != nullptr);
// Make sure to only update the forwarding address AFTER you copy the object so that the
// monitor word doesn't Get stomped over.
obj->SetLockWord(
LockWord::FromForwardingAddress(reinterpret_cast(forward_address)), false);
// Push the object onto the mark stack for later processing.
MarkStackPush(forward_address);
}
obj_ptr->Assign(forward_address);
} else if (!collect_from_space_only_ && !immune_region_.ContainsObject(obj)) {
BitmapSetSlowPathVisitor visitor(this);
if (!mark_bitmap_->Set(obj, visitor)) {
// This object was not previously marked.
MarkStackPush(obj);
}
}
}
~~~
这个函数定义在文件art/runtime/gc/collector/semi_space-inl.h中。
这里我们先明确一下各个参数、本地变量以及成员变量的含义。参数obj_ptr指向的是前面SemiSpace类的静态成员函数MarkRootCallback构造的一个局部StackReferece对象,本地变量obj指向的是一个被引用的根集对象,成员变量from_space_描述的是当前Semi-Space GC的From Space,也就是相当于图1所示的Source Space。
只有当对象obj是属于From Space时,才需要对它进行移动。在其它情况下,不需要对对象obj进行处理,除非成员变量collect_from_space_only_的值被设置为false,也就是非From Space的对象也要进行处理,并且对象obj不属于成员变量immune_region_描述的非回收区域中。对这类对象的处理也比较简单,只是将对在Mark Bitmap的对应位设置为1,并且如果是第一次被设置,那么就将它压入到Mark Stack去等待递归处理它直接或者间接引用的其它对象。
现在我们就主要关注对象obj属于From Space的情况,SemiSpace类的成员函数MarkObejct首先是调用另外一个成员函数GetForwardingAddressInFromSpace检查对象obj是否已经被移动过了。如果已经被移动过,那么得到的forward_address就不等于nullptr,这时候只要修改参数obj_ptr指向的是前面StackReferece对象的成员变量reference_的值为forward_address即可。
另一方面,如果得到的forward_address就等于nullptr,那么就说没有对象obj还没有被移动过,这时候就需要调用另外一个成员函数MarkNonForwardedObject对对象obj进行移动,移动后得到的新地址即为forward_address。这时候首先要做的就是设置旧的obj对象的forwarding address,以便以后遍历其它引用了旧的obj对象的对象时,可以获得新的obj的地址。接下来再移动后的对象压入到Mark Stack中去等待递归遍历。最后一步同样是要修改参数obj_ptr指向的是前面StackReferece对象的成员变量reference_的值为forward_address。
至此,对象移动完毕,再回到SemiSpace类的静态成员函数MarkRootCallback中。注意,这时候我们修改的仅仅是本地变量ref指向的一个StackReference对象的成员变量reference_的值,而栈上引用了对象*root的slot还没有被修改,因此,最后就需要继续修改该slot的值,这个slot的位置就是通过*root来描述的。
分析完成栈位置引用的修改过程之后,我们再来看堆位置引用的修改过程。我们依然是以Semi-Space标记对象的过程为例。Semi-Space GC标记完成根集对象后,就开始标记可达对象,如下所示:
~~~
void SemiSpace::MarkReachableObjects() {
......
for (auto& space : heap_->GetContinuousSpaces()) {
// If the space is immune then we need to mark the references to other spaces.
accounting::ModUnionTable* table = heap_->FindModUnionTableFromSpace(space);
if (table != nullptr) {
......
table->UpdateAndMarkReferences(MarkHeapReferenceCallback, this);
......
}
......
}
......
}
~~~
这个函数定义在文件art/runtime/gc/collector/semi_space.cc中。
SemiSpace类的成员函数MarkReachableObjects依次遍历组成当前堆的各个Space。如果一个Space有对应的Mod Union Table,那么就调用它的成员函数UpdateAndMarkReferences来标记那些自上次GC以来被修改的对象。对于每一个这样的对象,都回调SemiSpace类的静态成员函数MarkHeapReferenceCallback进行处理。关于Mod Union Table在GC过程中的作用,可以参考前面[ART运行时垃圾收集机制简要介绍和学习计划](http://blog.csdn.net/luoshengyang/article/details/42072975)这个系列的文章。
Mod Union Table的基类是ModUnionTable,它的子类ModUnionTableReferenceCache用来描述一个具体的Mod Union Table,接下来我们就通过它的成员函数UpdateAndMarkReferences来进一步描述被堆对象引用的对象的移动过程,它的实现如下所示:
~~~
void ModUnionTableReferenceCache::UpdateAndMarkReferences(MarkHeapReferenceCallback* callback,
void* arg) {
CardTable* card_table = heap_->GetCardTable();
std::vector*> cards_references;
ModUnionReferenceVisitor add_visitor(this, &cards_references);
for (const auto& card : cleared_cards_) {
// Clear and re-compute alloc space references associated with this card.
cards_references.clear();
uintptr_t start = reinterpret_cast(card_table->AddrFromCard(card));
uintptr_t end = start + CardTable::kCardSize;
auto* space = heap_->FindContinuousSpaceFromObject(reinterpret_cast
';