Android Runtime内存分配与对象生命周期深度解析(57)

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

一、内存分配与对象生命周期概述

在Android Runtime(ART)中,内存分配与对象生命周期管理是保障应用高效运行的核心机制。内存分配负责为应用程序创建的对象分配物理内存空间,而对象生命周期管理则跟踪对象从创建到销毁的整个过程,确保内存资源的合理使用和及时回收。

从源码角度来看,内存分配与对象生命周期管理涉及art/runtime目录下多个源文件。heap.ccheap.h定义了堆内存的基本结构和操作,object_allocator.cc实现了对象分配的具体逻辑,gc目录下的文件则负责垃圾回收和对象生命周期的管理。接下来,我们将深入分析其原理与实现细节。

二、内存分配策略

2.1 堆内存结构

ART的堆内存被划分为多个不同的空间,每个空间用于存储不同类型的对象。在art/runtime/heap.h中定义了堆内存的基本结构:

cpp 复制代码
class Heap {
public:
    // 年轻代空间,包含Eden和Survivor区
    Space* young_space_;
    // 老年代空间
    Space* old_space_;
    // 大对象空间
    Space* large_object_space_;
    //...其他空间和属性
};

这种分代式的堆内存结构基于"弱分代假说",即大多数对象生命周期短暂,少数对象生命周期较长。通过将对象按生命周期分类存储,可以更高效地进行垃圾回收。

2.2 分配策略选择

对象分配时,ART会根据对象大小和类型选择合适的分配策略。在art/runtime/object_allocator.cc中,ObjectAllocator::Alloc方法实现了分配策略的选择逻辑:

cpp 复制代码
mirror::Object* ObjectAllocator::Alloc(Class* clazz, size_t extra_bytes,
                                      size_t* bytes_allocated,
                                      size_t* bytes_tl_bulk_allocated) {
    // 计算对象总大小
    size_t object_size = CalculateObjectSize(clazz, extra_bytes);
    
    // 如果对象超过大对象阈值,分配到大对象空间
    if (object_size >= kLargeObjectThreshold) {
        return AllocLargeObject(clazz, object_size, bytes_allocated);
    }
    
    // 否则,根据线程本地分配缓存(TLAB)状态分配到年轻代
    if (UseTLAB()) {
        mirror::Object* obj = AllocWithTLAB(clazz, object_size, bytes_allocated);
        if (obj != nullptr) {
            return obj;
        }
    }
    
    // TLAB分配失败,直接在Eden空间分配
    return AllocWithoutTLAB(clazz, object_size, bytes_allocated);
}

该方法首先计算对象大小,然后根据对象大小与大对象阈值的比较结果,决定是分配到大对象空间还是年轻代。对于年轻代分配,优先尝试使用线程本地分配缓存(TLAB),如果TLAB分配失败,则直接在Eden空间分配。

三、线程本地分配缓存(TLAB)

3.1 TLAB基本原理

线程本地分配缓存(Thread Local Allocation Buffer,TLAB)是ART为每个线程分配的一小块私有内存区域,用于快速分配小对象。TLAB的目的是减少多线程环境下对公共分配锁的竞争,提高对象分配效率。

art/runtime/thread.h中定义了线程与TLAB的关系:

cpp 复制代码
class Thread {
public:
    // TLAB起始地址
    uint8_t* tlab_start_;
    // TLAB当前可用地址
    uint8_t* tlab_pos_;
    // TLAB结束地址
    uint8_t* tlab_end_;
    //...其他线程相关属性
};

每个线程通过维护TLAB的起始地址、当前可用地址和结束地址,实现快速的对象分配。

3.2 TLAB分配流程

当线程需要分配小对象时,首先尝试在TLAB中分配。在art/runtime/object_allocator.cc中,ObjectAllocator::AllocWithTLAB方法实现了TLAB分配逻辑:

cpp 复制代码
mirror::Object* ObjectAllocator::AllocWithTLAB(Class* clazz, size_t size,
                                              size_t* bytes_allocated) {
    Thread* self = Thread::Current();
    uint8_t* tlab_pos = self->tlab_pos_;
    uint8_t* new_tlab_pos = tlab_pos + size;
    
    // 检查TLAB是否有足够空间
    if (new_tlab_pos <= self->tlab_end_) {
        // TLAB空间足够,直接分配
        mirror::Object* result = reinterpret_cast<mirror::Object*>(tlab_pos);
        self->tlab_pos_ = new_tlab_pos;
        *bytes_allocated = size;
        InitializeObjectHeader(result, clazz, size);
        return result;
    }
    
    // TLAB空间不足,需要重新分配或填充TLAB
    return RefillTLAB(clazz, size, bytes_allocated);
}

该方法首先检查TLAB是否有足够空间分配对象,如果有,则直接在TLAB中分配并更新TLAB指针;如果空间不足,则调用RefillTLAB方法重新分配或填充TLAB。

3.3 TLAB的优缺点

TLAB的优点在于减少了多线程环境下的锁竞争,提高了对象分配速度。由于每个线程有自己的TLAB,线程内的对象分配可以在无锁状态下进行,避免了频繁获取和释放锁的开销。然而,TLAB也存在一些缺点。例如,TLAB可能导致内存碎片化,因为每个TLAB的剩余空间可能无法被其他线程利用;此外,如果TLAB设置过大,可能会浪费内存资源。

四、年轻代对象分配

4.1 年轻代内存布局

年轻代采用"复制算法"进行垃圾回收,其内存布局分为Eden空间和两个Survivor空间(From Space和To Space)。在art/runtime/gc/space/space.h中定义了这些空间的基本结构:

cpp 复制代码
class Space {
public:
    // 空间起始地址
    uint8_t* begin_;
    // 空间结束地址
    uint8_t* end_;
    //...其他空间属性
};

class EdenSpace : public Space {
    // Eden空间特有的属性和方法
};

class SurvivorSpace : public Space {
    // Survivor空间特有的属性和方法
};

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

4.2 年轻代对象分配流程

art/runtime/gc/space/eden_space.cc中,EdenSpace::Alloc方法实现了年轻代对象分配逻辑:

cpp 复制代码
mirror::Object* EdenSpace::Alloc(Thread* self, size_t size, size_t* bytes_allocated) {
    // 获取Eden空间当前可用地址
    uint8_t* pos = top_.load(std::memory_order_relaxed);
    uint8_t* new_pos = pos + size;
    
    // 检查Eden空间是否有足够空间
    if (new_pos <= end_) {
        // 空间足够,尝试通过原子操作更新top指针
        if (top_.compare_exchange_weak(pos, new_pos, std::memory_order_seq_cst)) {
            mirror::Object* result = reinterpret_cast<mirror::Object*>(pos);
            *bytes_allocated = size;
            return result;
        }
        // 原子操作失败,重试
        return Alloc(self, size, bytes_allocated);
    }
    
    // Eden空间不足,返回nullptr,触发垃圾回收
    return nullptr;
}

该方法首先检查Eden空间是否有足够空间分配对象,如果有,则通过原子操作更新Eden空间的top指针,确保多线程环境下的线程安全;如果空间不足,则返回nullptr,触发垃圾回收。

五、老年代对象分配

5.1 老年代对象分配场景

老年代主要用于存储生命周期较长的对象。对象晋升是老年代对象的主要来源,当年轻代中的对象经过多次垃圾回收仍然存活时,会晋升到老年代。此外,大对象也可能直接分配到老年代,具体取决于ART的配置和实现。

5.2 老年代对象分配流程

art/runtime/gc/space/tenured_space.cc中,TenuredSpace::Alloc方法实现了老年代对象分配逻辑:

cpp 复制代码
mirror::Object* TenuredSpace::Alloc(Thread* self, size_t size, size_t* bytes_allocated) {
    // 检查老年代是否有足够空间
    if (ContinuousSpace::GetBytesAllocated() + size > GetMaxSize()) {
        // 空间不足,触发老年代垃圾回收
        Heap* heap = Runtime::Current()->GetHeap();
        heap->CollectGarbageInternal(kGcTypeOld, kGcCauseForAllocation, self);
        
        // 回收后再次检查空间
        if (ContinuousSpace::GetBytesAllocated() + size > GetMaxSize()) {
            // 仍然空间不足,返回nullptr
            return nullptr;
        }
    }
    
    // 有足够空间,分配对象
    mirror::Object* result = ContinuousSpace::Alloc(self, size, bytes_allocated);
    if (result != nullptr) {
        // 初始化对象头
        InitializeObjectHeader(result, size);
    }
    return result;
}

该方法首先检查老年代是否有足够空间分配对象,如果空间不足,则触发老年代垃圾回收。回收后再次检查空间,如果仍然不足,则返回nullptr;否则,分配对象并初始化对象头。

六、大对象空间分配

6.1 大对象定义与判断标准

大对象是指超过一定大小阈值的对象,该阈值在art/runtime/heap.h中定义:

cpp 复制代码
constexpr size_t kLargeObjectThreshold = 12 * KB;  // 大对象阈值,默认为12KB

当对象大小超过该阈值时,会被分配到大对象空间。

6.2 大对象空间分配流程

art/runtime/gc/space/large_object_space.cc中,LargeObjectSpace::Alloc方法实现了大对象分配逻辑:

cpp 复制代码
mirror::Object* LargeObjectSpace::Alloc(Thread* self, size_t size, size_t* bytes_allocated) {
    // 为大对象分配专用内存块
    uint8_t* memory = AllocateMemoryBlock(size);
    if (memory == nullptr) {
        // 分配失败,触发垃圾回收
        Heap* heap = Runtime::Current()->GetHeap();
        heap->CollectGarbageInternal(kGcTypeFull, kGcCauseForLargeAllocation, self);
        
        // 回收后再次尝试分配
        memory = AllocateMemoryBlock(size);
        if (memory == nullptr) {
            return nullptr;
        }
    }
    
    // 初始化对象头
    mirror::Object* result = reinterpret_cast<mirror::Object*>(memory);
    InitializeObjectHeader(result, size);
    
    // 记录大对象信息
    AddLargeObject(result, size);
    
    *bytes_allocated = size;
    return result;
}

该方法首先尝试为大对象分配专用内存块,如果分配失败,则触发全量垃圾回收。回收后再次尝试分配,如果仍然失败,则返回nullptr;否则,初始化对象头,记录大对象信息并返回分配的对象。

七、对象生命周期阶段

7.1 对象创建阶段

对象的生命周期始于创建阶段。在art/runtime/object_allocator.cc中,ObjectAllocator::Alloc方法负责对象的创建和内存分配:

cpp 复制代码
mirror::Object* ObjectAllocator::Alloc(Class* clazz, size_t extra_bytes,
                                      size_t* bytes_allocated,
                                      size_t* bytes_tl_bulk_allocated) {
    // 计算对象大小
    size_t size = CalculateObjectSize(clazz, extra_bytes);
    
    // 根据对象大小选择分配策略
    if (size >= kLargeObjectThreshold) {
        return AllocLargeObject(clazz, size, bytes_allocated);
    } else {
        return AllocNonLargeObject(clazz, size, bytes_allocated, bytes_tl_bulk_allocated);
    }
}

该方法根据对象大小选择合适的分配策略,分配内存空间并返回新创建的对象。

7.2 对象使用阶段

对象使用阶段是对象生命周期中最长的阶段。在此阶段,应用程序通过引用访问和操作对象。对象的状态可能会被修改,对象之间的引用关系也可能会发生变化。在这个过程中,垃圾回收器会跟踪对象的引用状态,以便在对象不再被引用时进行回收。

7.3 对象回收阶段

当对象不再被任何引用指向时,对象进入回收阶段。垃圾回收器会在适当的时候检测到这些不可达对象,并回收它们占用的内存空间。在art/runtime/gc/collector目录下的垃圾回收器实现中,定义了对象回收的具体逻辑。例如,在scavenger.cc中,Scavenger类实现了年轻代垃圾回收,包括标记可达对象和回收不可达对象的过程:

cpp 复制代码
void Scavenger::Collect(GcType gc_type, GcCause gc_cause) {
    // 标记阶段:从根集合开始,标记所有可达对象
    MarkRoots();
    MarkHeap();
    
    // 清除阶段:回收未被标记的对象
    Sweep();
}

该方法首先标记所有可达对象,然后回收未被标记的对象,从而完成对象的回收过程。

八、对象状态转换

8.1 对象状态模型

ART中的对象有多种状态,包括活跃状态、可终结状态、已终结状态和已回收状态。在art/runtime/gc/object_state.h中定义了对象状态的枚举:

cpp 复制代码
enum class ObjectState {
    kLive,             // 活跃状态
    kFinalizable,      // 可终结状态
    kFinalized,        // 已终结状态
    kBeingReclaimed,   // 正在被回收状态
    kReclaimed,        // 已回收状态
};

对象在生命周期中会在这些状态之间转换。

8.2 状态转换流程

对象状态转换的流程在垃圾回收过程中实现。例如,当对象不再被引用但有终结器时,会进入可终结状态。在art/runtime/gc/collector/marksweep.cc中,MarkSweep::MarkObject方法实现了对象状态的标记和转换:

cpp 复制代码
void MarkSweep::MarkObject(mirror::Object* obj, uint8_t* card) {
    // 获取对象当前标记状态
    MarkWord mark = obj->GetMarkWord();
    
    // 如果对象未被标记
    if (!mark.IsMarked()) {
        // 标记对象
        obj->SetMarkWord(mark.Mark());
        
        // 如果对象有终结器,将其加入终结器队列
        if (obj->IsClassRootSubclass() && obj->GetClass()->HasFinalizer()) {
            EnqueueFinalizer(obj);
            // 对象状态转换为可终结状态
            obj->SetState(ObjectState::kFinalizable);
        } else {
            // 对象状态保持活跃状态
            obj->SetState(ObjectState::kLive);
        }
        
        // 递归标记对象的引用
        ProcessReferences(obj);
    }
}

该方法首先检查对象是否已被标记,如果未被标记,则标记对象并根据对象是否有终结器进行相应处理。如果对象有终结器,则将其加入终结器队列并将状态转换为可终结状态;否则,保持活跃状态。

九、垃圾回收与对象生命周期

9.1 垃圾回收触发条件

垃圾回收的触发条件在art/runtime/gc/heap.cc中定义。主要触发条件包括内存分配失败、堆内存使用率达到阈值、系统内存压力等。例如,当在Eden空间分配对象失败时,会触发年轻代垃圾回收:

cpp 复制代码
bool Heap::ShouldTriggerCollection(size_t bytes_needed) {
    // 检查当前堆内存使用情况
    size_t used = GetBytesAllocated();
    size_t max = GetMaxSize();
    
    // 如果需要的内存超过当前可用内存,触发垃圾回收
    if (used + bytes_needed > max) {
        return true;
    }
    
    // 如果堆内存使用率达到阈值,触发垃圾回收
    if (used * 100 / max > kHeapUsageThresholdPercent) {
        return true;
    }
    
    // 检查系统内存压力
    if (IsSystemMemoryUnderPressure()) {
        return true;
    }
    
    return false;
}

该方法根据内存需求、堆内存使用率和系统内存压力等因素,判断是否需要触发垃圾回收。

9.2 垃圾回收对对象生命周期的影响

垃圾回收过程直接影响对象的生命周期。在垃圾回收过程中,垃圾回收器会标记所有可达对象,并回收不可达对象。对象的状态会根据垃圾回收的结果发生变化。例如,在年轻代垃圾回收中,存活的对象可能会晋升到老年代,而未被标记的对象将被回收。在art/runtime/gc/collector/scavenger.cc中,Scavenger::PromoteObject方法实现了对象晋升:

cpp 复制代码
void Scavenger::PromoteObject(mirror::Object* obj) {
    // 获取对象年龄
    uint8_t age = obj->GetAge();
    
    // 如果对象年龄达到晋升阈值,晋升到老年代
    if (age >= kMaxScavengeNumber) {
        Heap* heap = Runtime::Current()->GetHeap();
        mirror::Object* new_obj = heap->PromoteToTenured(obj);
        if (new_obj != nullptr) {
            // 更新引用
            UpdateReferences(obj, new_obj);
        }
    } else {
        // 否则,对象留在年轻代的Survivor空间
        CopyToSurvivorSpace(obj);
    }
}

该方法根据对象年龄决定是将对象晋升到老年代还是留在年轻代的Survivor空间,从而影响对象的生命周期和后续垃圾回收行为。

十、内存分配与对象生命周期的性能优化

10.1 内存分配性能优化

内存分配的性能优化主要集中在减少分配延迟和提高吞吐量。ART通过多种方式实现这一目标,包括线程本地分配缓存(TLAB)、对象预分配、内存池技术等。例如,TLAB通过为每个线程分配私有内存区域,减少了多线程环境下的锁竞争,提高了小对象的分配速度。

10.2 对象生命周期管理优化

对象生命周期管理的优化主要集中在减少垃圾回收频率和缩短垃圾回收暂停时间。ART采用分代回收策略,将对象按生命周期分类存储,针对不同代采用不同的回收算法,提高了垃圾回收效率。此外,并发垃圾回收技术允许垃圾回收器与应用程序线程同时运行,减少了垃圾回收对应用程序的影响。

10.3 内存碎片管理

内存碎片会影响内存分配效率和应用程序性能。ART通过多种方式管理内存碎片,包括对象整理、内存压缩等技术。在老年代垃圾回收中,采用标记 - 整理算法,将存活对象移动到连续的内存区域,减少内存碎片。在art/runtime/gc/collector/marksweep_compact.cc中,MarkSweepCompact::Compact方法实现了内存整理:

cpp 复制代码
void MarkSweepCompact::Compact() {
    // 计算存活对象的新位置
    CalculateNewObjectPositions();
    
    // 移动存活对象
    MoveObjects();
    
    // 更新引用
    UpdateReferences();
}

该方法首先计算存活对象的新位置,然后将存活对象移动到新位置,最后更新所有引用,确保引用指向对象的新位置,从而减少内存碎片,提高内存利用率。

十一、内存分配与对象生命周期在不同场景下的应用与挑战

11.1 高并发应用场景

在高并发应用场景下,多个线程同时进行大量的内存分配和对象创建操作,这对内存分配和对象生命周期管理提出了更高的要求。高并发会导致线程间的锁竞争加剧,影响内存分配效率。ART通过优化锁机制、扩大TLAB使用范围等方式应对这一挑战。例如,在art/runtime/object_allocator.cc中,使用细粒度锁和无锁数据结构减少锁竞争:

cpp 复制代码
mirror::Object* ObjectAllocator::AllocWithTLAB(Class* clazz, size_t size,
                                              size_t* bytes_allocated) {
    Thread* self = Thread::Current();
    uint8_t* tlab_pos = self->tlab_pos_;
    uint8_t* new_tlab_pos = tlab_pos + size;
    
    // 使用原子操作检查和更新TLAB指针,避免锁竞争
    if (new_tlab_pos <= self->tlab_end_ &&
        __sync_bool_compare_and_swap(&self->tlab_pos_, tlab_pos, new_tlab_pos)) {
        // TLAB分配成功
        mirror::Object* result = reinterpret_cast<mirror::Object*>(tlab_pos);
        *bytes_allocated = size;
        return result;
    }
    
    // TLAB分配失败,使用锁保护的全局分配路径
    return AllocWithoutTLAB(clazz, size, bytes_allocated);
}

该方法首先尝试使用原子操作在TLAB中分配对象,避免锁竞争。只有当TLAB分配失败时,才使用需要获取锁的全局分配路径,从而减少了高并发场景下的锁竞争,提高了内存分配效率。

11.2 内存敏感型应用场景

对于内存敏感型应用,如嵌入式设备上的轻量级应用、运行在低配置设备上的应用等,内存资源非常有限,需要更精细的内存分配和对象生命周期管理策略。在这种场景下,ART会采用更保守的内存分配策略,减少内存碎片,提高内存利用率。例如,调整各代空间的大小比例,增加老年代空间比例,减少对象晋升频率,从而降低垃圾回收的开销。在art/runtime/gc/heap_tuning.cc中,可能会有针对内存敏感型应用的特殊调优逻辑:

cpp 复制代码
void HeapTuner::TuneForMemorySensitiveApp() {
    // 降低堆内存最大值,避免过度使用内存
    SetMaxHeapSize(kMemorySensitiveMaxHeapSize);
    
    // 调整各代空间比例,增加老年代比例
    SetYoungGenerationRatio(kMemorySensitiveYoungRatio);
    SetOldGenerationRatio(kMemorySensitiveOldRatio);
    
    // 优化垃圾回收阈值,减少回收频率
    SetGcThreshold(kMemorySensitiveGcThreshold);
    
    // 启用更积极的内存压缩,减少碎片
    EnableAggressiveCompaction(true);
}

该方法针对内存敏感型应用,调整堆内存大小、各代空间比例、垃圾回收阈值等参数,并启用更积极的内存压缩策略,以减少内存碎片,提高内存利用率,确保应用在有限的内存资源下稳定运行。

相关推荐
东风西巷11 分钟前
MolyCamCCD复古胶片相机:复古质感,时尚出片
android·数码相机·智能手机·软件需求
君鼎17 分钟前
C++面试需知——并发与多线程
c++·面试
天才测试猿21 分钟前
2025最新软件测试面试题总结【附文档】
自动化测试·软件测试·python·测试工具·面试·职场和发展·测试用例
Alfred king24 分钟前
面试150 除自身以外数组的乘积
leetcode·面试·职场和发展
恋猫de小郭1 小时前
Flutter 里的像素对齐问题,深入理解为什么界面有时候会出现诡异的细线?
android·前端·flutter
liang_jy1 小时前
Java 线程实现方式
android·java·面试
CYRUS_STUDIO1 小时前
逆向某物 App 登录接口:热修复逻辑挖掘隐藏参数、接口完整调用
android·app·逆向
BoomHe2 小时前
Android 源码两种执行脚本的区别
android·源码
前端小巷子2 小时前
跨标签页通信(七):postMessage
前端·javascript·面试
Kapaseker2 小时前
Jetpack Compose的副作用一览
android·kotlin