先把主题说透一点:
我们写的 Android App,大部分代码是 Java/Kotlin 写的,
但手机 CPU 只认识“机器指令(0 和 1)”。
中间那层“把 Java 世界变成 CPU 能跑的世界”的,
就是 Android Runtime:Dalvik / ART + 一堆核心类库。
你可以把它想象成:
- 一群会多国语言的翻译 + 管家:
- 能听懂 Java/Kotlin(字节码)
- 能跟底层的 C/C++ 库、Linux 内核沟通
- 还负责内存管理、垃圾回收、线程调度、类加载等等
这篇就围绕几个问题展开:
- Android 为啥需要 Runtime,而不是直接“裸跑”Java?
- Dalvik 和 ART 有啥区别,为啥要从 Dalvik 换成 ART?
- Runtime 在架构里处于哪一层,和 App、Framework、Native 库是什么关系?
- 字节码是怎么跑起来的:解释执行、JIT、AOT 这些词背后都什么含义?
- GC(垃圾回收)、堆、栈、类加载、反射,这些经典概念在 ART 里是什么样?
- 核心类库(Core Libraries)包括啥?跟普通 JVM 有什么差别?
- 这些“底层机制”,对我们写 App 的性能、内存、启动速度有哪些具体影响?
一、把 Runtime 放进大地图:它在 Android 里是“哪一层”?
先简单画一张脑补图(文字版):
从下到上,Android 大致是:
- Linux 内核(内存、进程、线程、文件系统、驱动、网络…)
- Native 层(C/C++ 库:libc、OpenGL、libmedia、SQLite 等 + 各种 Native 服务进程)
- Android Runtime(ART / Dalvik + Core Libraries)
- Framework 层(Java/Kotlin 的系统服务 API:ActivityManager、View、Context 等)
- App 层(你写的那些 APK)
Runtime 处在Java 世界和Native 世界的夹缝位置:
向“上”承接:
- App 里的 Java/Kotlin 代码
- Framework 里的大量 Java 代码(比如 Activity、View、Handler 等)
向“下”接通:
- 各种 C/C++ 库(通过 JNI)
- Linux 提供的线程、内存、文件、Socket 等能力
你可以粗暴理解为:
Runtime = Java 虚拟机(简化版 JVM) + Android 自带的 Java 类库 + 一堆运行时管理组件(GC、线程、异常、类加载…)
没有它,Java/Kotlin 代码压根跑不起来。
二、为什么 Android 要有自己的 Runtime?不能直接用 JVM 吗?
很多人会问:
“Java 不是有 JVM 了吗?Android 为什么不直接把 JVM 搬进来?”
原因大致有几类:
2.1 授权 & 生态问题(历史原因)
早期 Java 的标准 JVM(Oracle 那套)有授权问题:
- 商业使用要考虑版权、授权
- Android 想做一个开放生态,不想被某家公司死死掐住脖子
于是 Google 选择:
- 实现自己的一套 Java 字节码执行环境(Dalvik),
- 使用的是 Java 语言,但运行时不完全等同于标准 JVM。
这也是当年 Google 和 Oracle 打官司的一个背景(不展开)。
2.2 移动设备的特点:内存小、电池有限、CPU 多样
桌面/服务器上的 JVM 没太把“省电、节省内存”当头号问题。
而手机上:
- 内存少(尤其早期 Android 只有几百 MB)
- CPU 弱,还可能是多种架构(ARM、x86、MIPS 等)
- 电池续航是生死线
Runtime 的设计必须:
- 更轻量
- 更适合多进程、多 App 的场景
- 更适应频繁创建/销毁进程、短生命周期程序
2.3 多 App、沙箱、多进程模型
Android 不是只跑一个大程序,而是:
- 每个 App 是一个独立进程、有自己的虚拟机实例
- 有各种安全隔离、权限控制
传统 JVM 往往是:
- 在一个进程里跑一堆 Java 程序(比如 Web 容器里部署多个应用)
两种模式很不一样,Android Runtime 必须针对“一个进程一个虚拟机”的模式做专门优化。
三、Dalvik vs ART:Android Runtime 的两代“灵魂”
Android Runtime 的历史可以简单分两代:
- 第一代:Dalvik VM(早期 Android 到 4.x)
- 第二代:ART(Android Runtime)(从 5.0 开始逐渐替代)
3.1 Dalvik:一开始的“解释 + JIT”方案
Dalvik 的特点:
- 使用的是自己的字节码格式:DEX(Dalvik Executable)
- 设计为“寄存器架构”(register-based),而传统 JVM 是 stack-based
- 早期主要靠解释执行,即一条一条翻译字节码给 CPU 跑
- 后来逐步加入 JIT(Just-In-Time,即时编译),对热点代码做优化
简单类比:
解释执行:
字节码每执行一行,都临时翻译一次给 CPU,边翻译边跑。JIT:
热门代码段,先“记下来”,然后一次性编译成机器码,下次就直接用。
Dalvik 在当年的手机环境下已经算不错了,但随着设备变强、App 越来越复杂,它的几大问题暴露出来:
- 启动速度不够快
- 运行效率不如原生机器码
- GC 卡顿比较明显
于是 Google 搞了第二代 Runtime:ART。
3.2 ART:从“即时翻译”变为“提前编译为主”
ART(Android Runtime)做了一个很重要的改变:
把“安装应用时就编译(AOT, Ahead-of-Time)”作为基础策略,
加上运行时的 JIT 和 Profile 引导优化,
目标就是:
- 启动更快
- 运行更快
- 整体更省电
它的大致思路是:
- 安装 APK 时,ART 把里面的 .dex 字节码编译成机器码(生成 .oat/.odex 等文件),存到本地。
- App 运行时,直接从这些编译好的机器码里调起,不再大量依赖解释执行。
- 运行中还有一套 JIT,它会记录哪些方法/代码块经常被执行,然后针对这些“热点”做进一步优化。
你可以把 Dalvik 和 ART 的差异理解为:
- Dalvik:
- 直接拿原文小说给你边翻译边读,翻译速度快但读起来不流畅。
- ART:
- 先把整部小说翻译成目标语言(安装时做),之后你随时看都是“本地语言版”,读起来流畅多了。
当然,Android 的实际实现比这个复杂得多,比如:
- 存储空间考虑:不能把所有方法都编译成超大代码,否则浪费空间
- 不同设备性能差异:低端设备可能不能放太多编译成果,策略要动态调整
- 升级系统/更新应用时,还要考虑重新编译、兼容性等问题
四、从 APK 到执行:Runtime 眼里的一条完整链路
现在我们来走一遍“App 的 Java/Kotlin 代码是怎么跑起来的”。
4.1 开发期:Java/Kotlin → .class → .dex
你在 Android Studio 里写代码:
classMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)}}编译流程大致是:
- Kotlin/Java 源文件 → 编译成标准的 JVM
.class字节码 - 编译工具再把一堆
.class合并/转换成.dex格式(专门为 Dalvik/ART 设计)
所以 APK 里包含的是:
.dex(一个或多个):所有 Java/Kotlin 代码编译后的字节码- 资源文件、Manifest、native so 库等
4.2 安装期:.dex → 设备上的“可执行/优化格式”
在 Android 5.0 后,安装 APK 时:
- ART 会扫描 .dex 文件
- 根据当前设备的 CPU 架构(arm64, armv7, x86…)
- 把字节码编译/优化成特定格式的文件(比如
.oat/.vdex/.odex等) - 存到
/data/dalvik-cache/或类似目录
这一步叫做“预编译 / 优化”(AOT & Dexopt)。
好处:
- 下次运行 App 时,不需要从头解释执行 .dex
- 冷启动速度明显提升
4.3 运行期:Zygote fork 出新进程,ART 开始干活
当你点开一个 App 图标时,系统会做这样几件事:
通过 Zygote 快速 fork 进程
- Zygote 是一个“预热好了的 Java 虚拟机进程”,里面已经加载了核心类库和大部分 Framework 类
- AMS(ActivityManagerService)让 Zygote fork 出一个子进程当成你的 App 进程
- 子进程里已经有了 ART 运行时环境 + 大量预加载类(通过 COW 共享内存节省空间)
在新进程里加载你的应用代码
- ART 在这个新进程里:
- 设置好 ClassLoader
- 加载你 APK 里的 .dex/.oat 文件
- 初始化 Application 类、执行
attachBaseContext()、onCreate()等
- ART 在这个新进程里:
后续所有 Java/Kotlin 代码都跑在这个 ART 进程里
- 当你调用框架 API,如
new Thread {}、ArrayList.add()、View.draw(), - 背后都是 ART 帮你执行指令、分配内存、处理异常/GC 等。
- 当你调用框架 API,如
所以:
每个 App 进程里,都有一个 ART 运行时实例,
它就是你的 Java/Kotlin 世界的“宿主环境”。
五、解释执行 vs JIT vs AOT:三种“翻译模式”的区别
这是理解 Runtime 性能的关键。
5.1 解释执行:一步一翻译,执行速度慢但灵活
解释执行的过程类似于:
有一本外文书(字节码),
你不预先翻译,而是读一行翻一行,读一句翻一句。
所以你开始读很快(不需要准备),但整体读得不顺畅。
在 Dalvik 早期,这基本是主要方式:
- 每条字节码都由解释器模块看一眼,然后根据指令含义执行对应 C 函数、汇编片段
- 执行慢,但实现简单、占空间少
5.2 JIT(Just-In-Time,即时编译):对“热门段落”提前翻译
JIT 的思路:
你读书的时候发现某几个片段反复要看,那就干脆单独翻译好,之后再看就不用一遍遍现场翻译了。
在运行时:
- ART 统计哪些方法/代码段被频繁调用(热点),例如一个循环里的逻辑
- 当某个方法达到一定“热度阈值”,JIT 引擎就会把这一段字节码编译成机器码保存起来
- 下次再执行时,直接跳到编译好的机器码,速度大幅提升
优点:
- 不用事先把所有代码都编译,只对“热点”优化
- 节省空间
- 能根据运行时实际情况优化(profile-guided)
5.3 AOT(Ahead-Of-Time,提前编译):安装时就大块翻译
AOT 的思路:
在你拿到外文书时,就让翻译提前把书翻成目标语言一遍,
之后你每次看都是本地语言版,读起来很快。
在 ART 的 AOT 模式下:
- 安装 APK 时,就把绝大部分(或全部)字节码编译成本地机器码
- 生成
.oat/.odex等文件 - 运行时直接调用机器码,解释执行大幅减少
优点:
- 冷启动速度更快(初次执行不用 JIT 热身)
- 运行效率更接近 C/C++ 的原生性能
- 减轻运行时的 CPU 负担(对电池更友好)
缺点:
- 安装时间变长
- 生成的机器码占用磁盘空间
- 不同 CPU 架构要分别编译
现实中,Android 采用的是AOT + JIT + Profile 混合策略:
- 初次安装时进行基础 AOT 编译
- 运行中用 JIT 统计 profile(哪些代码经常跑)
- 后台空闲或充电时,根据 profile 再做二次优化编译(profile-guided AOT)
六、GC(垃圾回收):ART 如何帮你“打扫内存卫生”
Java/Kotlin 世界的一个大卖点是:不用手动 free 内存,Runtime 会帮你做垃圾回收。
6.1 堆、栈、对象、引用:先建立直觉
在 ART 里,大致是这样:
栈(Stack):
- 存放方法调用栈帧、局部变量、方法参数
- 每个线程一个栈
- 方法结束后栈帧弹出,里面的局部变量自然消亡
堆(Heap):
new出来的对象都在这里:new String()、new ArrayList()等- 多个线程共享
- 对象什么时候被回收,不由你直接决定,而由垃圾回收算法判断
引用关系:
- 只要还有某个“活动的”引用指向一个对象(可达),这个对象就不能被回收
- 当一个对象不再能从任何“根对象”(线程栈、本地变量、静态变量等)触达时,它就成了垃圾
GC 的任务就是:
定期扫描堆,找出那些“再也用不到的对象”,把它们的内存回收掉。
6.2 ART 的 GC 策略:高效 + 尽量少卡顿
ART 使用了一系列 GC 算法组合,目标是在:
- 不太浪费内存
- 让应用尽量不卡顿(尤其是 UI 线程)
- 支持多核并行、增量回收
你不用死记算法名字(Mark-Sweep、Mark-Compact、Concurrent GC、Generational GC 等),
了解几个关键点就够了:
分代回收
- 年轻代 vs 老年代:
- 新创建的对象放在“年轻代”区域,
- 存活多次 GC 后才会晋升到“老年代”。
- 因为绝大多数对象很快就不用了,把精力放在年轻代能更高效地回收垃圾。
- 年轻代 vs 老年代:
并发标记 / 并发清理
- GC 尽量在后台线程执行,减少停顿时间
- 通过写屏障等机制,保证在标记过程中即便应用还在改引用关系,GC 也能正确判断
Stop-The-World(STW)时间控制
- 传统 GC 会有一段时间暂停所有应用线程(STW),
- ART 努力把这段时间压缩到很短,降低卡顿感。
对我们来说,有几个结论很重要:
- 你创建的每个对象都会增加 GC 压力。
- 大量短生命的临时对象、频繁的分配与释放,会引发频繁 GC。
- 当 GC 发生在主线程敏感时刻(比如滑动列表),就会出现丢帧、卡顿。
这就是为什么:
- 避免在
onDraw()、onBindViewHolder()这样的热点函数里疯狂 new 对象 - 避免创建过多临时字符串、盒装类型(如频繁装箱 Integer)
- 尽量复用对象(比如 ViewHolder 模式)
这些都本质上是在“帮 ART 少干点 GC 的活”。
七、类加载 / 反射 / 多 Dex:Runtime 怎么管理你的代码世界?
Java 世界里有一个经典概念:ClassLoader(类加载器)。
在 Android 中,这也是 Runtime 的一项重要工作。
7.1 ClassLoader:不同 APK 有自己的一套“类空间”
每个 App 进程里,ART 会创建一系列 ClassLoader:
- 最底下:
- BootClassLoader:加载核心类库(java.lang、java.util、android.* 等)
- 上面:
- PathClassLoader:加载应用自身的 classes.dex
- 再上面可能还有:
- DexClassLoader:你动态加载的 dex / jar
当你在代码中:
Class.forName("com.example.Foo");ClassLoader 就会负责从对应的 dex 文件里找到这个类的定义,
如果没找到就抛ClassNotFoundException。
好处:
- 不同 App 的类互不干扰,即使有同名类也不会冲突
- 系统可以控制哪些类对谁可见
- 支持插件化(通过自定义 ClassLoader 动态加载新代码)
7.2 反射:Runtime 提供的“动态自省”能力
反射(Class,Method,Field等)也是 Runtime 的职责之一:
- 管理类的元信息(字段、方法、注解等)
- 支持运行时查找方法并调用
反射很强大,但也有成本:
- 反射调用一般慢于直接方法调用
- 需要额外保存元数据,占空间
- 大量使用反射,还会加重 GC 压力
ART 对反射做了不少优化,但总体原则不变:
- 反射用在框架和工具库上可以提高灵活性
- 业务逻辑里大量反射容易伤性能
7.3 MultiDex:方法数多到爆,Runtime 也得适配
由于 Dalvik/ART 指令集限制,单个 dex 文件方法数有 65535 限制。
大型 App(比如超级 App)很容易超过这个数,
于是就有了MultiDex:
- 把字节码拆成多个 dex 文件:classes.dex、classes2.dex、classes3.dex…
- 启动时把多个 dex 文件都挂到 ClassLoader 的搜索路径里
Runtime 需要:
- 支持多个 dex 文件同时被加载
- 管理好它们之间的类索引和方法索引
MultiDex 会:
- 增加启动成本(需要加载更多 dex 文件)
- 影响内存使用
所以很多优化手段(比如代码分包、按需加载、R8/ProGuard 压缩)本质上也是在帮 Runtime 更轻松。
八、Core Libraries:Android 提供给 Java 世界的“标准工具箱”
Android Runtime 不只是一个“执行引擎”,还自带一整套 Java 类库。
这些就是所谓的Core Libraries(核心类库)。
8.1 包括哪些?
大致可以分几类:
Java 标准库的一部分:
java.lang.*:- String、Object、Thread、Throwable、Class、Math…
java.util.*:- List、Map、Set、HashMap、ArrayList、Collections、Date、Calendar…
java.io.*:- InputStream、OutputStream、File、Reader、Writer…
java.net.*:- URL、Socket、Http 等(早期)
注意:Android 并不是完全实现所有标准 Java SE 的类库,而是选了一部分适合移动场景的子集。
Android 专有库:
android.os.*:- Binder、Handler、Looper、Message、Parcel…
android.util.*:- Log、SparseArray 等
android.graphics.*:- Canvas、Bitmap、Paint…
android.view.*:- View、ViewGroup、MotionEvent…
android.content.*:- Intent、Context、SharedPreferences…
这些实际上属于 Framework 层的 Java API,但底层依托 Runtime 执行。
第三方兼容库(部分)
- 比如部分 Apache Harmony 的实现(历史原因)
- 后来引入了很多开源实现(如 OkHttp 做网络库的基础)
8.2 Core Libraries 与 ART 的关系
ART 负责:
- 加载这些类(通过 BootClassLoader)
- 存放它们的元数据、常量池
- 提供执行环境(线程、栈、堆)
Core Libraries 就是我们日常写代码时用到的各种类,
而 Runtime 就是支撑这些类“活起来”的地基。
九、这些 Runtime 机制,具体会影响我们哪些开发体验?
说了这么多机制,你我最关心的还是:那对我写 App 有啥用?
我列几条很直接的影响点:
9.1 冷启动速度:AOT 编译和 Zygote 预加载
ART 通过安装时编译 + Zygote 预加载,
大幅提升了 App 冷启动速度。对开发者来说:
- 大量类、方法、字段,会影响 dex 大小和编译时间
- 启动时大量初始化对象、静态字段,会增加 Runtime 压力,影响冷启动
结论:
- 启动流程要精简:
- 不要在 Application.onCreate() 里干太多活
- 不要一次性初始化所有 SDK
9.2 内存与 GC 卡顿:对象分配模式要合理
- ART 的 GC 虽然更先进,但卡顿问题永远不会“自动消失”
- 高频 new 对象、过多临时对象、长生命周期大对象,都会给 Runtime 压力
建议:
- 避免在 onDraw/onLayout/onBindViewHolder/热点循环里 new 大对象
- 重用对象池(比方说 RecyclerView 的 ViewHolder、Bitmap 缓存等)
- 使用合适的数据结构(比如 SparseArray 替代 HashMap<Integer,?>)
9.3 反射、动态代理、大量 Kotlin 特性:Runtime 也会累
- 动态反射调用(如 Gson、各种 ORM、依赖注入框架大量使用反射)
- Lambda、大量 inline 函数、高阶函数(Kotlin)
- 这些都可能增加方法数、类数、元数据量,使 Runtime 的工作变重
办法:
- 适当使用编译期代码生成(如 Dagger、kapt)减少运行时反射
- 对性能敏感逻辑,避免过度函数式、过度抽象
- 用 Profiler 和 Systrace 看清楚热点在哪
9.4 MultiDex:方法数爆炸对 Runtime 的影响
- 方法数过多会触发 MultiDex,启动时需要加载多个 dex 文件
- Runtime 在类加载、方法查找上工作量更大,可能影响启动和内存
解决办法:
- 使用 R8/ProGuard 做混淆、压缩,去掉没用代码
- 模块化拆分,避免一个主 dex 太大
- 对大体量 App 做专项启动优化
十、最后用一段话,把 Android Runtime 这块“黑盒”打开
把上面所有内容浓缩成一个生活化比喻:
整个 Android 世界里,
- Linux 内核 是土地和水电
- C/C++ Native 库 是各种机器设备(锅炉、空调、网络交换机)
- Framework API 是政府制定的规章制度和办事窗口
- App 是各种住户和公司
那 **Android Runtime(ART / Dalvik + Core Libraries)**呢?
它是:
- 一套专门给“说 Java/Kotlin 语言的人”准备的生活区;
- 里面有翻译官(解释器、JIT、AOT),把 Java 字节码变成 CPU 指令;
- 有垃圾清理工(GC),定期清理没人用的内存垃圾;
- 有档案管理员(ClassLoader/反射),整理和查找所有的类和方法;
- 有工具库仓库(Core Libraries),提供日常要用的各种工具(集合、IO、网络、线程等)。
你写的每一行 Java/Kotlin 代码,
能在哪个进程里活起来、什么时候被执行、什么时候被回收、
怎么跟底层 native 库说话,
都是通过这套 Runtime 来安排的。
理解 ART / Dalvik + 核心类库,并不是为了你以后自己去写个虚拟机,
而是当你面对“启动慢、卡顿、内存抖动、方法数超限、反射过多”等问题时,
脑子里有一张“这背后到底是谁在干活”的清晰地图,
能在正确的层级去优化和定位问题。
这才是理解 Android Runtime 的真正意义。