Android Runtime分代式堆内存布局深度解析(48)

71 阅读24分钟

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

一、分代式堆内存布局概述

在Android Runtime(ART)中,分代式堆内存布局是提升内存管理效率的核心策略。它将堆内存划分为年轻代(Young Generation)、老年代(Old Generation)和大对象空间(Large Object Space),针对不同生命周期的对象采用差异化管理方式。这种布局基于“弱分代假说”,即大部分对象生命周期短暂,少量对象生命周期较长。通过分代,ART能够为不同类型对象定制回收策略,减少垃圾回收开销,提升应用性能与稳定性。

从源码层面看,在art/runtime/heap.h文件中,定义了Heap类,该类管理着整个堆内存空间,其中包含了对年轻代、老年代和大对象空间的引用:

class Heap {
public:
  // 年轻代空间指针
  Space* young_space_;
  // 老年代空间指针
  Space* old_space_;
  // 大对象空间指针
  Space* large_object_space_;
  // 堆内存初始化等相关方法
  void Initialize(Isolate* isolate);
  //...
};

Heap类的Initialize方法负责初始化这些内存空间,其逻辑在art/runtime/heap.cc中实现。通过这样的架构设计,ART实现了分代式堆内存的高效管理。

二、年轻代(Young Generation)内存布局与管理

2.1 年轻代整体架构

年轻代是新创建Java对象的主要存放区域。在ART中,年轻代采用“复制算法”进行垃圾回收,将其划分为两个大小相等的半区:Eden空间和两个Survivor空间(From Space和To Space)。这种设计使得在垃圾回收时,可将存活对象从一个半区复制到另一个半区,同时清理原半区,从而高效回收垃圾。

art/runtime/gc/space/space.h文件中,定义了Space类,年轻代的各个空间均继承自该类。EdenSpaceSurvivorSpace类分别用于表示Eden空间和Survivor空间,它们在art/runtime/gc/space/eden_space.hart/runtime/gc/space/survivor_space.h中定义:

// Eden空间类
class EdenSpace : public Space {
public:
  // Eden空间相关的初始化方法
  void Initialize(uint8_t* start, size_t size, const char* name);
  // 分配对象的方法
  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);
  // 处理对象复制等操作的方法
  void CopyObject(mirror::Object* src, mirror::Object* dst);
  //...
};

2.2 内存分配流程

当创建新对象时,优先在Eden空间分配内存。在art/runtime/heap/internals.cc中,Heap::AllocObject方法实现了对象分配逻辑,其中涉及年轻代的分配流程:

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

若Eden空间内存不足,会触发年轻代垃圾回收(Minor GC)。在art/runtime/gc/collector/scavenger.cc中,Scavenger::Collect方法实现了年轻代垃圾回收逻辑:

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角色,为下一次分配和回收做准备。

2.3 对象晋升机制

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

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

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

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

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

三、老年代(Old Generation)内存布局与管理

3.1 老年代架构特点

老年代用于存储生命周期较长的对象,这些对象经过年轻代多次回收后依然存活。与年轻代不同,老年代采用“标记-整理(Mark-Compact)”算法进行垃圾回收。这种算法先标记所有可达对象,然后将存活对象移动到内存一端,释放中间的垃圾内存空间,避免内存碎片。

art/runtime/gc/space/tenured_space.h中,定义了TenuredSpace类表示老年代空间,它同样继承自Space类:

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();
  //...
};

3.2 内存分配与回收流程

当年轻代对象晋升或直接在老年代分配对象时,TenuredSpace::AllocObject方法负责内存分配:

mirror::Object* TenuredSpace::AllocObject(Class* clazz, size_t extra_bytes) {
  // 检查老年代是否有足够内存
  if (AvailableMemory() < CalculateObjectSize(clazz, extra_bytes)) {
    // 内存不足时触发老年代垃圾回收
    MarkCompact();
    if (AvailableMemory() < CalculateObjectSize(clazz, extra_bytes)) {
      // 回收后仍不足,处理内存分配失败情况
      HandleAllocationFailure();
      return nullptr;
    }
  }
  // 分配内存并初始化对象
  void* allocated_memory = AllocateMemory(CalculateObjectSize(clazz, extra_bytes));
  mirror::Object* object = CreateObject(clazz, allocated_memory);
  return object;
}

老年代垃圾回收在TenuredSpace::MarkCompact方法中实现,其流程分为标记和整理两个阶段。在art/runtime/gc/collector/marksweep_compact.cc中,详细实现了这两个阶段的逻辑:

void MarkSweepCompact::MarkObjects() {
  // 从根集合开始遍历,标记所有可达对象
  RootVisitor root_visitor(this);
  root_visitor.VisitRoots();
}

void MarkSweepCompact::CompactObjects() {
  // 计算存活对象新位置
  CalculateNewObjectAddresses();
  // 移动存活对象到新位置
  MoveLiveObjects();
  // 更新对象引用关系
  UpdateReferences();
}

通过标记-整理算法,老年代能有效回收垃圾内存,整理内存空间,保证后续内存分配的高效性。

3.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) {
    // 全量垃圾回收逻辑
    // 从根集合开始标记所有可达对象
    MarkRoots();
    // 标记老年代中引用年轻代对象的卡表
    card_table_.MarkCardsForOldToYoungReferences();
    // 扫描年轻代,标记可达对象
    scavenger_.MarkObjectsInYoungGen();
    // 标记老年代可达对象
    marksweep_compact_.MarkObjectsInOldGen();
    // 整理老年代内存
    marksweep_compact_.CompactOldGen();
    // 处理大对象空间
    large_object_space_.CollectLargeObjects();
    // 更新所有对象引用关系
    UpdateAllReferences();
  }
}

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

四、大对象空间(Large Object Space)内存布局与管理

4.1 大对象空间设计目的

大对象空间专门用于存储超过一定大小阈值的对象。这些大对象若频繁在年轻代和老年代移动,会产生较大开销。将其单独存储在大对象空间,可避免这种不必要的移动,同时简化垃圾回收逻辑。大对象空间的大小阈值在art/runtime/heap.cc中定义:

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

4.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();
  //...
};

当分配对象大小超过kLargeObjectThreshold时,LargeObjectSpace::AllocObject方法负责在大对象空间分配内存:

mirror::Object* LargeObjectSpace::AllocObject(Class* clazz, size_t object_size) {
  // 检查大对象空间是否有足够内存
  if (AvailableMemory() < object_size) {
    // 内存不足时触发大对象空间垃圾回收
    CollectLargeObjects();
    if (AvailableMemory() < object_size) {
      // 回收后仍不足,处理内存分配失败情况
      HandleLargeObjectAllocationFailure(object_size);
      return nullptr;
    }
  }
  // 分配内存并创建对象
  void* allocated_memory = AllocateMemory(object_size);
  mirror::Object* object = CreateObject(clazz, allocated_memory, object_size);
  return object;
}

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

void LargeObjectSpace::CollectLargeObjects() {
  // 标记阶段:从根集合开始,标记所有可达大对象
  RootVisitor root_visitor(this);
  root_visitor.VisitRootsForLargeObjects();
  // 清除阶段:遍历大对象空间,释放未标记的大对象占用内存
  for (mirror::Object* obj : objects_in_large_object_space) {
    if (!obj->IsMarked()) {
      FreeMemory(obj);
    }
  }
}

通过这种方式,大对象空间能高效管理大对象的内存分配与回收,减少对其他内存区域的影响。

五、分代式堆内存的对象引用关系管理

5.1 跨代引用处理

由于堆内存分为不同代,对象之间可能存在跨代引用。例如,老年代对象可能引用年轻代对象,若不特殊处理,在年轻代垃圾回收时,可能误将被老年代引用的年轻代对象当作垃圾回收。为解决这个问题,ART引入了记忆集(Remembered Set)机制。

art/runtime/gc/collector/card_table.cc中,实现了记忆集的核心数据结构——卡表(Card Table)。卡表将堆内存划分为固定大小的“卡”,当老年代对象引用年轻代对象时,对应的卡会被标记。在年轻代垃圾回收时,只需扫描被标记的卡,就能找到所有跨代引用:

class CardTable {
public:
  // 标记卡的方法,当老年代对象引用年轻代对象时调用
  void MarkCard(uint8_t* card_address) {
    // 通过位运算标记对应卡
    uint8_t* card = GetCard(card_address);
    *card |= kCardMarkBit;
  }
  // 扫描卡表,获取跨代引用对象的方法
  void ScanCards(Closure* closure) {
    for (size_t i = 0; i < card_table_size_; ++i) {
      uint8_t* card = &card_table_[i];
      if (*card & kCardMarkBit) {
        closure->Run(card);
      }
    }
  }
  //...
};

Scavenger::MarkObjects方法中,利用卡表处理跨代引用:

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

5.2 引用类型与内存管理

ART支持多种引用类型,如强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference),不同引用类型在内存管理中有不同作用。

强引用是最常见的引用类型,只要存在强引用,对象就不会被垃圾回收。软引用在系统内存不足时,会被垃圾回收器回收,常用于实现缓存机制。弱引用在垃圾回收时,无论内存是否充足,只要对象没有其他强引用,就会被回收,常用于解决内存泄漏问题。虚引用主要用于跟踪对象被垃圾回收的状态。

art/runtime/mirror/reference.h中,定义了各种引用类型的类:

// 强引用类(实际由Java对象引用隐式实现)
// 软引用类
class SoftReference : public Reference {
public:
  // 获取软引用指向的对象
  mirror::Object* Get() const {
    return AccessibleObject();
  }
  //...
};
// 弱引用类
class WeakReference : public Reference {
public:
  // 获取弱引用指向的对象
  mirror::Object* Get() const {
    return AccessibleObject();
  }
  //...
};
// 虚引用类
class PhantomReference : public Reference {
public:
  // 获取虚引用队列
  ReferenceQueue* GetQueue() const {
    return queue_;
  }
  //...
};

垃圾回收器在回收对象时,会根据引用类型判断对象是否可回收,从而实现更灵活的内存管理。例如,在垃圾回收过程中,对于软引用对象,会在内存紧张时将其加入待回收队列;对于弱引用对象,只要没有强引用关联,就直接回收其所指向的对象。

六、分代式堆内存布局的初始化与动态调整

6.1 内存布局初始化

在ART启动时,会对分代式堆内存布局进行初始化。在art/runtime/heap.cc中,Heap::Initialize方法负责整个堆内存的初始化,包括年轻代、老年代和大对象空间的初始化:

void Heap::Initialize(Isolate* isolate) {
  // 计算堆内存总

6.1 内存布局初始化

在ART启动时,会对分代式堆内存布局进行初始化。在art/runtime/heap.cc中,Heap::Initialize方法负责整个堆内存的初始化,包括年轻代、老年代和大对象空间的初始化:

void Heap::Initialize(Isolate* isolate) {
  // 从系统参数或配置中获取堆内存初始大小配置
  size_t initial_heap_size = GetInitialHeapSizeFromConfig();
  // 计算年轻代、老年代和大对象空间的初始大小比例,这里假设一种常见比例关系
  size_t young_generation_size = initial_heap_size * 0.3; 
  size_t old_generation_size = initial_heap_size * 0.6; 
  size_t large_object_space_size = initial_heap_size * 0.1; 

  // 初始化年轻代空间,包含Eden和两个Survivor区域
  young_space_ = new SpaceCollection();
  young_space_->AddSpace(new EdenSpace());
  young_space_->AddSpace(new SurvivorSpace());
  young_space_->AddSpace(new SurvivorSpace());
  young_space_->InitializeSpaces(AllocateMemory(young_generation_size), young_generation_size);

  // 初始化老年代空间
  old_space_ = new TenuredSpace();
  old_space_->Initialize(AllocateMemory(old_generation_size), old_generation_size, "Old Space");

  // 初始化大对象空间
  large_object_space_ = new LargeObjectSpace();
  large_object_space_->Initialize(AllocateMemory(large_object_space_size), 
                                  large_object_space_size, "Large Object Space");

  // 初始化卡表等辅助数据结构,用于管理跨代引用
  card_table_ = new CardTable();
  card_table_->Initialize(CalculateCardTableSize(initial_heap_size));

  // 初始化与垃圾回收相关的组件,如年轻代回收器、老年代回收器
  scavenger_ = new Scavenger(this);
  marksweep_compact_ = new MarkSweepCompact(this);
}

上述代码首先确定各代内存空间的初始大小,接着分别对年轻代、老年代和大对象空间进行实例化与初始化操作,同时还创建并初始化了卡表 以及垃圾回收相关组件,为后续内存管理和垃圾回收工作奠定基础。

6.2 动态调整机制

随着应用的运行,内存使用情况会不断发生变化,ART具备动态调整分代式堆内存布局的能力,以适应不同的内存使用场景。

6.2.1 年轻代大小动态调整

当年轻代频繁触发垃圾回收(Minor GC),且每次回收后剩余空间仍不足以满足新对象分配需求时,ART会考虑增大年轻代的空间。在art/runtime/gc/heap.cc中,通过监测年轻代的垃圾回收频率和内存使用情况来决定是否调整:

void Heap::MonitorYoungGeneration() {
  // 记录最近一段时间内的Minor GC次数
  static uint32_t minor_gc_count = 0; 
  // 记录每次Minor GC后剩余的可用内存
  static size_t remaining_memory = 0; 

  minor_gc_count++;
  remaining_memory = young_space_->AvailableMemory();

  // 如果Minor GC次数超过阈值,且剩余内存持续较少
  if (minor_gc_count > kMinorGcThreshold && remaining_memory < kYoungGenLowMemoryThreshold) {
    // 计算可扩展的内存大小,这里从堆内存总大小的空闲部分分配
    size_t expand_size = CalculateAvailableMemoryForExpansion() * 0.2; 
    // 扩展年轻代空间
    young_space_->ExpandSpace(expand_size);
    // 调整老年代或大对象空间的大小,保持堆内存总量平衡
    AdjustOtherSpacesAfterYoungGenExpansion(expand_size);
    // 重置计数和监测参数
    minor_gc_count = 0;
    remaining_memory = young_space_->AvailableMemory();
  }
}

反之,当年轻代内存使用率长期较低时,为了更合理地利用内存,ART会适当缩小年轻代空间,将释放的内存分配给老年代或大对象空间。

6.2.2 老年代与大对象空间的调整

老年代的内存调整主要与对象晋升和内存分配失败相关。当老年代内存不足,触发标记 - 整理回收后仍然无法满足对象分配需求时,可能会触发全量垃圾回收(Full GC)。如果Full GC后依然内存不足,且堆内存还有可扩展空间(如系统允许堆内存增长),则会扩展老年代空间:

void TenuredSpace::HandleAllocationFailure() {
  // 尝试触发全量垃圾回收
  Heap::Current()->CollectGarbage(kGcTypeFull, kGcCauseAllocationFailure);
  if (AvailableMemory() < kAllocationRequestSize) {
    // 如果Full GC后仍不足,且堆内存可扩展
    if (Heap::Current()->CanExpandHeap()) {
      size_t expand_size = CalculateRequiredMemoryForExpansion();
      // 扩展老年代空间
      ExpandSpace(expand_size);
    } else {
      // 无法扩展时,抛出内存不足异常
      ThrowOutOfMemoryError();
    }
  }
}

大对象空间的调整相对简单,当大对象空间内存不足时,会先进行垃圾回收。若回收后仍无法满足大对象分配需求,且堆内存有可扩展空间,同样会扩展大对象空间。此外,在某些情况下,如果大对象空间长期使用率较低,也可能会将部分空闲内存重新分配给其他代空间 。

七、分代式堆内存布局与并发处理

7.1 并发垃圾回收与分代布局

ART支持并发垃圾回收,以减少垃圾回收对应用程序性能的影响。在并发垃圾回收过程中,分代式堆内存布局的特点对回收策略和实现方式有着重要影响。

对于年轻代,由于其采用复制算法且对象生命周期较短,并发回收相对简单。在art/runtime/gc/collector/concurrent_scavenger.cc中实现的并发年轻代回收,在标记阶段,可以与应用线程并发执行。通过写屏障(Write Barrier)技术来记录应用线程对对象引用的修改,保证标记阶段能够准确标记所有可达对象。在复制阶段,需要短暂暂停应用线程,以确保存活对象的复制操作不会受到应用线程干扰 :

void ConcurrentScavenger::MarkObjects() {
  // 启动并发标记线程
  StartConcurrentMarkThread();
  // 应用线程继续执行,写屏障记录对象引用变化
  while (ConcurrentMarkingInProgress()) {
    // 应用线程执行过程中,写屏障处理逻辑
    HandleWriteBarrierForConcurrentMarking();
  }
  // 等待并发标记完成
  WaitForConcurrentMarkToFinish();
}

void ConcurrentScavenger::CopyObjects() {
  // 暂停应用线程
  SuspendAllThreads();
  // 执行对象复制操作
  DoCopyObjects();
  // 恢复应用线程
  ResumeAllThreads();
}

老年代的并发回收更为复杂,因为其采用标记 - 整理算法,且对象生命周期长、引用关系复杂。在并发标记 - 整理回收过程中,需要多次暂停和恢复应用线程,以保证标记的准确性和内存整理的正确性。在标记阶段,先进行初始标记(暂停应用线程),然后进行并发标记(与应用线程并发执行),最后进行重新标记(再次暂停应用线程,处理并发标记期间的引用变化)。在整理阶段,同样需要暂停应用线程 :

void ConcurrentMarkSweepCompact::MarkObjects() {
  // 初始标记阶段,暂停应用线程
  InitialMark();
  // 并发标记阶段,与应用线程并发执行
  StartConcurrentMark();
  while (ConcurrentMarkingInProgress()) {
    // 处理写屏障记录的引用变化
    HandleWriteBarrierForConcurrentMarking();
  }
  // 重新标记阶段,暂停应用线程
  Remark();
}

void ConcurrentMarkSweepCompact::CompactObjects() {
  // 暂停应用线程
  SuspendAllThreads();
  // 执行内存整理操作
  DoCompactObjects();
  // 恢复应用线程
  ResumeAllThreads();
}

7.2 多线程环境下的内存分配并发控制

在多线程环境下,多个线程可能同时请求内存分配,为了保证内存分配的正确性和高效性,ART采用了多种并发控制手段。

对于年轻代的内存分配,在EdenSpace::AllocObject方法中,使用轻量级锁(如自旋锁)来保护内存分配操作。当多个线程同时尝试在Eden空间分配对象时,只有一个线程能够获取锁并进行分配操作,其他线程会自旋等待,直到锁可用:

mirror::Object* EdenSpace::AllocObject(Class* clazz, size_t extra_bytes) {
  SpinLock lock(&allocation_lock_);
  if (AvailableMemory() < CalculateObjectSize(clazz, extra_bytes)) {
    // 内存不足,触发年轻代垃圾回收
    TriggerMinorGc();
    if (AvailableMemory() < CalculateObjectSize(clazz, extra_bytes)) {
      return nullptr;
    }
  }
  // 分配内存并创建对象
  void* allocated_memory = AllocateMemory(CalculateObjectSize(clazz, extra_bytes));
  return CreateObject(clazz, allocated_memory);
}

老年代和大对象空间的内存分配,由于操作相对复杂且频率较低,通常采用互斥锁进行保护。在TenuredSpace::AllocObjectLargeObjectSpace::AllocObject方法中,使用互斥锁来确保同一时刻只有一个线程能够进行内存分配操作 :

mirror::Object* TenuredSpace::AllocObject(Class* clazz, size_t extra_bytes) {
  MutexLock lock(&allocation_mutex_);
  // 检查内存并进行分配操作
  //...
}

mirror::Object* LargeObjectSpace::AllocObject(Class* clazz, size_t object_size) {
  MutexLock lock(&allocation_mutex_);
  // 检查内存并进行分配操作
  //...
}

此外,为了进一步提高并发性能,ART还采用了线程本地分配缓冲(Thread - Local Allocation Buffer,TLAB)技术。每个线程拥有自己的TLAB,在分配对象时,优先在TLAB中进行分配,只有当TLAB耗尽时,才会去竞争全局的内存分配资源,从而减少线程之间的竞争,提高内存分配效率 。

八、分代式堆内存布局与JNI交互

8.1 JNI本地代码对分代式堆内存的访问

当Java代码通过JNI调用本地C/C++代码时,本地代码可能需要访问Java堆内存中的对象。由于分代式堆内存布局的存在,本地代码需要遵循特定的规则来正确访问和操作对象。

在本地代码中,通过JNI提供的接口获取Java对象的引用。例如,使用GetObjectFieldSetObjectField等函数来访问和修改对象的字段。在访问对象时,需要注意对象所在的代空间。对于年轻代的对象,由于其可能在垃圾回收过程中被移动(复制算法),因此在进行长时间操作时,需要确保对象的引用不会失效。ART通过句柄(Handle)机制来解决这个问题,在art/runtime/native/jni/jni_handles.cc中实现了句柄的管理:

Handle<mirror::Object> CreateHandle(mirror::Object* obj) {
  // 判断对象所在的代空间
  if (obj->IsInYoungGeneration()) {
    // 为年轻代对象创建句柄,确保在垃圾回收后引用仍有效
    return CreateYoungGenerationHandle(obj);
  } else {
    return CreateGeneralHandle(obj);
  }
}

对于老年代和大对象空间的对象,由于它们在垃圾回收过程中相对稳定(老年代采用标记 - 整理,大对象空间采用标记 - 清除),访问时相对简单,但同样需要注意对象的生命周期和引用关系 。

8.2 JNI本地内存与分代式堆内存的协同管理

JNI本地代码会分配和管理自己的内存,这些本地内存与Java堆内存是相互独立的。然而,在实际应用中,常常需要在两者之间进行数据传递和共享,这就需要进行协同管理。

当本地代码创建的数据需要传递给Java对象时,一种常见的做法是在Java堆内存中创建对应的对象,并将本地数据复制到Java对象中。在这个过程中,需要考虑内存的分配和释放。例如,在将本地的字节数组数据传递给Java的byte[]对象时:

jbyteArray CreateJavaByteArrayFromNative(JNIEnv* env, const jbyte* native_array, jsize length) {
  // 在Java堆内存中创建字节数组对象
  jbyteArray java_array = env->NewByteArray(length);
  if (java_array == nullptr) {
    // 处理内存分配失败情况
    HandleJavaOutOfMemoryError();
    return nullptr;
  }
  // 将本地数据复制到Java数组中
  env->SetByteArrayRegion(java_array, 0, length, native_array);
  return java_array;
}

反之,当Java对象的数据需要传递给本地代码时,也需要进行类似的操作。同时,还需要注意对象的引用关系和垃圾回收的影响。例如,如果本地代码持有对Java对象的引用,需要确保在不再使用该对象时,及时释放引用,避免对象无法被垃圾回收而导致内存泄漏 。

此外,在JNI调用过程中,还可能涉及到大对象的处理。如果本地代码创建的大对象需要在Java中使用,可能需要将其分配到大对象空间(如果符合大对象标准),以避免对年轻代和老年代内存管理造成影响 。

九、分代式堆内存布局的性能优化策略

9.1 减少垃圾回收开销

垃圾回收是分代式堆内存管理中的重要操作,但频繁的垃圾回收会带来性能开销。为了减少垃圾回收开销,ART采用了多种优化策略。

首先,通过合理调整各代空间的大小比例,可以减少垃圾回收的频率。例如,适当增大年轻代空间,可以容纳更多新创建的对象,减少Minor GC的触发次数。在art/runtime/gc/heap_tuning.cc中,包含了堆内存空间大小调整的相关策略和算法:

void HeapTuner::TuneGenerationSizes() {
  // 根据应用的内存使用模式和性能指标,计算各代空间的最优大小
  size_t young_size = CalculateOptimalYoungGenerationSize();
  size_t old_size = CalculateOptimalOldGenerationSize();
  size_t large_size = CalculateOptimalLargeObjectSpaceSize();

  // 调整堆内存中各代空间的大小
  Heap::Current()->ResizeYoungGeneration(young_size);
  Heap::Current()->ResizeOldGeneration(old_size);
  Heap::Current()->ResizeLargeObjectSpace(large_size);
}

其次,优化垃圾回收算法的实现细节也能降低开销。例如,在年轻代的复制算法中,通过减少对象复制过程中的数据移动次数和优化对象地址映射表的更新方式,可以提高复制效率。在老年代的标记 - 整理算法中,采用更高效的标记和整理策略,如并行标记、增量式整理等,减少垃圾回收暂停时间 。

9.2 降低内存碎片

内存碎片会降低内存分配的效率,影响系统性能。在分代式堆内存布局中,不同代空间采用不同的策略来降低内存碎片。

年轻代由于采用复制算法,在每次垃圾回收后,存活对象会被集中复制到一个Survivor空间,不会产生内存碎片。老年代采用标记 - 整理算法,通过将存活对象移动到内存一端,释放中间的空闲内存,从而整理内存空间,减少碎片。为了进一步优化,ART还会在内存分配过程中,尽量采用首次适应(First - Fit)或最佳适应(Best - Fit)算法,选择最适合的空闲内存块进行分配,避免产生过多小的空闲内存块 。

对于大对象空间,由于大对象的分配和回收相对独立,为了减少碎片,会采用边界对齐和块合并等技术。当大对象被回收后,会检查相邻的空闲内存块是否可以合并,以形成更大的空闲内存块,提高后续大对象分配的成功率 。

9.3 提升内存访问效率

内存访问效率对应用性能有着直接影响。在分代式堆内存布局中,通过对象布局优化和缓存友好设计来提升内存访问效率。

对象布局方面,ART会根据对象的类型和字段分布,合理安排对象在内存中的布局。例如,将频繁访问的字段放在对象开头,利用CPU缓存行(Cache Line)特性,提高字段访问速度。同时,对于一些小型对象,会采用压缩存储方式,减少内存占用,提高内存访问效率 。

在缓存友好设计上,ART会尽量将相关对象存储在相邻的内存区域,使得CPU在访问这些对象时,能够充分利用缓存,减少内存访问延迟。例如,对于同一类的对象实例,会尽量分配在连续的内存空间中 。此外,还会优化垃圾回收过程中的对象移动和内存整理操作,确保在垃圾回收后,对象的内存布局仍然保持缓存友好 。

十、分代式堆内存布局在不同场景下的应用与挑战

10.1 不同类型应用的内存布局需求

不同类型的应用对分代式堆内存布局有着不同的需求。对于游戏类应用,由于其需要频繁创建和销毁大量临时对象(如游戏场景中的特效、粒子效果等对象),因此对年轻代的内存分配和回收效率要求较高。这类应用通常希望年轻代空间能够适当增大,以容纳更多临时对象,减少Minor GC的频率,避免游戏画面出现卡顿 。

对于大型企业级应用,如办公软件、金融应用等,它们往往会创建大量生命周期较长的对象,如数据库连接对象、业务逻辑处理