Dalvik 虚拟机(Dalvik Virtual Machine),简称 DVM。它并没有遵循 JVM 规范来实现,因此 DVM 并不是一个 Java 虚拟机。
【JVM 基于栈】
【DVM 基于寄存器】
【指令对比】
int x = 4; int y = 2;
计算 (x + y) * (x - y)
:
// JVM 字节码指令(基于栈)
0: iload_1 // 从局部变量1号槽位读取变量x的值到操作数栈 [4
1: iload_2 // 从局部变量2号槽位读取变量y的值到操作数栈 [4,2
2: iadd // 栈顶两个元素相加并保存到操作数栈(x+y) [6
3: iload_1 // 从局部变量1号槽位读取变量x的值到操作数栈 [6,4
4: iload_2 // 从局部变量2号槽位读取变量y的值到操作数栈 [6,4,2
5: isub // 栈顶两个元素相减并保存到操作数栈(x-y) [6,2
6: imul // 栈顶两个元素相乘并保存到操作数栈 [12
// DVM 字节码指令(基于寄存器)
0000: add-int v0, v3, v4 // 将v3和v4寄存器的值相加并保存到v0寄存器(x+y)
0002: sub-int v1, v3, v4 // 将v3和v4寄存器的值相减并保存到v1寄存器(x-y)
0004: mul-int/2addr v0, v1 // 将v0和v1寄存器的值相乘并保存到v0寄存器
【JVM 执行 class 字节码】
【DVM 执行 dex 字节码】
COW(copy-on-write)即写时复制。
Android 系统启动后,第一个进程 Zygote 会创建第一个 Dalvik 虚拟机,维护了一个 Zygote 堆。第一个应用程序进程创建(从 Zygote 进程 fork)时,会使用 COW 策略,创建 Active 堆并把 Zygote 堆中的内容复制进去。
Zygote 堆中不会触发 GC,Active 堆使用并发标记清除(Concurrent-Mark-Sweep)算法进行 GC。
【Mark 阶段】
通过递归,从 GC Roots 开始标记被引用的对象。为了避免 Stop-The-World,采用 GC 线程和其他线程并发执行,分为两步:
【Sweep 阶段】
【缺点】
在 DVM 中每次垃圾收集都会将 GC 日志打印到 logcat 中,具体格式为:
GC_Reason
指的是引起 GC 的原因,有以下几种。
GC_CONCURRENT
:当堆开始填充时,并发 GC 可以释放内存。GC_FOR_MALLOC
:当堆内存已满时, App尝试分配内存而引起的 GC,系统必须停止 App 并回收内存。GC_HPROF_DUMP_HEAP
:当你请求创建 HPROF 文件来分析堆内存时出现的 GC。GC_EXPLICIT
:显式的 GC,例如调用 System.gc()
(应该避免调用显式的 GC,信任 GC 会在需要时运行)。GC_EXTERNAL_ALLOC
:仅适用于 API 级别小于等于 10,且用于外部分配内存的 GC。Amount_freed
:本次 GC 释放内存的大小。
Heap_stats
:堆的空闲内存百分比(已用内存 / 堆总内存)。
External_memory_stats
:API 小于等于级别 10 的内存分配(已分配的内存 / 引起 GC 的阈值)。
Pause_time
:暂停时间,堆越大暂停时间越长。并发暂停时间会显示两个,一个是垃圾收集开始时, 另一个是垃圾收集快要完成时。
D/dalvikvm: GC_CONCURRENT freed 2012K, 63% free 3213K/9291K, external 4501K/5161K, paused 2ms+2ms
本次 GC 原因是 GC_CONCURRENT;释放内存 2012KB;堆空闲内存占比 63%,已用3213KB,总内存为 9291KB;暂停总时长 4ms。
ART(Android Runtime)虚拟机于 Android 4.4 发布,Android 5.0 中默认使用,用来代替 Dalvik 虚拟机。
【DVM 基于 JIT 运行】
运行程序时使用解释器执行,同时将热点代码通过 JIT 编译器编译成机器码,并缓存到 jit code cache
,再执行时就无需解释直接运行。
【ART 基于 AOT 运行】
.oat
文件,并存储到磁盘,程序运行时不需要编译直接使用。jit code cache
,再执行时就无需解释直接运行。.oat
文件,并存储到磁盘待下次运行时直接使用。实现 ART 即时 (JIT) 编译器
Java 为了实现“一次编写,随处运行”,将 java 代码编译成与本地平台无关的字节码文件,让字节码文件在不同平台、不同系统的不同虚拟机上运行,从而实现跨平台。而虚拟机运行字节码文件有多种不同方式:解释器
、JIT
和 AOT
。
jit code cache
,再次执行相同方法时就无需解释器编译,而直接运行,性能一般。.oat
文件并存储到磁盘空间,程序运行时不需要编译直接使用,性能好。⭐️ **注意:**尽管 JIT 和 AOT 使用相同的编译器,且进行的一系列优化也较相似,但它们生成的代码可能会有所不同。JIT 会利用运行时类型信息,可以更高效地进行内联,并可让堆栈替换 (OSR) 编译成为可能,而这一切都会使其生成的代码略有不同。
.oat
文件,ART 虚拟机直接运行。.dex
文件,ART 虚拟机先判断是否热点代码:
在 ART 中 ,执行一个方法前,可以在 ArtMethod
结构体中判断改方法是否已经被编译过了,从而使用不同的策略来执行方法。
当设备空闲且在充电时,AOT 编译守护程序(dex2oat
)将解析 JIT 配置文件来编译热点方法。
.oat
文件。.oat
文件。不同版本,垃圾回收器不同,运行时堆也不同。而 ART 同时包含多种方案,OEM 厂商可以更改 GC 类型。
默认采用并发标记清除(CMS)方案。
支持内存压缩,但是有条件,进行的次数不多,可能会产生内存碎片。
默认采用并发复制(CC)方案。
默认采用并发复制(CC)方案,但是增加了分代处理。
ART 会在主动请求 GC 时或认为 GC 速度慢(暂停超过5ms或者持续超过100ms,且暂停可以被察觉)时才会打印 GC 日志,具体格式为:
GC_Reason
指的是引起 GC 的原因,有以下几种。
Concurrent
:并发 GC,不会使 App 的线程暂停,该 GC 在后台线程运行,不会阻止内存分配。Alloc
:当堆内存已满时,App 尝试分配内存引起的 GC,这个 GC 会发生在正在分配内存的线程中。Explicit
:App显式的请求垃圾回收,例如调用 System.gc()
。NativeAlloc
:Native 内存分配时,触发的 GC。CollectorTransition
:由堆转换引起的回收,运行时切换 GC 引起的。将所有对象从空闲列表空间复制到碰撞指针空间,反之亦然。仅出现在内存较小的设备上App将进程从可察觉的暂停状态更改为可察觉的非暂停状态。HomogeneousSpaceCompact
:齐性空间压缩是指空闲列表到压缩的空闲列表空间,通常发生在App移动到可察觉的暂停进程状态。以此来减小内存使用并对堆内存进行碎片整理。DisableMovingGc
:不是真正触发 GC 的原因。发生并发堆压缩时,由于使用了GetPrimitiveArrayCritical,收集会被阻塞。HeapTrim
:不是触发 GC 的原因。收集会一直被阻塞,直到堆内存整理完毕。GC_Name
指的是垃圾收集器名称,有以下几种。
Concurrent Mark Sweep
:CMS 收集器,采用标记清除算法实现,收集暂停时间短。完整的堆垃圾收集器,能释放除了 Image Space 外的所有空间。Concurrent Partial Mark Sweep
:局部收集器,能释放除了 Image Space 和 Zygote Space 外的所有空间。Concurrent Sticky Mark Sweep
:粘性收集器,基于分代的垃圾收集思想,只能释放自上次 GC 以来分配的对象。比完整或局部垃圾收集器扫描更频繁、更快且暂停时间更短。Marksweep + Semispace
:非并发的 GC,复制 GC 用于堆转换以及碎片整理。Objects_freed
:从非 Large Object Space 中回收的对象的数量。Size_freed
:从非 Large Object Space 中回收的字节数。Large_objects_freed
:从 Large O同ect Space 中回收的对象的数量。Large_object_size_freed
:从 Large Object Space 中回收的字节数。Heap_stats
:堆的空闲内存百分比,即(已用内存 / 堆的总内存)。Pause_times
:暂停时间,暂停时间与在 GC 运行时修改的对象引用的数量成比例。目前, ART 的 CMS 收集器仅有一次暂停,它出现在 GC 的结尾附近。移动的垃圾收集器暂停时间会很长,会在大部分垃圾回收期间持续出现 。