内存管理概览:ART堆结构、Region空间与TLAB、内存分配流程

内存管理,说白了就是ART的"后勤部"。Java程序跑得再欢,对象总得有个地方放吧?GC再厉害,也得知道哪些内存能回收。今天我们就来聊聊ART堆的骨架——堆结构、Region空间、TLAB,以及对象是怎么一步步分配进来的。

一、ART堆的整体结构

ART的堆不像JVM那样分新生代、老年代。它更简洁,但也更灵活。我个人习惯把ART堆看成三块区域:

  • Zygote空间:系统启动时预加载的类、资源都在这。这块区域几乎不动,所有子进程共享。
  • 分配空间(Allocation Space):应用自己创建的对象,大部分都落在这里。
  • 大对象空间(Large Object Space):超过一定阈值的大数组,比如Bitmap的像素数据,直接扔到这里。

嗯,这里要注意:Zygote空间是只读的,GC不会去动它。你想想看,如果GC去扫描Zygote空间,那所有进程都得跟着遭殃。

核心要点:ART堆的地址空间是连续的,但逻辑上分为多个Region。每个Region有自己的用途和GC策略。

二、Region空间——ART的"格子间"

从Android 8.0开始,ART引入了Region-based堆管理。说白了,就是把堆切成一个个固定大小的Region(默认256KB)。每个Region都有自己的状态:

Region状态 说明
kRegionStateAllocated 已分配,正在使用
kRegionStateFree 空闲,可回收
kRegionStateLargeObject 用于大对象
kRegionStateReclaim 标记为可回收

为什么用Region?我在项目中遇到过一个问题:老版本ART做并发标记时,需要遍历整个堆,卡顿很明显。Region化之后,GC可以按Region粒度来操作,局部性更好,停顿时间也降下来了。

小技巧:调试时可以打印Region信息,看看哪些Region被频繁分配,哪些一直空闲。这对优化内存分配很有帮助。

三、TLAB——线程的"私房钱"

TLAB(Thread Local Allocation Buffer)这个概念,搞过JVM的应该不陌生。ART里也有,而且实现得更轻量。

每个线程在堆里预申请一小块内存(默认256KB),作为自己的TLAB。线程创建对象时,优先从TLAB里分配。TLAB用完了,再去堆里申请新的。

为什么要这么做?你想想看,如果所有线程都去抢堆的全局锁,那并发一高,分配效率就崩了。TLAB让每个线程有自己的"私房钱",互不干扰。

// ART中TLAB分配的简化逻辑
void* TLAB::Alloc(size_t size) {
  if (free_end_ - free_begin_ >= size) {
    void* ptr = free_begin_;
    free_begin_ += size;
    return ptr;
  }
  // TLAB空间不足,重新申请
  RefillTLAB(size);
  return Alloc(size);
}

我曾经调试过一个性能问题:某个线程频繁创建小对象,TLAB很快就用完了,导致频繁触发全局分配。后来把TLAB大小调大到512KB,性能提升很明显。

注意:TLAB不是越大越好。太大了会浪费内存,太小了又频繁触发全局分配。一般256KB到512KB是比较合理的范围。

四、内存分配流程——一个对象的"出生记"

一个Java对象从new到真正落地,经历了哪些步骤?我画了一张图,你看完就明白了。

ART内存分配流程图 1. 计算对象大小 2. 检查TLAB是否有空间 TLAB不足 TLAB足够 3a. TLAB分配 3b. 全局分配 4. 分配成功,返回对象指针 5. 对象初始化(header等)

流程其实不复杂:

  1. 计算对象大小:包括对象头、实例字段、对齐填充。ART的对象头是8字节(32位)或12字节(64位)。
  2. 检查TLAB:如果当前线程的TLAB还有空间,直接分配。这是最快路径。
  3. TLAB不足时:走全局分配路径。ART会尝试从堆的Free Region中拿一块,或者触发一次小GC。
  4. 分配成功:返回对象指针,设置对象头信息。
  5. 对象初始化:调用构造函数,设置字段默认值。

我记得有一次线上OOM,排查下来发现是某个线程的TLAB设置太小,频繁触发全局分配,导致GC压力剧增。调大TLAB后,问题就解决了。

关键点:TLAB分配是ART中最快的分配路径,没有之一。尽量让对象分配走TLAB,能大幅提升性能。

五、避坑指南

做内存优化这么多年,我踩过不少坑。分享几个常见的:

  • 大对象不要走TLAB:超过TLAB剩余空间的对象,会直接走全局分配。如果你频繁创建大对象,建议考虑对象池。
  • TLAB大小要合理:太小了频繁触发全局分配,太大了浪费内存。我一般建议256KB起步,根据线程分配频率动态调整。
  • 注意Zygote空间:Zygote空间是只读的,不要试图修改它。否则你会看到奇怪的crash。

调试技巧:用adb shell dumpsys meminfo可以查看堆的Region分布。重点关注Allocated和Free的比例,如果Free太少,说明内存压力大。

好了,这一章就到这里。内存管理是ART的基石,理解堆结构、Region和TLAB,后面讲GC和并发标记时你会轻松很多。


公众号:蓝海资料掘金营,微信deep3321