Android Runtime分代回收与写屏障深度解析(54)

54 阅读14分钟

码字不易,请大佬们点点关注,谢谢~

一、分代回收与写屏障概述

在Android Runtime(ART)中,分代回收与写屏障(Write Barrier)是保障内存高效管理与对象引用正确性的核心机制。分代回收基于对象生命周期不同的特性,将堆内存划分为年轻代、老年代和大对象空间,针对不同代采用差异化回收策略,以提升垃圾回收效率。而写屏障作为辅助机制,在对象引用关系发生变化时发挥作用,确保垃圾回收器能够准确标记可达对象,避免误回收。

从源码层面看,分代回收与写屏障的逻辑分散在art/runtime目录下众多源文件中。heap.ccheap.h定义了堆内存结构与基本操作,gc目录下的文件实现了分代回收算法与写屏障功能。接下来,我们将深入探究其原理与实现细节。

二、分代回收基础原理

2.1 分代理论基础

分代回收基于“弱分代假说”,即大部分对象生命周期短暂,少量对象生命周期较长。根据这一理论,ART将堆内存划分为不同代:年轻代用于存放新创建的对象,这些对象大多很快不再被引用;老年代用于存放经过多次回收仍存活的对象;大对象空间则用于存储超过一定大小阈值的对象。

这种划分使得垃圾回收器可以针对不同代的特点采用不同算法。年轻代对象存活率低,适合采用复制算法快速回收垃圾;老年代对象存活率高,采用标记 - 整理算法能有效避免内存碎片;大对象空间的对象由于其特殊性,通常采用标记 - 清除算法。

2.2 分代回收的优势

分代回收极大提升了内存管理效率。通过将对象按生命周期分类,避免了对整个堆内存进行频繁、大规模的垃圾回收。例如,年轻代的快速回收能及时释放大量不再使用的对象占用的内存,减少内存碎片的产生。同时,针对不同代的优化算法,降低了垃圾回收的时间开销,提高了应用程序的响应速度和整体性能。此外,分代回收还能更好地利用内存资源,延长内存的有效使用周期,减少内存不足导致的应用崩溃情况。

三、年轻代回收机制

3.1 年轻代内存布局

年轻代采用“复制算法”进行垃圾回收,其内存布局分为Eden空间和两个Survivor空间(From Space和To Space)。在art/runtime/gc/space/space.h中定义了Space类,EdenSpaceSurvivorSpace类继承自该类。

// Eden空间类
class EdenSpace : public Space {
public:
    // 初始化Eden空间,设置起始地址、大小和名称
    void Initialize(uint8_t* start, size_t size, const char* name);
    // 在Eden空间分配对象
    mirror::Object* AllocObject(Class* clazz, size_t extra_bytes);
    //...
};

// Survivor空间类
class SurvivorSpace : public Space {
public:
    // 初始化Survivor空间
    void Initialize(uint8_t* start, size_t size, const char* name);
    // 复制对象到Survivor空间
    void CopyObject(mirror::Object* src, mirror::Object* dst);
    //...
};

新创建的对象优先在Eden空间分配,当Eden空间满时,触发年轻代垃圾回收(Minor GC)。

3.2 年轻代垃圾回收流程

art/runtime/gc/collector/scavenger.cc中,Scavenger类实现了年轻代垃圾回收逻辑。

void Scavenger::Collect(GcType gc_type, GcCause gc_cause) {
    // 标记阶段:从根集合开始,标记所有可达对象
    MarkObjects();
    // 复制阶段:将存活对象从Eden和From Space复制到To Space
    CopyObjects();
    // 更新对象引用关系
    UpdateReferences();
    // 交换From Space和To Space角色,为下一次回收做准备
    SwapSurvivors();
}

在标记阶段,垃圾回收器从根集合(如栈中的对象引用、静态变量等)出发,遍历并标记所有可达对象。复制阶段,将标记的存活对象复制到To Space,同时清理Eden和From Space中的垃圾对象。更新引用关系确保应用程序中所有指向对象的引用都指向新位置。最后交换From Space和To Space,使原To Space成为下一次回收的From Space,原From Space变为To Space。

3.3 对象晋升机制

对象在年轻代经历多次垃圾回收后,若仍存活,将晋升到老年代。对象晋升的年龄阈值在art/runtime/gc/collector/scavenger.cc中定义:

constexpr uint8_t kMaxScavengeNumber = 15;  // 对象晋升年龄阈值

每次经历Minor GC,存活对象的年龄计数器加1,当达到kMaxScavengeNumber时,对象将被晋升到老年代。在Scavenger::CopyObjects方法中实现了对象晋升逻辑:

void Scavenger::CopyObjects() {
    for (mirror::Object* obj : objects_in_eden_and_from_space) {
        if (obj->GetAge() >= kMaxScavengeNumber) {
            // 将达到晋升年龄的对象晋升到老年代
            PromoteObjectToOldSpace(obj);
        } else {
            // 未达到晋升年龄的对象复制到To Space
            CopyObjectToSurvivorSpace(obj);
        }
    }
}

通过对象晋升机制,ART将生命周期长的对象转移到老年代,减少年轻代垃圾回收压力,提高内存管理效率。

四、老年代回收机制

4.1 老年代内存布局与特点

老年代用于存储生命周期较长的对象,采用“标记 - 整理(Mark - Compact)”算法进行垃圾回收。在art/runtime/gc/space/tenured_space.h中定义了TenuredSpace类表示老年代空间:

class TenuredSpace : public Space {
public:
    // 初始化老年代空间
    void Initialize(uint8_t* start, size_t size, const char* name);
    // 在老年代分配对象
    mirror::Object* AllocObject(Class* clazz, size_t extra_bytes);
    // 执行标记 - 整理垃圾回收
    void MarkCompact();
    //...
};

老年代对象由于存活时间长,对象间引用关系复杂,且内存分配和回收相对不频繁,因此需要更复杂的回收算法来处理内存碎片问题。

4.2 老年代垃圾回收流程

老年代垃圾回收在art/runtime/gc/collector/marksweep_compact.cc中实现。

void MarkSweepCompact::Collect(GcType gc_type, GcCause gc_cause) {
    // 标记阶段:从根集合开始,标记所有可达对象
    MarkObjects();
    // 整理阶段:计算存活对象新位置,移动存活对象,释放垃圾内存
    CompactObjects();
    // 更新对象引用关系
    UpdateReferences();
}

在标记阶段,与年轻代类似,从根集合出发标记所有可达对象。整理阶段,先计算存活对象在内存中的新位置,然后将存活对象移动到新位置,将垃圾对象占用的内存空间释放并整理,避免内存碎片。最后更新对象引用关系,确保应用程序能正确访问对象。

4.3 老年代与年轻代协同回收

老年代与年轻代在内存管理中相互协作。年轻代对象晋升到老年代,为老年代补充对象;老年代垃圾回收时,若内存不足,可能触发全量垃圾回收(Full GC),对整个堆内存进行回收。在art/runtime/gc/heap.cc中,Heap::CollectGarbage方法实现了这种协同逻辑:

void Heap::CollectGarbage(GcType gc_type, GcCause gc_cause) {
    if (gc_type == kGcTypeYoung) {
        // 触发年轻代垃圾回收
        scavenger_.Collect(gc_type, gc_cause);
    } else if (gc_type == kGcTypeOld) {
        // 触发老年代垃圾回收
        marksweep_compact_.Collect(gc_type, gc_cause);
        if (IsHeapOutOfMemory()) {
            // 老年代回收后仍内存不足,触发全量垃圾回收
            CollectGarbage(kGcTypeFull, gc_cause);
        }
    } else if (gc_type == kGcTypeFull) {
        // 全量垃圾回收逻辑,涉及年轻代、老年代和大对象空间回收
        //...
    }
}

这种协同机制确保了整个堆内存的有效管理,根据对象生命周期和内存使用情况动态调整回收策略。

五、大对象空间回收机制

5.1 大对象空间设计目的

大对象空间专门用于存储超过一定大小阈值的对象,该阈值在art/runtime/heap.cc中定义:

constexpr size_t kLargeObjectThreshold = 12 * KB;  // 大对象大小阈值,这里设为12KB

将大对象单独存储在大对象空间,可避免大对象在年轻代和老年代频繁移动带来的开销,同时简化垃圾回收逻辑。

5.2 大对象空间垃圾回收流程

art/runtime/gc/space/large_object_space.h中定义了LargeObjectSpace类用于管理大对象空间:

class LargeObjectSpace : public Space {
public:
    // 初始化大对象空间
    void Initialize(uint8_t* start, size_t size, const char* name);
    // 在大对象空间分配对象
    mirror::Object* AllocObject(Class* clazz, size_t object_size);
    // 大对象空间垃圾回收
    void CollectLargeObjects();
    //...
};

大对象空间的垃圾回收在LargeObjectSpace::CollectLargeObjects方法中实现,通常采用标记 - 清除算法:

void LargeObjectSpace::CollectLargeObjects() {
    // 标记阶段:从根集合开始,标记所有可达大对象
    MarkLargeObjects();
    // 清除阶段:释放未标记的大对象占用内存
    SweepLargeObjects();
}

在标记阶段,从根集合出发标记所有可达大对象;清除阶段,遍历大对象空间,释放未标记的大对象占用的内存,从而完成垃圾回收。

六、写屏障基础原理

6.1 写屏障的作用

写屏障是一种在对象引用关系发生变化时执行的特殊代码,其作用是确保垃圾回收器能够准确标记可达对象。在分代回收中,由于存在跨代引用(如老年代对象引用年轻代对象),如果没有写屏障,年轻代垃圾回收时可能会误将被老年代引用的年轻代对象当作垃圾回收。写屏障通过记录引用关系的变化,让垃圾回收器能够感知到这些跨代引用,保证对象引用的正确性和垃圾回收的准确性。

6.2 写屏障的类型

常见的写屏障类型有前向写屏障(Pre - Write Barrier)和后向写屏障(Post - Write Barrier)。前向写屏障在对象引用被修改之前执行,记录旧的引用关系;后向写屏障在对象引用被修改之后执行,记录新的引用关系。在ART中,根据不同的场景和需求,会采用不同类型的写屏障。

七、写屏障在分代回收中的实现

7.1 写屏障与卡表(Card Table)

在ART中,写屏障与卡表紧密配合实现跨代引用管理。卡表是一种数据结构,将堆内存划分为固定大小的“卡”,在art/runtime/gc/collector/card_table.cc中实现:

class CardTable {
public:
    // 标记卡,当老年代对象引用年轻代对象时调用
    void MarkCard(uint8_t* card_address);
    // 扫描卡表,获取跨代引用对象
    void ScanCards(Closure* closure);
    //...
};

当老年代对象引用年轻代对象时,对应的卡会被写屏障标记。在年轻代垃圾回收时,只需扫描被标记的卡,就能找到所有跨代引用,从而准确标记可达对象。例如,在art/runtime/gc/collector/scavenger.ccScavenger::MarkObjects方法中:

void Scavenger::MarkObjects() {
    // 从根集合开始标记可达对象
    MarkRoots();
    // 扫描卡表,标记被老年代引用的年轻代对象
    card_table_.ScanCards([this](uint8_t* card) {
        ScanCardForReferences(card);
    });
}

7.2 写屏障的具体实现代码

在ART中,写屏障的实现代码分散在对象引用赋值操作相关的地方。以art/runtime/mirror/object-inl.h中对象字段赋值为例,可能会插入写屏障代码:

template <typename T>
void Object::SetFieldObject(T* field, mirror::Object* value) {
    // 执行前向写屏障(假设这里采用前向写屏障)
    WriteBarrierPre(this, field, value);
    *field = value;
    // 执行后向写屏障(假设这里采用后向写屏障)
    WriteBarrierPost(this, field, value);
}

WriteBarrierPreWriteBarrierPost函数根据具体的写屏障策略实现,可能涉及卡表标记、引用关系记录等操作,以确保垃圾回收器能够正确处理对象引用变化。

八、并发分代回收与写屏障

8.1 并发分代回收特点

ART支持并发分代回收,以减少垃圾回收对应用程序性能的影响。在并发回收过程中,垃圾回收器与应用程序线程同时运行。对于年轻代,由于其采用复制算法且对象生命周期较短,并发回收相对简单,通过写屏障记录应用线程对对象引用的修改,保证标记阶段能够准确标记可达对象。老年代的并发回收更为复杂,采用标记 - 整理算法,且对象生命周期长、引用关系复杂,需要多次暂停和恢复应用线程,以保证标记的准确性和内存整理的正确性。

8.2 并发场景下写屏障的作用

在并发场景下,写屏障的作用更加关键。由于应用程序线程和垃圾回收器线程同时运行,对象引用关系可能随时发生变化。写屏障确保在垃圾回收过程中,即使应用线程修改了对象引用,垃圾回收器也能正确识别可达对象。例如,在并发标记阶段,应用线程修改了老年代对象对年轻代对象的引用,写屏障会及时标记对应的卡,使得垃圾回收器在扫描卡表时能够发现这个跨代引用,避免误判对象可达性,保证并发回收的正确性和稳定性。

九、分代回收与写屏障的性能优化

9.1 分代回收性能优化

分代回收的性能优化主要从算法和内存布局两方面进行。在算法上,对年轻代的复制算法,优化对象复制过程中的数据移动次数和地址映射表更新方式;对老年代的标记 - 整理算法,采用并行标记、增量式整理等技术,减少垃圾回收暂停时间。在内存布局上,合理调整各代空间大小比例,减少垃圾回收频率。例如,在art/runtime/gc/heap_tuning.cc中:

void HeapTuner::TuneGenerationSizes() {
    // 根据应用内存使用模式和性能指标,计算各代空间最优大小
    size_t young_size = CalculateOptimalYoungGenerationSize();
    size_t old_size = CalculateOptimalOldGenerationSize();
    // 调整堆内存中各代空间大小
    Heap::Current()->ResizeYoungGeneration(young_size);
    Heap::Current()->ResizeOldGeneration(old_size);
    //...
}

9.2 写屏障性能优化

写屏障的性能优化主要在于减少其带来的额外开销。由于写屏障在对象引用变化时执行,频繁的引用修改会导致写屏障开销增大。优化方式包括采用更高效的卡表数据结构和操作算法,减少卡表标记和扫描的时间成本;对写屏障代码进行精简和优化,避免不必要的操作。例如,通过位运算优化卡表标记操作,减少计算量;对写屏障函数进行内联优化,减少函数调用开销。

十、分代回收与写屏障在不同场景下的应用与挑战

10.1 高并发应用场景

在高并发应用场景下,如即时通讯应用、在线游戏等,多个线程同时进行大量内存分配、对象创建和引用修改操作。这对分代回收和写屏障提出了更高要求。分代回收需要更精细的并发控制机制,避免回收过程与应用线程操作冲突;写屏障需要处理好多线程环境下的引用变化记录,保证垃圾回收器准确标记可达对象。同时,高并发下写屏障的性能开销更加明显,需要进行针对性优化,以平衡内存管理准确性和应用性能。

10.2 内存敏感型应用场景

对于内存敏感型应用,如嵌入式设备上的轻量级应用、运行在低配置设备上的应用等,分代回收和写屏障需要更加谨慎。分代回收要采用更严格的内存需求评估和回收策略,避免过度回收或扩展内存;写屏障需要在保证对象引用正确性的前提下,尽量减少性能开销。此外,还需要考虑与其他系统组件的资源共享和协调,确保在有限内存资源下,应用能够稳定运行。