Android Runtime (ART) 和 Dalvik

Android Runtime (ART) 是 Android 上的应用和部分系统服务使用的托管式运行时。ART 及其前身 Dalvik 最初是专为 Android 项目打造的。作为运行时的 ART 可执行 Dalvik 可执行文件并遵循 Dex 字节码规范。

ART 和 Dalvik 是运行 Dex 字节码的兼容运行时,因此针对 Dalvik 开发的应用也能在 ART 环境中运作。不过,Dalvik 采用的一些技术并不适用于 ART。有关最重要问题的信息,请参阅在 Android Runtime (ART) 上验证应用行为

参考:https://source.android.com/devices/tech/dalvik?hl=zh-cn

ART 功能

以下是 ART 实现的一些主要功能。

预先 (AOT) 编译

ART 引入了预先编译机制,可提高应用的性能。ART 还具有比 Dalvik 更严格的安装时验证。

在安装时,ART 使用设备自带的 dex2oat 工具来编译应用。此实用工具接受 DEX 文件作为输入,并为目标设备生成经过编译的应用可执行文件。该工具应能够顺利编译所有有效的 DEX 文件。但是,一些后处理工具会生成无效文件,Dalvik 可以接受这些文件,但 ART 无法编译这些文件。如需了解详情,请参阅处理垃圾回收问题

垃圾回收方面的优化

垃圾回收 (GC) 会耗费大量资源,这可能有损于应用性能,导致显示不稳定、界面响应速度缓慢以及其他问题。ART 通过以下几种方式对垃圾回收做了优化:

大多采用并发设计,具有一次 GC 暂停并发复制,可减少后台内存使用和碎片GC 暂停的时间不受堆大小影响在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见

开发和调试方面的优化

ART 提供了大量功能来优化应用开发和调试。

支持采样分析器

一直以来,开发者都使用 Traceview 工具(用于跟踪应用执行情况)作为分析器。虽然 Traceview 可提供有用的信息,但每次方法调用产生的开销会导致 Dalvik 分析结果出现偏差,而且使用该工具明显会影响运行时性能。

ART 添加了对没有这些限制的专用采样分析器的支持,因而可更准确地了解应用执行情况,而不会明显减慢速度。KitKat 版本为 Dalvik 的 Traceview 添加了采样支持。

支持更多调试功能

ART 支持许多新的调试选项,特别是与监控和垃圾回收相关的功能。例如,您可以:

查看堆栈跟踪中保留了哪些锁,然后跳转到持有锁的线程。询问指定类的当前活动的实例数、请求查看实例,以及查看使对象保持有效状态的参考。过滤特定实例的事件(如断点)。查看方法退出(使用“method-exit”事件)时返回的值。设置字段观察点,以在访问和/或修改特定字段时暂停程序执行。

优化了异常和崩溃报告中的诊断详细信息

当发生运行时异常时,ART 会为您提供尽可能多的上下文和详细信息。ART 会提供 java.lang.ClassCastExceptionjava.lang.ClassNotFoundExceptionjava.lang.NullPointerException 的更多异常详细信息。(较高版本的 Dalvik 会提供 java.lang.ArrayIndexOutOfBoundsExceptionjava.lang.ArrayStoreException 的更多异常详细信息,这些信息现在包括数组大小和越界偏移量;ART 也提供这类信息。)

例如,java.lang.NullPointerException 现在会显示有关应用尝试处理 null 指针时所执行操作的信息,例如应用尝试写入的字段或尝试调用的方法。一些典型示例如下:

1
2
3
4
5
java.lang.NullPointerException: Attempt to write to field 'int
android.accessibilityservice.AccessibilityServiceInfo.flags' on a null object
reference
java.lang.NullPointerException: Attempt to invoke virtual method
'java.lang.String java.lang.Object.toString()' on a null object reference

ART 还通过纳入 Java 和原生堆栈信息,在应用原生代码崩溃报告中提供更实用的上下文信息。

Android 8.0 中的 ART 功能改进

在 Android 8.0 版本中,Android Runtime (ART) 有了极大改进。下面的列表总结了设备制造商可以在 ART 中获得的增强功能。

并发压缩式垃圾回收器

正如 Google 在 Google I/O 大会上所宣布的那样,ART 在 Android 8.0 中提供了新的并发压缩式垃圾回收器 (GC)。该回收器会在每次执行 GC 时以及应用正在运行时对堆进行压缩,且仅在处理线程根时短暂停顿一次。该回收器具有以下优势:

GC 始终会对堆进行压缩:堆的大小平均比 Android 7.0 中的小 32%。得益于压缩,系统现可实现线程局部碰撞指针对象分配:分配速度比 Android 7.0 中的快 70%。H2 基准的停顿次数比 Android 7.0 GC 的少 85%。停顿次数不再随堆的大小而变化,应用在使用较大的堆时也无需担心造成卡顿。GC 实现细节 - 读取屏障:读取屏障是在读取每个对象字段时所做的少量工作。它们在编译器中经过了优化,但可能会减慢某些用例的速度。

循环优化

在 Android 8.0 版本中,ART 采取了多种循环优化措施,具体如下:

消除边界检查静态:在编译时证明范围位于边界内动态:运行时测试确保循环始终位于边界内(否则不进行优化)消除归纳变量移除无用归纳用封闭式表达式替换仅在循环后使用的归纳消除循环主体内的无用代码,移除整个死循环强度降低循环转换:逆转、交换、拆分、展开、单模等SIMDization(也称为矢量化)

循环优化器位于 ART 编译器中一个独立的优化环节中。大多数循环优化与其他方面的优化和简化类似。采用比平时更复杂的方式进行一些重写 CFG 的优化时会面临挑战,因为大多数 CFG 实用工具(请参阅 nodes.h)都侧重于构建而不是重写 CFG。

类层次结构分析

在 Android 8.0 中,ART 会使用类层次结构分析 (CHA),这是一种编译器优化,可根据对类层次结构的分析结果,将虚拟调用去虚拟化为直接调用。虚拟调用代价高昂,因为它们围绕 vtable 查找来实现,且会占用几个依赖负载。另外,虚拟调用也不能内嵌。

以下是对相关增强功能的总结:

动态单一实现方法状态更新 - 在类关联时间结束时,如果 vtable 已被填充,ART 会按条目对超类的 vtable 进行比较。编译器优化 - 编译器会利用某种方法的单一实现信息。如果方法 A.foo 设置了单一实现标记,则编译器会将虚拟调用去虚拟化为直接调用,并借此进一步尝试内嵌直接调用。已编译代码无效 - 另外,在类关联时间结束时,如果单一实现信息已更新,且方法 A.foo 之前拥有单一实现,但该状态现已变为无效,则依赖方法 A.foo 拥有单一实现这一假设的所有已编译代码都需要变为无效代码。去优化 - 对于堆栈上已编译的有效代码,系统会启动去优化功能,以强制使已编译无效代码进入解释器模式,从而确保正确性。系统会采用结合了同步和异步去优化的全新去优化机制。

.oat 文件中的内嵌缓存

ART 现在采用内嵌缓存,并对有足够数据可用的调用站点进行优化。内嵌缓存功能会将额外的运行时信息记录到配置文件中,并利用这类信息将动态优化添加到预先编译中。

Dexlayout

Dexlayout 是在 Android 8.0 中引入的一个库,用于分析 dex 文件,并根据配置文件对其进行重新排序。Dexlayout 旨在使用运行时配置信息,在设备的空闲维护编译期间对 dex 文件的各个部分进行重新排序。通过将经常一起访问的部分 dex 文件集中在一起,程序可以因改进文件位置而拥有更好的内存访问模式,从而节省 RAM 并缩短启动时间。

由于配置文件信息目前仅在运行应用后可用,因此系统会在空闲维护期间将 dexlayout 集成到 dex2oat 的设备编译中。

Dex 缓存移除

在 Android 7.0 及更低版本中,DexCache 对象拥有四个大型数组,与 DexFile 中特定元素的数量成正比,即:

字符串(每个 DexFile::StringId 一个引用),类型(每个 DexFile::TypeId 一个引用),方法(每个 DexFile::MethodId 一个原生指针),字段(每个 DexFile::FieldId 一个原生指针)。

这些数组用于快速检索我们以前解析的对象。在 Android 8.0 中,除方法数组外,所有数组都已移除。

解释器性能

在 Android 7.0 版本中,通过引入 mterp(一种解释器,具有以汇编语言编写的核心提取/解码/解释机制),解释器性能得以显著提升。Mterp 模仿了快速 Dalvik 解释器,并支持 arm、arm64、x86、x86_64、mips 和 mips64。对于计算代码而言,ART 的 Mterp 大致相当于 Dalvik 的快速解释器。不过,有时候,它的速度可能会显著变慢,甚至急剧变慢:

调用性能。字符串操作和 Dalvik 中其他被视为内嵌函数的高频用户方法。堆栈内存使用量较高。

Android 8.0 解决了这些问题。

详细了解内嵌

从 Android 6.0 开始,ART 可以内嵌同一个 dex 文件中的任何调用,但只能内嵌来自其他 dex 文件的叶方法。此项限制具有以下两个原因:

从其他 dex 文件进行内嵌要求使用该 dex 文件的 dex 缓存,这与同一个 dex 文件内嵌(只需重复使用调用方的 dex 缓存)有所不同。已编译代码中需要具有 dex 缓存,以便执行一系列指令,例如静态调用、字符串加载或类加载。堆栈映射只对当前 dex 文件中的方法索引进行编码。

为了应对这些限制,Android 8.0 做出了以下改进:

从已编译代码中移除 dex 缓存访问(另请参阅“Dex 缓存移除”部分)扩展堆栈映射编码。

同步方面的改进

ART 团队调整了 MonitorEnter/MonitorExit 代码路径,并减少了我们对 ARMv8 上传统内存屏障的依赖,尽可能将其替换为较新的(获取/释放)指令。

更快速的原生方法

使用 @FastNative@CriticalNative 注解可以更快速地对 Java 原生接口 (JNI) 进行原生调用。这些内置的 ART 运行时优化可以加快 JNI 转换速度,并取代了现已弃用的 !bang JNI 标记。这些注解对非原生方法没有任何影响,并且仅适用于 bootclasspath 上的平台 Java 语言代码(无 Play 商店更新)。

@FastNative 注解支持非静态方法。如果某种方法将 jobject 作为参数或返回值进行访问,请使用此注解。

利用 @CriticalNative 注解,可更快速地运行原生方法,但存在以下限制:

方法必须是静态方法 - 没有参数、返回值或隐式 this 的对象。仅将基元类型传递给原生方法。原生方法在其函数定义中不使用 JNIEnvjclass 参数。方法必须使用 RegisterNatives 进行注册,而不是依靠动态 JNI 链接。

@FastNative@CriticalNative 注解在执行原生方法时会停用垃圾回收。不要与长时间运行的方法一起使用,包括通常很快但一般不受限制的方法。

停顿垃圾回收可能会导致死锁。如果锁尚未得到本地释放(即尚未返回受管理代码),请勿在原生快速调用期间获取锁。此要求不适用于常规的 JNI 调用,因为 ART 将正执行的原生代码视为已暂停的状态。

@FastNative 可以使原生方法的性能提升高达 3 倍,而 @CriticalNative 可以使原生方法的性能提升高达 5 倍。例如,在 Nexus 6P 设备上测量的 JNI 转换如下:

Java 原生接口 (JNI) 调用执行时间(以纳秒计)常规 JNI115!bang JNI60@FastNative35@CriticalNative25

调试 ART 垃圾回收

本页介绍了如何调试 Android 运行时 (ART) 垃圾回收 (GC) 的正确性和性能问题。此外,还说明了如何使用 GC 验证选项、确定应对 GC 验证失败的解决方案,以及衡量并解决 GC 性能问题。

如需使用 ART,请参阅此 ART 和 Dalvik 部分中介绍的内容,以及 Dalvik 可执行文件格式。如需获得验证应用行为方面的其他帮助,请参阅在 Android Runtime (ART) 上验证应用行为

ART GC 概览

ART 有多个不同的 GC 方案,涉及运行不同的垃圾回收器。从 Android 8 (Oreo) 开始,默认方案是并发复制 (CC)。另一个 GC 方案是并发标记清除 (CMS)。

并发复制 GC 的一些主要特性包括:

CC 支持使用名为“RegionTLAB”的触碰指针分配器。此分配器可以向每个应用线程分配一个线程本地分配缓冲区 (TLAB),这样,应用线程只需触碰“栈顶”指针,而无需任何同步操作,即可从其 TLAB 中将对象分配出去。CC 通过在不暂停应用线程的情况下并发复制对象来执行堆碎片整理。这是在读取屏障的帮助下实现的,读取屏障会拦截来自堆的引用读取,无需应用开发者进行任何干预。GC 只有一次很短的暂停,对于堆大小而言,该次暂停在时间上是一个常量。在 Android 10 及更高版本中,CC 会扩展为分代 GC。它支持轻松回收存留期较短的对象,这类对象通常很快便会无法访问。这有助于提高 GC 吞吐量,并显著延迟执行全堆 GC 的需要。

ART 仍然支持的另一个 GC 方案是 CMS。此 GC 方案还支持压缩,但不是以并发方式。在应用进入后台之前,它会避免执行压缩,应用进入后台后,它会暂停应用线程以执行压缩。如果对象分配因碎片而失败,也必须执行压缩操作。在这种情况下,应用可能会在一段时间内没有响应。

由于 CMS 很少进行压缩,因此空闲对象可能会不连续。CMS 使用一个名为 RosAlloc 的基于空闲列表的分配器。与 RegionTLAB 相比,该分配器的分配成本较高。最后,由于内部碎片,Java 堆的 CMS 内存用量可能会高于 CC 内存用量。

GC 验证和性能选项

更改 GC 类型

原始设备制造商 (OEM) 可以更改 GC 类型。如需进行更改,需要在构建时设置 ART_USE_READ_BARRIER 环境变量。默认值为 true,这会启用 CC 回收器,因为该回收器使用读取屏障。对于 CMS,此变量应明确设置为 false。

默认情况下,在 Android 10 及更高版本中,CC 回收器在分代模式下运行。如需停用分代模式,可以使用 -Xgc:nogenerational_cc 命令行参数。或者,也可以按如下方式设置系统属性:

1
adb shell setprop dalvik.vm.gctype nogenerational_cc

CMS 回收器始终在分代模式下运行。

验证堆

堆验证可能是调试 GC 相关错误或堆损坏的最有用的 GC 选项。启用堆验证会使 GC 在垃圾回收过程中在几个点检查堆的正确性。堆验证的选项与更改 GC 类型的相同。启用后,堆验证流程会验证根,并确保可访问对象仅引用了其他可访问对象。您可以通过传入以下 -Xgc 值来启用 GC 验证:

启用后,[no]preverify 将在启动 GC 之前执行堆验证。启用后,[no]presweepingverify 将在开始垃圾回收器清除过程之前执行堆验证。启用后,[no]postverify 将在 GC 完成清除之后执行堆验证。[no]preverify_rosalloc[no]postsweepingverify_rosalloc[no]postverify_rosalloc 是附加 GC 选项,仅验证 RosAlloc 内部记录的状态。因此,它们仅适用于使用 RosAlloc 分配器的 CMS 回收器。验证的主要内容是,魔法值是否与预期常量匹配,以及可用内存块是否已全部在 free_page_runs_ 映射中注册。

性能

衡量 GC 性能的工具主要有两个:GC 时序转储和 Systrace。Systrace 还有一个高级版本,称为 Perfetto。如需衡量 GC 性能问题,直观的方法是使用 Systrace 和 Perfetto 确定哪些 GC 会导致长时间暂停或抢占应用线程。尽管 ART GC 经过多年发展已得到显著改进,但不良更改器行为(例如过度分配)仍会导致性能问题

回收策略

CC GC 通过运行新生代 GC 或全堆 GC 来回收垃圾。理想情况下,新生代 GC 的运行频率更高。GC 会一直执行新生代 CC 回收,直到刚结束的回收周期的吞吐量(计算公式是:释放的字节数除以 GC 持续秒数)小于全堆 CC 回收的平均吞吐量。发生这种情况时,将为下一次并发 GC 选择全堆 CC(而不是新生代 CC)。全堆回收完成后,下一次 GC 将切换回新生代 CC。新生代 CC 在完成后不会调整堆占用空间限制,这是此策略发挥作用的一个关键因素。这使得新生代 CC 运行得越来越频繁,直到吞吐量低于全堆 CC,最终导致堆增大。

使用 SIGQUIT 获取 GC 性能信息

如需获得应用的 GC 性能时序,请将 SIGQUIT 发送到已在运行的应用,或者在启动命令行程序时将 -XX:DumpGCPerformanceOnShutdown 传递给 dalvikvm。当应用获得 ANR 请求信号 (SIGQUIT) 时,会转储与其锁定、线程堆栈和 GC 性能相关的信息。

如需获得 GC 时序转储,请使用以下命令:

1
adb shell kill -S QUIT PID

这会在 /data/anr/ 中创建一个文件(名称中会包含日期和时间,例如 anr_2020-07-13-19-23-39-817)。此文件包含一些 ANR 转储信息以及 GC 时序。您可以通过搜索“Dumping cumulative Gc timings”(转储累计 GC 时序)来确定 GC 时序。这些时序会显示一些需要关注的内容,包括每个 GC 类型的阶段和暂停时间的直方图信息。暂停信息通常比较重要。例如:

1
young concurrent copying paused:Sum: 5.491ms 99% C.I. 1.464ms-2.133ms Avg: 1.830ms Max: 2.133ms

本示例中显示平均暂停时间为 1.83 毫秒,该值应该足够低,在大多数应用中不会导致丢帧,因此您不必担心。

需要关注的另一个方面是挂起时间,挂起时间测量在 GC 要求某个线程挂起后,该线程到达挂起点所需的时间。此时间包含在 GC 暂停时间中,所以对于确定长时间暂停是由 GC 缓慢还是线程挂起缓慢造成的很有用。以下是 Nexus 5 上的正常挂起时间示例:

1
suspend all histogram:Sum: 1.513ms 99% C.I. 3us-546.560us Avg: 47.281us Max: 601us

还有其他一些需要关注的方面,包括总耗时和 GC 吞吐量。示例:

1
2
3
Total time spent in GC: 502.251ms
Mean GC size throughput: 92MB/s
Mean GC object throughput: 1.54702e+06 objects/s

以下示例说明了如何转储已在运行的应用的 GC 时序:

1
2
adb shell kill -s QUIT PID
adb pull /data/anr/anr_2020-07-13-19-23-39-817

此时,GC 时序在 anr_2020-07-13-19-23-39-817 中。以下是 Google 地图的输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
Start Dumping histograms for 2195 iterations for concurrent copying
MarkingPhase: Sum: 258.127s 99% C.I. 58.854ms-352.575ms Avg: 117.651ms Max: 641.940ms
ScanCardsForSpace: Sum: 85.966s 99% C.I. 15.121ms-112.080ms Avg: 39.164ms Max: 662.555ms
ScanImmuneSpaces: Sum: 79.066s 99% C.I. 7.614ms-57.658ms Avg: 18.014ms Max: 546.276ms
ProcessMarkStack: Sum: 49.308s 99% C.I. 6.439ms-81.640ms Avg: 22.464ms Max: 638.448ms
ClearFromSpace: Sum: 35.068s 99% C.I. 6.522ms-40.040ms Avg: 15.976ms Max: 633.665ms
SweepSystemWeaks: Sum: 14.209s 99% C.I. 3.224ms-15.210ms Avg: 6.473ms Max: 201.738ms
CaptureThreadRootsForMarking: Sum: 11.067s 99% C.I. 0.835ms-13.902ms Avg: 5.044ms Max: 25.565ms
VisitConcurrentRoots: Sum: 8.588s 99% C.I. 1.260ms-8.547ms Avg: 1.956ms Max: 231.593ms
ProcessReferences: Sum: 7.868s 99% C.I. 0.002ms-8.336ms Avg: 1.792ms Max: 17.376ms
EnqueueFinalizerReferences: Sum: 3.976s 99% C.I. 0.691ms-8.005ms Avg: 1.811ms Max: 16.540ms
GrayAllDirtyImmuneObjects: Sum: 3.721s 99% C.I. 0.622ms-6.702ms Avg: 1.695ms Max: 14.893ms
SweepLargeObjects: Sum: 3.202s 99% C.I. 0.032ms-6.388ms Avg: 1.458ms Max: 549.851ms
FlipOtherThreads: Sum: 2.265s 99% C.I. 0.487ms-3.702ms Avg: 1.031ms Max: 6.327ms
VisitNonThreadRoots: Sum: 1.883s 99% C.I. 45us-3207.333us Avg: 429.210us Max: 27524us
InitializePhase: Sum: 1.624s 99% C.I. 231.171us-2751.250us Avg: 740.220us Max: 6961us
ForwardSoftReferences: Sum: 1.071s 99% C.I. 215.113us-2175.625us Avg: 488.362us Max: 7441us
ReclaimPhase: Sum: 490.854ms 99% C.I. 32.029us-6373.807us Avg: 223.623us Max: 362851us
EmptyRBMarkBitStack: Sum: 479.736ms 99% C.I. 11us-3202.500us Avg: 218.558us Max: 13652us
CopyingPhase: Sum: 399.163ms 99% C.I. 24us-4602.500us Avg: 181.851us Max: 22865us
ThreadListFlip: Sum: 295.609ms 99% C.I. 15us-2134.999us Avg: 134.673us Max: 13578us
ResumeRunnableThreads: Sum: 238.329ms 99% C.I. 5us-2351.250us Avg: 108.578us Max: 10539us
ResumeOtherThreads: Sum: 207.915ms 99% C.I. 1.072us-3602.499us Avg: 94.722us Max: 14179us
RecordFree: Sum: 188.009ms 99% C.I. 64us-312.812us Avg: 85.653us Max: 2709us
MarkZygoteLargeObjects: Sum: 133.301ms 99% C.I. 12us-734.999us Avg: 60.729us Max: 10169us
MarkStackAsLive: Sum: 127.554ms 99% C.I. 13us-417.083us Avg: 58.111us Max: 1728us
FlipThreadRoots: Sum: 126.119ms 99% C.I. 1.028us-3202.499us Avg: 57.457us Max: 11412us
SweepAllocSpace: Sum: 117.761ms 99% C.I. 24us-400.624us Avg: 53.649us Max: 1541us
SwapBitmaps: Sum: 56.301ms 99% C.I. 10us-125.312us Avg: 25.649us Max: 1475us
(Paused)GrayAllNewlyDirtyImmuneObjects: Sum: 33.047ms 99% C.I. 9us-49.931us Avg: 15.055us Max: 72us
(Paused)SetFromSpace: Sum: 11.651ms 99% C.I. 2us-49.772us Avg: 5.307us Max: 71us
(Paused)FlipCallback: Sum: 7.693ms 99% C.I. 2us-32us Avg: 3.504us Max: 32us
(Paused)ClearCards: Sum: 6.371ms 99% C.I. 250ns-49753ns Avg: 207ns Max: 188000ns
Sweep: Sum: 5.793ms 99% C.I. 1us-49.818us Avg: 2.639us Max: 93us
UnBindBitmaps: Sum: 5.255ms 99% C.I. 1us-31us Avg: 2.394us Max: 31us
Done Dumping histograms
concurrent copying paused: Sum: 315.249ms 99% C.I. 49us-1378.125us Avg: 143.621us Max: 7722us
concurrent copying freed-bytes: Avg: 34MB Max: 54MB Min: 2062KB
Freed-bytes histogram: 0:4,5120:5,10240:19,15360:69,20480

文章来源:

Author:Honng
link:http://yonghong.tech/2021/07/android-runtime-dalvik/