Android Runtime内存管理全体系解构(46)

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

一、Android Runtime内存管理概述

Android Runtime(ART)是Android系统自Android 5.0(Lollipop)开始采用的运行时环境,取代了之前的Dalvik虚拟机。ART在内存管理方面进行了大量优化,以提升应用程序的性能和稳定性。其内存管理的核心目标是高效地分配和回收内存,避免内存泄漏和内存碎片,同时满足不同类型对象的生命周期管理需求。

ART的内存管理涉及多个组件和机制的协同工作。从整体架构上看,它与Linux内核的内存管理紧密交互,利用内核提供的内存分配接口,如mmapmalloc等,来获取和管理物理内存。在ART内部,又包含了多个子模块,如垃圾回收器(Garbage Collector,简称GC)、内存分配器、对象布局管理等,这些模块共同构成了完整的内存管理体系。

以Android 11的ART源码为例,在art/runtime目录下,包含了众多与内存管理相关的源文件。heap.ccheap.h文件定义了堆内存的相关结构和操作,是内存管理的核心部分;gc目录下存放着各种垃圾回收算法的实现代码;mirror目录则包含了对象布局和对象头的定义。这些源码文件相互协作,实现了ART强大的内存管理功能。

二、ART内存架构基础

2.1 内存区域划分

ART将内存划分为多个不同的区域,每个区域承担着不同的功能。最主要的区域是堆内存(Heap),用于存储Java对象实例。堆内存又进一步细分为多个子区域,以提高内存管理的效率和灵活性。

art/runtime/heap.cc中,定义了堆内存的结构:

cpp 复制代码
class Heap {
 public:
  // 堆内存的各个子区域相关成员变量和操作方法
  // 例如年轻代(Young Generation)
  Space* young_space_;
  // 老年代(Old Generation)
  Space* old_space_;
  // 大对象空间(Large Object Space)
  Space* large_object_space_;
  // 其他辅助空间...
  // 初始化堆内存的方法
  void Initialize(Isolate* isolate);
  // 其他堆内存管理相关方法
  //...
};

年轻代是新创建对象的存储区域,大多数对象在年轻代中很快就会变为不可达,从而被垃圾回收。老年代则用于存储生命周期较长的对象,当对象在年轻代中经历多次垃圾回收后仍然存活,就会被晋升到老年代。大对象空间专门用于存储超过一定大小阈值的对象,避免这些大对象在年轻代和老年代频繁移动带来的开销。

2.2 与Linux内核的交互

ART依赖Linux内核提供的内存管理功能。在申请内存时,ART会调用mmap系统调用来映射一段虚拟内存到进程地址空间。例如,在创建堆内存时,art/runtime/heap.cc中的Heap::Initialize方法会通过mmap来分配内存:

cpp 复制代码
void Heap::Initialize(Isolate* isolate) {
  // 计算需要分配的内存大小
  size_t heap_size = CalculateHeapSize();
  // 使用mmap分配内存
  void* allocated_memory = mmap(nullptr, heap_size, PROT_READ | PROT_WRITE,
                                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (allocated_memory == MAP_FAILED) {
    // 处理内存分配失败的情况
    HandleMemoryAllocationFailure();
  }
  // 初始化堆内存的各个子区域
  InitializeSpaces(allocated_memory, heap_size);
}

当ART不再需要某些内存时,会通过munmap系统调用来释放内存,将其归还给系统。这种与Linux内核的交互方式,使得ART能够充分利用操作系统提供的内存管理机制,同时又能实现自身特有的内存管理策略。

三、对象布局与内存对齐

3.1 对象头结构

在ART中,每个Java对象在内存中都有一个对象头(Object Header),用于存储对象的元数据信息。对象头的结构定义在art/runtime/mirror/object-inl.h中:

cpp 复制代码
class Object {
 public:
  // 对象头包含的信息
  // 指向类的指针,用于获取对象的类信息
  Class* GetClass() const REQUIRES(Locks::mutator_lock_) {
    return reinterpret_cast<Class*>(core_.GetClass());
  }
  // 哈希码,用于对象的哈希表操作
  uint32_t GetHashCode() const REQUIRES(Locks::mutator_lock_) {
    return core_.GetHashCode();
  }
  // 锁信息,用于对象的同步操作
  void* GetMonitor() const REQUIRES(Locks::mutator_lock_) {
    return core_.GetMonitor();
  }
  // 其他对象头相关的操作方法
  //...
 private:
  // 对象头的实际存储结构
  // 包含类指针、哈希码、锁信息等
  ObjectCore core_;
};

对象头中的类指针指向对象所属的类信息,通过这个指针,ART可以获取对象的方法表、字段信息等。哈希码用于对象在哈希表中的快速查找,锁信息则与对象的同步机制相关,例如synchronized关键字的实现就依赖于对象头中的锁信息。

3.2 内存对齐规则

为了提高内存访问的效率,ART采用了内存对齐的策略。内存对齐是指将对象或数据结构的内存地址按照一定的规则进行对齐,使得内存访问能够以最有效的方式进行。

art/runtime/base/macros.h中,定义了一些与内存对齐相关的宏:

cpp 复制代码
// 计算结构体或类的对齐边界
#define ROUND_UP_TO_ALIGNMENT(x, alignment) \
  (((x) + ((alignment) - 1)) & ~((alignment) - 1))
// 确保指针按照指定的对齐边界对齐
#define ALIGN_POINTER(ptr, alignment) \
  reinterpret_cast<void*>(ROUND_UP_TO_ALIGNMENT(reinterpret_cast<uintptr_t>(ptr), alignment))

例如,一个对象的大小可能不是CPU字长的整数倍,通过内存对齐,会在对象的末尾填充一些字节,使其大小满足对齐要求。这样,CPU在访问对象的各个字段时,可以一次读取多个字段,提高访问效率。同时,内存对齐也有助于硬件设备更高效地处理内存操作,避免出现未对齐内存访问导致的性能问题或硬件错误。

四、内存分配器原理

4.1 快速分配路径

ART为了提高内存分配的效率,实现了快速分配路径(Fast Allocation Path)。当创建新的Java对象时,在大多数情况下,ART会尝试通过快速分配路径来完成内存分配,避免复杂的内存分配逻辑。

art/runtime/heap/internals.cc中,Heap::AllocObject方法实现了对象的分配,其中就包含了快速分配路径的逻辑:

cpp 复制代码
mirror::Object* Heap::AllocObject(Class* clazz, size_t extra_bytes,
                                  PretenureFlag pretenure_flag,
                                  bool from_quick_alloc) {
  // 检查是否可以使用快速分配路径
  if (from_quick_alloc && ShouldUseQuickAlloc(clazz)) {
    // 通过快速分配路径分配对象
    mirror::Object* result = QuickAllocObject(clazz, extra_bytes, pretenure_flag);
    if (result != nullptr) {
      return result;
    }
  }
  // 如果快速分配路径失败,采用常规的分配方式
  return AllocObjectSlowPath(clazz, extra_bytes, pretenure_flag);
}

快速分配路径主要依赖于预先分配好的内存块和一些优化策略。例如,在年轻代中,会维护一个名为Allocator的组件,它负责管理可用的内存空间。当需要分配对象时,Allocator会从预先分配的内存块中快速找到合适的位置进行分配,减少了内存分配的时间开销。

4.2 大对象分配

对于大对象(通常是指超过一定大小阈值的对象),ART采用了专门的大对象空间(Large Object Space)进行存储。大对象的分配逻辑在art/runtime/heap/large_object_space.cc中实现:

cpp 复制代码
mirror::Object* LargeObjectSpace::AllocObject(Class* clazz, size_t object_size) {
  // 检查大对象空间是否有足够的内存
  if (AvailableMemory() < object_size) {
    // 如果内存不足,触发垃圾回收或其他处理逻辑
    HandleLargeObjectAllocationFailure(object_size);
    return nullptr;
  }
  // 在大对象空间中分配内存
  void* allocated_memory = AllocateMemoryFromLargeObjectSpace(object_size);
  if (allocated_memory == nullptr) {
    return nullptr;
  }
  // 创建对象并初始化
  mirror::Object* object = CreateObject(clazz, allocated_memory, object_size);
  return object;
}

大对象由于其占用内存较大,如果频繁在年轻代和老年代之间移动,会带来较大的开销。因此,将大对象直接存储在大对象空间中,可以减少这种不必要的移动,提高内存管理的效率。同时,在大对象空间中,也有相应的回收策略,当大对象不再被引用时,会及时回收其占用的内存。

五、垃圾回收器机制

5.1 垃圾回收算法概述

ART支持多种垃圾回收算法,以适应不同的应用场景和内存管理需求。常见的垃圾回收算法包括标记-清除(Mark-Sweep)、标记-整理(Mark-Compact)、复制(Copying)等。这些算法在ART中都有相应的实现,并且可以根据运行时的情况动态切换。

标记-清除算法首先会遍历所有可达对象,将它们标记为存活状态,然后清除所有未被标记的对象,即垃圾对象。标记-整理算法在标记-清除的基础上,会将存活对象移动到内存的一端,整理内存空间,避免内存碎片。复制算法则是将存活对象从一个区域复制到另一个区域,同时清除原区域的垃圾对象。

5.2 分代垃圾回收

ART采用了分代垃圾回收(Generational Garbage Collection)策略,将对象按照生命周期的长短划分为不同的代,如年轻代和老年代,然后对不同代采用不同的垃圾回收算法和回收频率。

在年轻代中,由于大多数对象的生命周期较短,ART采用复制算法进行垃圾回收。art/runtime/gc/collector/scavenger.cc中实现了年轻代的垃圾回收逻辑:

cpp 复制代码
void Scavenger::Collect(GcType gc_type, GcCause gc_cause) {
  // 标记阶段:标记所有可达对象
  MarkObjects();
  // 复制阶段:将存活对象复制到新的区域
  CopyObjects();
  // 更新对象的引用关系
  UpdateReferences();
}

对于老年代,由于对象生命周期较长,垃圾回收频率较低,ART通常采用标记-整理算法。art/runtime/gc/collector/marksweep_compact.cc中实现了老年代的垃圾回收:

cpp 复制代码
void MarkSweepCompact::Collect(GcType gc_type, GcCause gc_cause) {
  // 标记阶段
  MarkObjects();
  // 整理阶段:移动存活对象,释放垃圾对象占用的内存
  CompactObjects();
  // 更新对象的引用关系
  UpdateReferences();
}

分代垃圾回收策略可以根据不同代中对象的特点,选择最适合的垃圾回收算法,从而提高垃圾回收的效率,减少应用程序的暂停时间。

六、内存屏障与并发控制

6.1 内存屏障原理

内存屏障(Memory Barrier)是一种用于保证内存操作顺序的机制。在多线程环境下,由于CPU的乱序执行和缓存一致性等问题,内存操作的执行顺序可能与程序代码中的顺序不一致。内存屏障可以强制CPU按照特定的顺序执行内存操作,确保程序的正确性。

在ART中,内存屏障的实现依赖于硬件提供的指令和编译器的支持。例如,在art/runtime/base/memory_ordering.h中,定义了一些与内存屏障相关的宏:

cpp 复制代码
// 内存屏障操作,确保在该屏障之前的写操作都完成
#define MEMORY_BARRIER() __asm__ volatile("mfence" ::: "memory")
// 确保在该屏障之前的读操作都完成
#define READ_MEMORY_BARRIER() __asm__ volatile("lfence" ::: "memory")
// 确保在该屏障之前的写操作都完成
#define WRITE_MEMORY_BARRIER() __asm__ volatile("sfence" ::: "memory")

这些内存屏障宏在ART的代码中被广泛使用,特别是在涉及对象的创建、销毁、引用更新等操作时,用于保证内存操作的顺序性和一致性。例如,在对象的赋值操作中,为了确保新的引用能够被其他线程正确看到,会使用内存屏障来保证写操作的完成。

6.2 并发垃圾回收

ART支持并发垃圾回收(Concurrent Garbage Collection),以减少垃圾回收对应用程序性能的影响。并发垃圾回收允许垃圾回收器在应用程序线程运行的同时进行垃圾回收操作,避免了长时间的应用程序暂停。

在并发垃圾回收过程中,需要解决一些复杂的问题,如对象的并发修改和引用关系的变化。ART通过一系列的技术手段来解决这些问题,例如使用写屏障(Write Barrier)来记录对象引用的变化,以及使用并发标记和并发整理等算法来实现垃圾回收的并发执行。

art/runtime/gc/collector/concurrent_mark_sweep.cc中实现了并发标记-清除垃圾回收算法:

cpp 复制代码
void ConcurrentMarkSweep::Collect(GcType gc_type, GcCause gc_cause) {
  // 初始阶段:暂停应用程序线程,进行一些初始化操作
  InitialMark();
  // 并发标记阶段:与应用程序线程并发执行,标记可达对象
  ConcurrentMark();
  // 重新标记阶段:再次暂停应用程序线程,处理并发标记期间的对象引用变化
  Remark();
  // 并发清除阶段:与应用程序线程并发执行,清除垃圾对象
  ConcurrentSweep();
}

并发垃圾回收虽然可以提高应用程序的响应性,但也增加了内存管理的复杂性,需要精细的设计和实现来确保垃圾回收的正确性和高效性。

七、JNI与内存管理

7.1 JNI中的内存交互

Java Native Interface(JNI)允许Java代码与本地代码(如C、C++代码)进行交互。在JNI中,涉及到Java对象和本地内存之间的转换和管理,这对内存管理提出了特殊的要求。

当Java代码调用本地方法时,需要将Java对象传递给本地代码。在本地代码中,可以通过JNI提供的接口来访问和操作这些Java对象。例如,在art/runtime/native/jni/jni_env_ext.cc中,定义了JNIEnv类的扩展方法,用于在本地代码中操作Java对象:

cpp 复制代码
jobject JNIEnvExt::NewObject(jclass clazz, jmethodID methodID, ...) {
  // 检查类和方法的有效性
  if (clazz == nullptr || methodID == nullptr) {
    return nullptr;
  }
  // 调用ART的对象创建接口来创建Java对象
  return art::Thread::Current()->GetJniEnv()->NewObject(clazz, methodID);
}

当本地代码需要返回Java对象给Java代码时,也需要确保对象的生命周期和内存管理的正确性。同时,本地代码在使用Java对象时,需要注意对象的引用计数和垃圾回收的影响,避免出现对象被提前回收或内存泄漏的问题。

7.2 本地内存管理

在JNI中,本地代码还需要管理自己的内存。本地内存的分配和释放通常使用C、C++的内存管理函数,如mallocfree。但是,这些本地内存与Java堆内存是相互独立的,需要开发者手动管理它们之间的关系。

例如,如果本地代码创建了一个包含大量数据的结构体,并希望在Java代码中使用这些数据,一种常见的做法是将结构体的数据复制到Java对象中,或者通过JNI引用(JNI Reference)来保持对本地内存的引用。在art/runtime/native/jni/jni_reference_table.cc中,实现了JNI引用表的管理,用于跟踪和管理JNI引用:

cpp 复制代码
void JniReferenceTable::AddReference(jobject reference) {
  // 检查引用表是否已满
  if (IsFull()) {
    // 处理引用表已满的情况,例如扩展引用表或抛出异常
    HandleReferenceTableFull();
  }
  // 将引用添加到引用表中
  references_.push_back(reference);
}

通过正确管理JNI引用和本地内存,开发者可以在Java代码和本地代码之间实现高效、安全的内存交互。

相关推荐
code bean24 分钟前
【C#】 C#中 nameof 和 ToString () 的用法与区别详解
android·java·c#
佛系小嘟嘟1 小时前
Android Studio Jetpack Compose毛玻璃特效按钮
android·ide·android studio
巴巴_羊2 小时前
6-16阿里前端面试记录
前端·面试·职场和发展
然我3 小时前
面试官最爱的 “考试思维”:用闭包秒杀递归难题 🚀
前端·javascript·面试
用户2018792831673 小时前
MagiskHidePropsConf 原理与实战故事
android
whysqwhw4 小时前
Egloo 项目结构分析
android
Wgllss4 小时前
大型异步下载器(二):基于kotlin+Compose+协程+Flow+Channel+ OKhttp 实现多文件异步同时分片断点续传下载
android·架构·android jetpack
yzpyzp4 小时前
KAPT 的版本如何升级,是跟随kotlin的版本吗
android·kotlin·gradle
lovebugs4 小时前
Java中的OutOfMemoryError:初学者的诊断与解决指南
jvm·后端·面试
泓博4 小时前
KMP(Kotlin Multiplatform)简单动画
android·开发语言·kotlin