之前发在通吧了,发回来供基佬们讨论
原文翻译自Anandtech: http://anandtech.com/print/8231/a-closer-look-at-android-runtime-art-in-android-l
自己翻译。如果有术语错误,请以原文为准。请谅解。
深入了解Android L的ART
最近的I/O大会之后,google终于正式公开了它对Android新的运行环境的计划。Android RunTime,简称ART,用于执行Android上的Java代码的虚拟机,是取代Dalvik的继任者。我们已经在去年秋季上市的KitKat上对它稍有了解,但是对于它的术语和技术细节,Google并未言之过多。
对比其它的移动平台,比如iOS,Windows或者Tizen,它们都运行着针对特定硬件架构编译的本地代码,但绝大多数Android软件都基于通用的代码语言,将编译代码生成的ByteCode转化为设备自身的本机指令。
从最早的Android版本开始,Dalvik从一个不太复杂的虚拟机起步。但是随着时间推移,Google开始意识到让其性能跟上业界硬件脚步的重要性。Google在Android 2.2中加入了JIT编译器,加入了多线程功能,并且想逐步优化改进。
但是,在最近几年中,整个生态系统已经超过了Dalvik的发展过程,所以Google需要建立一个能够为以后打下坚实基础,能够适应今天与未来的八核处理器,大容量存储和大内存的新的虚拟机。
所以ART诞生了。
架构
首先,ART的设计完全兼容Dalvik已有的字节码格dex(Dalvik executable)。所以从开发者的角度,在从Dalvik转移至ART的过程中,完全无需担心兼容性的问题并且代码无需任何改动。
ART带来最大的改变,就是用AOT(Ahead of time,运行前编译)取代了JIT(Just In Time,运行时编译)。ART将之前需要在每次应用运行时进行的编译,变成了只做一次。在之后所有的执行中,都会执行这一次编译完成的字节码。
当然,这些本地的转换使得应用程序会耗费更多存储空间,并且这个新的方案只有像现在这样Android设备的可用存储空间大幅提升了之后,才成为了可能。
这个转变使得以前很多无法实现的优化成为了可能:因为代码只被优化和编译一遍,所以将它优化到位是很重要的。Google宣称现在已经可以达到一个更高的针对整个程序代码的优化层次,而以前的JIT编译器只能进行本地方法块的优化。一般对代码检测异常的部分被大量的移除,并且方法和接口调用得到了显著的提速。新的"dex2oat"完成以上过程,取代以往Dalvik使用的"dexopt"。ART中也不需要odex文件(optimized dex,优化的dex),被ELF文件取代。
因为ART编译一个ELF可执行文件,所以内核现在可以处理代码分页——这将带来更好的内存管理与更少的内存使用。我很好奇KSM(Kernel same-page merging)会带来什么样的效果,这是绝对值得关注的。
对于电池续航,它的作用也是非常明显的——因为在应用运行时无需进行JIT的工作,直接减少了CPU的使用,降低功耗。
所有这一切带来的唯一坏处,就是一次编译需要更多时间。设备与应用的第一次启动时间,相比Dalvik都会明显变长。Google指出时间不会拉的太长,他们预期最终的正式版甚至会比Dalvik速度更快。
相比Dalvik,ART的性能提升是显著的。如上图,基本上运行在虚拟机上的代码,都有了2倍的性能提升。
Google指出诸如Chessbench这样提升3倍的应用,更能代表真实情况下,Android L正式版所带来的提升。
垃圾回收(Garbage Collection, GC):理论与实践
Android的虚拟机依靠自动内存管理的模式,它作为Java编程范式的基础从一开始,就是Android的一部分。对自动内存管理这个概念不甚熟悉的人来说,一个比较通俗的解释就是,一个程序员既不手动申请物理内存,也不手动释放它。它不同于以手动管理内存为常态的底层编程。当然,自动管理的好处就是开发者无须担心内存管理问题,坏处就是开发者无法控制,无法自己进行优化。
Android与Dalvik深受Dalvik的GC方案之苦。每次当某一程序需要分配内存并且堆(heap, 一块应用专属的内存空间)无法分配的时候,GC将会启动。
GC的工作是仔细检查堆,枚举所有应用分配的对象,逐一标记仍在使用的对象,并且释放掉剩余的未标记对象。
在Dalvik中,这导致以下两种暂停——一种因枚举过程产生,另一种因标记产生。在这里,暂停指的是为了保证应用的完整性,该应用所有线程所有的代码执行都会停止。如果暂停过长,这将导致在渲染的过程中掉帧,这就是Android卡顿的来源。
Google宣称在Nexus 5上,这样的暂停平均耗时54ms,这导致了每次GC启动时,都会带来至少4帧的掉帧。
在我自己的体验与调查中,这个数字因具体程序不同而差异巨大。比如,官方的FIFA app就是一个十分鲜明的例子,GC进行的十分狂野如下:
07-01 15:56:14.275: D/dalvikvm(30615): GC_FOR_ALLOC freed 4442K, 25% free 20183K/26856K, paused 24ms, total 24ms
07-01 15:56:16.785: I/dalvikvm-heap(30615): Grow heap (frag case) to 38.179MB for 8294416-byte allocation
07-01 15:56:17.225: I/dalvikvm-heap(30615): Grow heap (frag case) to 48.279MB for 7361296-byte allocation
07-01 15:56:17.625: I/Choreographer(30615): Skipped 35
07-01 15:56:19.035: D/dalvikvm(30615): GC_CONCURRENT freed 35838K, 43% free 51351K/89052K, paused 3ms+5ms, total 106ms
07-01 15:56:19.035: D/dalvikvm(30615): WAIT_FOR_CONCURRENT_GC blocked 96ms
07-01 15:56:19.815: D/dalvikvm(30615): GC_CONCURRENT freed 7078K, 42% free 52464K/89052K, paused 14ms+4ms, total 96ms
07-01 15:56:19.815: D/dalvikvm(30615): WAIT_FOR_CONCURRENT_GC blocked 74ms
07-01 15:56:20.035: I/Choreographer(30615): Skipped 141
07-01 15:56:20.275: D/dalvikvm(30615): GC_FOR_ALLOC freed 4774K, 45% free 49801K/89052K, paused 168ms, total 168ms
07-01 15:56:20.295: I/dalvikvm-heap(30615): Grow heap (frag case) to 56.900MB for 4665616-byte allocation
07-01 15:56:21.315: D/dalvikvm(30615): GC_FOR_ALLOC freed 1359K, 42% free 55045K/93612K, paused 95ms, total 95ms
07-01 15:56:21.965: D/dalvikvm(30615): GC_CONCURRENT freed 6376K, 40% free 56861K/93612K, paused 16ms+8ms, total 126ms
07-01 15:56:21.965: D/dalvikvm(30615): WAIT_FOR_CONCURRENT_GC blocked 111ms
07-01 15:56:21.965: D/dalvikvm(30615): WAIT_FOR_CONCURRENT_GC blocked 97ms
07-01 15:56:22.085: I/Choreographer(30615): Skipped 38
07-01 15:56:22.195: D/dalvikvm(30615): GC_FOR_ALLOC freed 1539K, 40% free 56833K/93612K, paused 87ms, total 87ms
07-01 15:56:22.195: I/dalvikvm-heap(30615): Grow heap (frag case) to 60.588MB for 1331732-byte allocation
07-01 15:56:22.475: D/dalvikvm(30615): GC_FOR_ALLOC freed 308K, 39% free 59497K/96216K, paused 84ms, total 84ms
07-01 15:56:22.815: D/dalvikvm(30615): GC_FOR_ALLOC freed 287K, 38% free 60878K/97516K, paused 95ms, total 95ms
以上是这个程序运行之后几秒内的日志。垃圾回收器一共被唤醒9次,导致程序冻结603ms,总共掉帧214帧。内存分配的请求导致的暂停,在Log中都以“GC_FOR_ALLOC”标记。
而ART承诺的垃圾回收性能的提升是极其巨大的。以下是ART在执行前面同样任务的对比:
07-01 16:00:44.531: I/art(198): Explicit concurrent mark sweep GC freed 700(30KB) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 186us total 12.763ms
07-01 16:00:44.545: I/art(198): Explicit concurrent mark sweep GC freed 7(240B) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 198us total 9.465ms
07-01 16:00:44.554: I/art(198): Explicit concurrent mark sweep GC freed 5(160B) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 224us total 9.045ms
07-01 16:00:44.690: I/art(801): Explicit concurrent mark sweep GC freed 65595(3MB) AllocSpace objects, 9(4MB) LOS objects, 810% free, 38MB/58MB, paused 1.195ms total 87.219ms
07-01 16:00:46.517: I/art(29197): Background partial concurrent mark sweep GC freed 74626(3MB) AllocSpace objects, 39(4MB) LOS objects, 1496% free, 25MB/32MB, paused 4.422ms total 1.371747s
07-01 16:00:48.534: I/Choreographer(29197): Skipped 30
07-01 16:00:48.566: I/art(29197): Background sticky concurrent mark sweep GC freed 70319(3MB) AllocSpace objects, 59(5MB) LOS objects, 825% free, 49MB/56MB, paused 6.139ms total 52.868ms
07-01 16:00:49.282: I/Choreographer(29197): Skipped 33
07-01 16:00:49.652: I/art(1287): Heap transition to ProcessStateJankImperceptible took 45.636146ms saved at least 723KB
07-01 16:00:49.660: I/art(1256): Heap transition to ProcessStateJankImperceptible took 52.650677ms saved at least 966KB
ART与Dalvik的差别不能再大——ART在同样的情况下,停顿了12.364ms,前台调用4次GC,后台调用2次。在应用运行的全程中,内存的占用也并无增长,而在Dalvik中,内存的占用增加了4次。掉帧也减少到了63帧。
显然这是一个对于一个烂应用能发生的最坏的场景,在ART的情况下,程序依然会有些许的掉帧,但是坏的编程习惯——比如重载UI线程是Android更需要面对的问题。
ART减轻了以前垃圾回收器的很多负担,使执行过程中的暂停不再必要,第二类暂停也通过在暂停之前尽可能完成工作而极大减少——一种被叫做Packard预清除的技术得以应用,而暂停本身在一个简单的检查与校验的过程中完成。Google宣称他们做到了将暂停时间减小到了3ms,是相对Dalvik GC的极大提升。
同时,不同于堆的“大对象空间”,Large Object Space,简称LOS,被引入,它依旧在应用程序内存中,它被用于更好的处理大的对象,比如图片。这些大的简单的对象如果使堆变得碎片化,将产生很严重的问题,导致在回收对象时更多的GC调用。在更加智能的内存分配方案下,GC调用频率大幅减少,同时内存的碎片化程度也有显著的降低。
另一个比较好的例子是Hangout的app,在Dalvik中,我们看到这样几次GC调用导致的暂停:
07-01 06:37:13.481: D/dalvikvm(7403): GC_EXPLICIT freed 2315K, 46% free 18483K/34016K, paused 3ms+4ms, total 40ms
07-01 06:37:13.901: D/dalvikvm(9871): GC_CONCURRENT freed 3779K, 22% free 21193K/26856K, paused 3ms+3ms, total 36ms
07-01 06:37:14.041: D/dalvikvm(9871): GC_FOR_ALLOC freed 368K, 21% free 21451K/26856K, paused 25ms, total 25ms
07-01 06:37:14.041: I/dalvikvm-heap(9871): Grow heap (frag case) to 24.907MB for 147472-byte allocation
07-01 06:37:14.071: D/dalvikvm(9871): GC_FOR_ALLOC freed 4K, 20% free 22167K/27596K, paused 25ms, total 25ms
07-01 06:37:14.111: D/dalvikvm(9871): GC_FOR_ALLOC freed 9K, 19% free 23892K/29372K, paused 27ms, total 28ms
我们可以看到所有GC调用的示例,明确的和并发的GC调用是GC通用的清理与维护的调用。for_alloc的调用是内存分配器试图分配内存但是发现并不匹配,于是GC启动以获得更多空间。在中间,我们看到了堆因为碎片化尺寸增加,而且不能装入大对象。总的暂停时间一共90ms。作为对比,以下是Android L预览版中的情况:
07-01 06:35:19.718: I/art(10844): Heap transition to ProcessStateJankPerceptible took 17.989063ms saved at least -138KB
07-01 06:35:24.171: I/art(1256): Heap transition to ProcessStateJankImperceptible took 42.936250ms saved at least 258KB
07-01 06:35:24.806: I/art(801): Explicit concurrent mark sweep GC freed 85790(3MB) AllocSpace objects, 4(10MB) LOS objects, 850% free, 35MB/56MB, paused 961us total 83.110ms
我们不是特别确定这样的记录代表什么,但是我们认为它们是重新为堆指定尺寸的过程。唯一的一次GC调用是在应用完成启动之后,耗时961us。我们没在这次GC之前看到任何类似的调用。比较有趣的事LOS的统计。我们看到LOS中有4个10MB的大对象,以前它们都会被分配在程序的堆中,但现在它们在分配的时候被忽略了,这成功的避免了GC的重复调用与可能拖慢Dalvik速度的碎片化内存。
内存分配系统自身也有提升。当ART自身提供相对Dalvik的25%速度提升时,Google并不是特别满意,并且引入了Linux内核中一个新的内存分配器,替换了当前使用的malloc分配器。
新的分配器叫rosalloc,全称是Rows of slots Allocator,为多线程Java应用程序设计。新的分配器有一个更加细粒度的结构,可以锁定独立的对象。在线程本地区域中的小对象也可以被一起锁定。
这使得分配速度的提升也极其巨大,多达10倍。
垃圾回收算法自身也被修订,用于提升用户体验与避免程序的中断。这些算法尚未完工,而Google最近只是发布了一个新的专用的算法,Moving Garbage collector,主要目的是整理后台程序堆的碎片。
64位支持
ART基于模块化的设计,面向不同的目标架构。因此,它面向目前最常见的架构提供了编译器后端,比如ARM,X86和MIPS,以及ARM64,x86-64和仍未实现的MIPS64。
当我们在iPhone5s的评测中讨论切换至64bit的优劣时,我们已经讨论得很深入了,主要的好处是在完全兼容已有32bit应用的同时,提升了可用的地址空间,提升了性能,以及极大提升的加密解密性能。
Google与Apple的一个很大的区别,至少是在虚拟机应用上,是Google使用了引用压缩避免通常64bit导致的内存膨胀,虚拟机依旧使用了32bit的引用。
Google展示了部分跑分以对比ARM和x86切换至64bit的性能。x86的跑分在Intel的Bay Trail上执行,在不同的Render
然而Google也拿出了一些比较有趣的数字。Google内建的Panorama,我们可以看到32bit到64bit有着13-19%的性能提升。我们也可以看到,在64bit下,Cortex-A53的提升更为显著。
Google声称当前Play商店中85%的应用已经准备好立即切换至64bit——这意味着只有15%的应用含有部分本地代码需要开发者进行对64bit架构针对性的编译,在以往切换至64bit的过程中,Google也算是很成功的。
结语
从很多方面来说,Google已经给出了性能的巨大提升,并且解决了很多阻碍Android发展的短板。
ART补足了很多在运行非本地应用与内存管理过程中的缺陷。作为一名开发者,我无法要求更多。以前我必须通过机智的算法与特别的编程技巧解决的诸多性能问题,现在都不再是一个硬伤了。
这也意味着Android终于可以在应用程序的流畅性与性能上,与iOS有所一拼了。这是消费者的大胜利。
Google保证未来依旧会不断改进ART,而且现在的它已经与6个月前刚发布时有了很大的区别。而6个月之后Android L最终上市的时候,它也一定不会与现在完全一致。未来很光明,我已经等不及要看看Google还会拿ART做些什么了。