咱们先把复杂的 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 件事:
- 创建一个 "订单对象"(存收货地址、商品信息);
- 调用 "计算配送费" 的方法(根据距离算钱);
- 调用手机定位(用 JNI 调用 C/C++ 写的定位模块);
- 计算完后,显示结果,没用的临时数据被清理。
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 会做两件事:
- 堆内存分配空间:在堆里划一块地方,存 Order 对象的所有字段(address="幸福小区",goodsPrice=18.0)------ 就像仓库里给这个订单分配一个货架,放好包裹。
- 栈内存存引用 :在
main
方法的 "栈帧" 里,存一个 "引用"(类似包裹的货架编号),变量myOrder
其实就是这个编号 ------ 快递员(CPU)通过编号,才能找到堆里的真正订单。
关键:堆里存的是 "实实在在的对象",栈里存的是 "找对象的地址"。就像你手机里存的是 "快递柜编号"(引用),不是 "快递本身"(对象)。
step 3:调用 calculateDeliveryFee → 栈内存 "记任务"
当执行 app.calculateDeliveryFee(myOrder)
时,栈内存会发生 "压栈" 操作:
-
栈是 "先进后出" 的结构,像快递员的任务清单:先记 "送奶茶",完成后再记 "送水果",水果送完再回头处理奶茶的收尾。
-
此时,栈会新创建一个 "calculateDeliveryFee 栈帧",里面存 3 样东西:
- 方法参数:
order
(其实是堆里 Order 对象的引用,和myOrder
指向同一个货架); - 局部变量:
distance
(距离)、fee
(配送费); - 返回地址:执行完后要回到
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 清理堆
- 栈弹栈 :
calculateDeliveryFee
执行完,返回配送费到main
方法,它的栈帧会被 "弹栈"(从任务清单里删掉),局部变量distance
、fee
消失。 - 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 个核心规律
- 各司其职:堆存对象、栈记任务、方法区存规则、本地内存给 JNI------ 每个区域不越界干活;
- 栈自动清,堆靠 GC:栈帧随方法执行自动弹栈,堆里的垃圾必须靠 GC 清理;
- ART 快在 AOT:安装时编译 OAT 文件,加载类时直接用机器码,比 Dalvik 的 "边跑边翻译" 快得多。
记住这个 "小镇故事",再看 ART 源码时,你就会发现:那些复杂的heap.cc
(堆管理)、stack.cc
(栈操作)、gc/
(垃圾回收)代码,本质都是在实现 "小镇的资源分配规则"------ 只不过用 C++ 写得更严谨罢了~