ART 内存模型:用 “手机 APP 小镇” 讲明白底层原理

咱们先把复杂的 ART 虚拟机(Android Runtime)想象成一个 "手机 APP 小镇"

  • 小镇里有很多 "店铺"(就是咱们用的 APP,比如微信、抖音);
  • 每个店铺要干活,得有 "仓库"(存数据)、"快递员"(执行任务)、"规则手册"(存代码逻辑);
  • 还得有 "清洁工"(GC,垃圾回收)定期清理没用的东西,避免小镇挤爆。

ART 的内存模型,本质就是这个小镇的 "资源分配规则"------ 咱们先认识小镇里的 4 个核心角色(内存区域),再通过一个 "外卖 APP 送单" 的故事,看它们怎么配合干活,最后用代码和时序图把逻辑钉死。

一、先认识 ART 内存的 4 个 "核心角色"

ART 的内存不像 "一个大抽屉",而是分成了 4 个功能明确的区域,就像小镇里的 4 个关键场所:

内存区域 小镇角色类比 核心功能 关键特点
堆(Heap) 小镇大仓库 存所有 "对象"(比如订单信息、用户信息) 空间大、共享、需要 GC 清理("过期包裹")
栈(Stack) 快递员的任务清单 存 "方法调用" 和 "临时数据"(比如参数、局部变量) 按 "先进后出" 排队、每个线程 1 个栈、自动清理
方法区(OAT) 规则手册库 存 "类信息"(比如 Order 类的结构)、"编译后的代码" 只读、提前编译(ART 是 AOT 编译,不是临时翻译)
本地内存(Native Memory) 小镇外的临时仓库 给 JNI(Java 调用 C/C++)用的内存 不受 GC 管、需手动释放(容易 "忘收拾")

误区 1:栈里不存对象!栈只存 "对象的引用"(比如仓库包裹的标签)和基本类型(int、boolean),真正的对象都在堆里。

误区 2:ART 没有 "方法区"?错!ART 把类信息和编译后的代码存在OAT 文件(优化过的 DEX 文件)里,加载时通过内存映射到方法区,本质和 JVM 的方法区功能一样,只是实现不同。

二、故事:外卖 APP 的 "送单流程"= ART 内存运作全场景

咱们以 "外卖 APP 计算配送费" 为例,一步一步看每个内存区域怎么协作:

故事背景

用户打开外卖 APP,下单买一杯奶茶,APP 要做 3 件事:

  1. 创建一个 "订单对象"(存收货地址、商品信息);
  2. 调用 "计算配送费" 的方法(根据距离算钱);
  3. 调用手机定位(用 JNI 调用 C/C++ 写的定位模块);
  4. 计算完后,显示结果,没用的临时数据被清理。

step 1:APP 启动 → 方法区加载 "规则手册"

当你点击外卖 APP 图标时,系统会让 ART 做一件事:加载 APP 的类信息到方法区

  • 就像小镇管理员给外卖店铺送 "规则手册":手册里写着 "Order 类怎么创建""怎么算配送费"(即类的字段、方法代码)。
  • ART 的特殊之处:它在 APP 安装时,就把 DEX 文件(Java 编译后的文件)编译成OAT 文件(本地机器码),加载时直接读 OAT,不用像老版 Dalvik 那样 "边跑边翻译"(JIT),所以更快。

对应代码(APP 的核心类):

java 复制代码
// 1. Order类:存订单信息(会被加载到方法区)
class Order {
    String address; // 收货地址(对象字段)
    double goodsPrice; // 商品价格

    // 构造方法:创建Order对象时调用
    public Order(String address, double goodsPrice) {
        this.address = address;
        this.goodsPrice = goodsPrice;
    }
}

// 2. 外卖APP的核心逻辑类(也加载到方法区)
public class TakeawayApp {
    // 本地方法:调用C/C++的定位模块(需加载Native库)
    public native double getDistance(String address); // 算"店铺到地址"的距离

    // 计算配送费的方法
    public double calculateDeliveryFee(Order order) {
        // 步骤A:调用本地方法获取距离(用Native内存)
        double distance = getDistance(order.address);
        
        // 步骤B:按距离算配送费(临时变量存在栈里)
        double fee;
        if (distance <= 3) {
            fee = 5.0; // 3公里内5元
        } else {
            fee = 5 + (distance - 3) * 2; // 超过部分每公里2元
        }
        return fee;
    }

    // APP入口方法
    public static void main(String[] args) {
        // 步骤1:创建Order对象(堆内存分配)
        Order myOrder = new Order("幸福小区1号楼", 18.0);
        
        // 步骤2:调用计算配送费的方法(栈内存压栈)
        TakeawayApp app = new TakeawayApp();
        double finalFee = app.calculateDeliveryFee(myOrder);
        
        // 步骤3:显示结果(无关内存核心,略)
        System.out.println("配送费:" + finalFee);
    }

    // 加载Native库(启动时执行,关联C/C++代码)
    static {
        System.loadLibrary("location"); // 加载liblocation.so库,用Native内存
    }
}

step 2:创建 Order 对象 → 堆内存 "存包裹"

当执行 new Order("幸福小区1号楼", 18.0) 时,ART 会做两件事:

  1. 堆内存分配空间:在堆里划一块地方,存 Order 对象的所有字段(address="幸福小区",goodsPrice=18.0)------ 就像仓库里给这个订单分配一个货架,放好包裹。
  2. 栈内存存引用 :在main方法的 "栈帧" 里,存一个 "引用"(类似包裹的货架编号),变量myOrder其实就是这个编号 ------ 快递员(CPU)通过编号,才能找到堆里的真正订单。

关键:堆里存的是 "实实在在的对象",栈里存的是 "找对象的地址"。就像你手机里存的是 "快递柜编号"(引用),不是 "快递本身"(对象)。

step 3:调用 calculateDeliveryFee → 栈内存 "记任务"

当执行 app.calculateDeliveryFee(myOrder) 时,栈内存会发生 "压栈" 操作:

  • 栈是 "先进后出" 的结构,像快递员的任务清单:先记 "送奶茶",完成后再记 "送水果",水果送完再回头处理奶茶的收尾。

  • 此时,栈会新创建一个 "calculateDeliveryFee 栈帧",里面存 3 样东西:

    1. 方法参数:order(其实是堆里 Order 对象的引用,和myOrder指向同一个货架);
    2. 局部变量:distance(距离)、fee(配送费);
    3. 返回地址:执行完后要回到main方法的哪一行(比如回到System.out.println那行)。

疑问:每个方法调用都会创建栈帧吗?是的!方法执行完,栈帧会自动 "弹栈"(从清单里删掉),局部变量也跟着消失 ------ 不用 GC 管,栈自己会清理。

step 4:调用 getDistance → 本地内存 "借空间"

calculateDeliveryFee里调用了getDistance(Native 方法,C/C++ 写的),这时候会用到本地内存

  • 因为 Java 代码不能直接操作手机硬件(比如 GPS),得靠 C/C++ 写的 "定位模块"(liblocation.so)。
  • ART 会给这个 Native 模块分配一块 "本地内存",用来存定位数据(比如经纬度)------ 这块内存不受 GC 控制,必须在 C/C++ 代码里手动释放(不然会内存泄漏,就像小镇外的垃圾没人清)。

step 5:方法执行完 → 栈弹栈 + GC 清理堆

  1. 栈弹栈calculateDeliveryFee执行完,返回配送费到main方法,它的栈帧会被 "弹栈"(从任务清单里删掉),局部变量distancefee消失。
  2. GC 清理堆 :当 APP 不再用myOrder对象(比如用户退出订单页面),堆里的这个对象就成了 "垃圾"。ART 的 GC(清洁工)会定期扫描堆,找到这些 "没人引用的垃圾",释放它们的内存 ------ 给新的对象腾空间。

GC 的小技巧:ART 用 "分代回收",把堆分成 "年轻代"(新对象)和 "老年代"(长期用的对象)。年轻代垃圾多,清理快;老年代垃圾少,偶尔清理 ------ 就像小镇清洁工先扫刚产生的垃圾,再定期清长期堆积的。

三、代码对应内存操作:逐行拆解

咱们把上面的核心代码再拆一遍,明确每一行对应哪个内存区域的操作:

java 复制代码
public static void main(String[] args) {
    // 1. 堆:分配Order对象,存address和goodsPrice;
    //    栈:在main栈帧里存myOrder引用(指向堆里的对象)
    Order myOrder = new Order("幸福小区1号楼", 18.0);
    
    // 2. 堆:分配TakeawayApp对象;
    //    栈:在main栈帧里存app引用
    TakeawayApp app = new TakeawayApp();
    
    // 3. 栈:压入calculateDeliveryFee栈帧,存order参数(引用);
    //    Native内存:调用getDistance时,分配内存存定位数据;
    //    栈:calculateDeliveryFee栈帧里存distance、fee局部变量
    double finalFee = app.calculateDeliveryFee(myOrder);
    
    // 4. 栈:main栈帧里存finalFee局部变量
    System.out.println("配送费:" + finalFee);
}

// 加载Native库:Native内存分配空间,存liblocation.so的代码和数据
static {
    System.loadLibrary("location");
}

四、时序图:看清楚整个调用流程

下面的时序图,把 "APP 启动→对象创建→方法调用→GC 回收" 的全流程画出来,每个步骤对应哪个角色(内存区域)一目了然:

markdown 复制代码
sequenceDiagram
    participant 用户
    participant APP(Java层)
    participant ART(虚拟机)
    participant Heap(堆)
    participant Stack(栈)
    participant MethodArea(方法区)
    participant NativeMemory(本地内存)
    participant GC(垃圾回收)

    1. 用户->>APP(Java层): 点击打开外卖APP
    2. APP(Java层)->>ART(虚拟机): 请求加载类
    3. ART(虚拟机)->>MethodArea(方法区): 加载Order、TakeawayApp类(从OAT文件)
    4. APP(Java层)->>ART(虚拟机): 执行main方法
    5. ART(虚拟机)->>Stack(栈): 压入main方法栈帧
    6. APP(Java层)->>ART(虚拟机): new Order()
    7. ART(虚拟机)->>Heap(堆): 分配Order对象内存
    8. Heap(堆)-->>ART(虚拟机): 返回对象引用
    9. ART(虚拟机)->>Stack(栈): main栈帧存myOrder引用
    10. APP(Java层)->>ART(虚拟机): 调用calculateDeliveryFee()
    11. ART(虚拟机)->>Stack(栈): 压入calculateDeliveryFee栈帧(存order参数)
    12. APP(Java层)->>ART(虚拟机): 调用native getDistance()
    13. ART(虚拟机)->>NativeMemory(本地内存): 分配内存存定位数据
    14. NativeMemory(本地内存)-->>ART(虚拟机): 返回距离数据
    15. ART(虚拟机)->>Stack(栈): calculateDeliveryFee栈帧存distance、fee
    16. ART(虚拟机)->>Stack(栈): calculateDeliveryFee栈帧弹栈(方法执行完)
    17. ART(虚拟机)->>Stack(栈): main栈帧存finalFee
    18. APP(Java层)->>用户: 显示配送费
    19. 用户->>APP(Java层): 退出订单页面(myOrder不再被引用)
    20. ART(虚拟机)->>GC(垃圾回收): 触发GC扫描
    21. GC(垃圾回收)->>Heap(堆): 回收Order对象内存
    22. Heap(堆)-->>GC(垃圾回收): 释放内存完成

五、总结:ART 内存模型的 3 个核心规律

  1. 各司其职:堆存对象、栈记任务、方法区存规则、本地内存给 JNI------ 每个区域不越界干活;
  2. 栈自动清,堆靠 GC:栈帧随方法执行自动弹栈,堆里的垃圾必须靠 GC 清理;
  3. ART 快在 AOT:安装时编译 OAT 文件,加载类时直接用机器码,比 Dalvik 的 "边跑边翻译" 快得多。

记住这个 "小镇故事",再看 ART 源码时,你就会发现:那些复杂的heap.cc(堆管理)、stack.cc(栈操作)、gc/(垃圾回收)代码,本质都是在实现 "小镇的资源分配规则"------ 只不过用 C++ 写得更严谨罢了~

相关推荐
xiangpanf5 小时前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx8 小时前
安卓线程相关
android
消失的旧时光-19439 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon10 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon10 小时前
VSYNC 信号完整流程2
android
dalancon10 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户693717500138411 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android11 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才12 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶13 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle