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++ 写得更严谨罢了~

相关推荐
liulangrenaaa6 小时前
Android NDK 命令规范
android
用户2018792831676 小时前
Android中的StackOverflowError与OOM:一场内存王国的冒险
android
用户2018792831677 小时前
类的回收大冒险:一场Android王国的"断舍离"故事
android
用户2018792831677 小时前
Android Class 回收原理及代码演示
android
前端 贾公子7 小时前
《Vuejs设计与实现》第 18 章(同构渲染)(上)
android·flutter
LiuYaoheng7 小时前
【Android】Android 的三种动画(帧动画、View 动画、属性动画)
android·java
苏苏码不动了7 小时前
Android Studio 虚拟机启动失败/没反应,排查原因。提供一种排查方式。
android·ide·android studio
weixin_456904278 小时前
YOLOv11安卓目标检测App完整开发指南
android·yolo·目标检测
W.Buffer10 小时前
通用:MySQL主库BinaryLog样例解析(ROW格式)
android·mysql·adb