JVM vs. DVM 的核心区别
- 基于的架构不同
- JVM基于栈,需要去栈中读写数据,所需的指令更多,导致速度变慢,对于性能有限的移动设备不是很适合
- DVM 基于寄存器, 指令紧凑,更简洁,可以减少指令分派、取指和解释执行的次数,降低CUP开销,因此更适合移动设备有限的CPU和内存环境
JVM 是栈式虚拟机,运算依赖频繁压栈出栈,指令数量多,解释执行时 CPU 取指开销大;DVM 采用虚拟寄存器架构,单条指令可以完成复杂运算,减少指令执行次数,降低 CPU 消耗,更适配早期移动设备有限的算力。
解释器执行每条字节码时,都需要经过取指、解码、分派和执行。取指是读取下一条字节码,分派是根据操作码跳转到对应的处理逻辑,解释执行则是由解释器完成该指令表示的操作。寄存器型字节码通常用更少的指令完成相同任务,因此可以减少解释器循环和指令分派次数,这在以解释执行为主的早期移动设备上有一定性能优势。
- 执行的字节码不同
- JVM 会从
.class文件 或 打包的.jar文件 中获取相应的字节码。
每个.class文件里面包含了该类的常量池、类信息、属性等。当JVM加载.jar文件的时候,会加载里面的所有的.class文件,JVM 的这种方式很慢,对内存有限的移动设备不适合。 - DVM 会从
.dex文件 读取指令和数据。
在.apk文件中只包含了一个.dex文件,这个.dex文件会将所有的.class里面包含的信息全部整合在一起,并且 dex 工具会去除很多.class文件的冗余信息,将 .class 文件整合到 .dex 文件中, 降低内存开销、减少了 I/O 操作,加快了类的查找速度。
- DVM 早期没有使用 JIT 编译器
JVM 使用了 JIT 编译器(Just In Time Compiler,即时编译器),而 DVM 早期没有使用 JIT 编译器。
早期的 DVM 每次执行代码,都需要通过解释器将 dex 代码编译成机器码,然后交给系统处理,效率不是很高。
为了解决该问题,后期,DVM 使用了 JIT 编译器,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器码(Native Code),这样在下次执行到相同逻辑的时候,直接使用编译之后的本地机器码,而不是每次都需要编译。
Dalvik 与 ART 的核心区别
编译方式不同(核心)
- Dalvik:采用JIT(即时编译),每次运行应用时,字节码实时编译为机器码,程序运行效率低。(本质上还是解释执行,边解释边执行)
- ART:默认采用AOT(预编译),安装时一次性将字节码编译为本地机器码,运行流畅、省电,但安装耗时、占用空间大。
- Android 7.0+ 混合模式:ART 加入 JIT 作为补充,安装时不将字节码全编译为机器码,运行时识别编译热点代码为机器码,平衡安装速度、空间和运行性能。
CPU 架构支持
- Dalvik 仅支持32 位CPU;
- ART 支持64 位且兼容 32 位,是 Dalvik 被淘汰的重要原因。
32 位寻址上限 4GB,限制 App 使用大内存;64 位放开逻辑限制,程序才有能力去使用大容量物理内存。
- 垃圾回收(GC)优化
- ART 对 GC 进行了改进,堆内存划分更精细,回收效率更高,减少了应用卡顿。
体现
- 堆划分更精细
Dalvik:整个堆一大块。回收经常扫描全部对象,耗时久。 (按来源划分了两个区域, 系统启动类、静态对象、全局只读共享在一个区域(Zygote space), APP 的所有业务对象在 一个区域(Active 堆)。 然后 整个区域 同时做 标记清除。
一旦堆变大,GC 扫描耗时久,STW 停顿长,碎片严重)
ART 把 Java 堆分成不同区域:新生代、老年代、大对象区。
新生代:存放短命临时对象(循环、局部临时对象),回收快、停顿很短
老年代:长期存活对象(页面数据、全局变量),回收频率低
大对象单独管理,不用扫描一堆小对象
小垃圾快速清理,不用每次都扫描整个内存。- 减少长时间 STW(核心优化)
Dalvik:标记阶段全部暂停。 ART 改成大部分标记工作和 App 线程并发执行(一边跑 App,一边标记垃圾) 只有很短的小段时间需要暂停主线程,GC 停顿时间大幅缩短。- 多线程并行回收
回收的时候启用多个 CPU 线程一起清理垃圾,加快整体 GC 速度。- 回收策略优化
新生代使用复制回收,速度快;
老年代采用并发标记清除;
更智能的触发时机,尽量不在用户滑动、动画这种关键界面操作时执行重 GC。
JIT 是什么?
JIT 出现之前或尚未触发时,JVM 通过解释器逐条读取和分派字节码,执行解释器内部已有的本地处理逻辑;当方法或循环成为热点后,JIT 会把整段字节码编译成本地机器码,后续直接执行机器码,不再逐条解释。
每条字节码都包含一个opcode,有些指令后面还带操作数。根据 操作码 选择对应处理逻辑的过程,就是指令分派(dispatch)。
没有 JIT: 字节码 → 解释器逐条分派 → 本地 handler → CPU 有 JIT: 前期: 字节码 → 解释器 热点后: 字节码 → JIT 编译 → 本地机器码 → CPU 直接执行“解释执行 + JIT 编译”的混合模式
- Java 源代码先通过
javac编译成.class字节码。 - 程序刚启动时,解释器逐条解释字节码并执行,因此启动较快。
- JVM 会统计代码的执行频率。
- 当某个方法或循环成为热点代码后,JIT 编译器会把它编译成本地机器码。
- 后续再次执行热点代码时,直接运行机器码,从而提高性能。