缘起
接着上期”了解一下,Android 10中的ART虚拟机(I)“,今天继续介绍ART。今年春节十几天假里,我大概把profman和dex2oat整体看了一遍。出乎我意料的是,dex2oat居然再一次让我看得万念俱灰。
在我写《深入了理解Android Java虚拟机ART》一书的时候,我最早也是先研究的dex2oat,稿子都写了100多页了,但数月过后就是无法拿下,所以只能放下它,转而去研究ART Runtime。这是我写深入理解Android 4本书来第一次碰到这种挫折。所以,书中dex2oat的字节码到机器码的编译部分在第六章,但dex2oat的源码分析却在第九章。
我的困惑
本来以为有了Nougat ART的基础,研究AOSP 10中的ART应该是水到渠成,但没想到依然是困难重重。虽然我很快能把AOSP 10中dex2oat的执行顺序梳理出来(这一点上和AOSP 7.0没有太大区别),但感觉这一路的风景变化很大。这其中,最让我感到恐慌的是这样一个问题:我很强烈的感觉到,dex2oat很多特性、其中的很多代码所对应的功能都和Java VM技术本身有着某种密切关系,但我却不知道这些特性、功能到底是为了解决JVM的什么问题——套用一个朋友的原话”就是每行代码都能看懂,但就是不知道为什么“——也就是知其然,却不知其所以然
也就是说,我一直以来只是被动的去解释ART的代码,但却没有真正掌握JVM。并且,没有掌握随着Java语言本身的发展,随着整体技术的发展,JVM本身所需要做的改进。我相信代码的作者在改进ART虚拟机时脑子是有一条路线的,绝对不是一拍脑袋就想出来的。
我之前很单纯的把上面这个问题归结于我对语言本身不了解。这才有了前面几期公众号中我试图以quickjs和javascript语言为研究课题在这方面有所突破。但这条路可能是失败了。因为js引擎(其实就是javascript虚拟机)难度并不比jvm少。而且,quickjs引擎还涉及到从js代码到quickjs字节码的编译,而这部分在jvm中是不涉及的,因为jvm处理的已经是字节码了。
那么,问题到底在哪?同时,我对研究ART的好处也产生了巨大的怀疑,研究这玩意到底学到了什么?(这个质疑对我个人来说已经是非常严厉的了)。万般无奈之下,我转而去看一下别人关于JVM的书都讲些什么。于是,我在微信读书PC版上完整的读了周志明老师《深入理解Java虚拟机:JVM高级特性与最佳实践》,最新版,两遍。它给我带来了意想不到的感受和认识。
我的一些认识
先简单说下周老师这本书,我认为是名至实归的深入理解。这种”深入理解“的表现方式并不是和我的书一样,把代码给你弄出来,一行一行去给你解释。而更倾向于一种”深入浅出“:一个很复杂的东西,用最简单,最让人理解的方式表达出来,同时把它出现的背景,渊源,未来发展,甚至在实际案例中的价值都讲出来。这就非常非常难了,光涉及”渊源“的地方就需要做大量长期的跟踪工作。神农班之前安排过一个局,就是介绍Java泛型,有同学洋洋洒洒千字,但和书中对泛型在java中的介绍对比起来,这文字背后所体现的技术功底的差距就非常明显了。
再说下我从周老师这本书里看到的一些东西。先讲讲技术之外的认识。我必须客观承认,周老师这本书我现在是能几乎无障碍读下来,而且有恍然大悟的体会。这个结果归根结底还是这几年在ART上花的功夫。这就是所谓的付出终有回报。尤其在周书中介绍编译器这一块,我没感受到太大的难度,包括Java字节码,Class的解析等。要知道在2015年前,我在Java上的经历只不过用Java开发过一些Android UI程序,对JVM毫无所知。所以,通过这一点,我觉得前几年对ART的投入,以及最终的结果——《深入理解Android Java虚拟机ART》这本书还是有效果。这个认识给了我一点信心。BTW,我感觉上述效果对其他读者也是有的。大家可以试一下——也就是拿我的书结合周老师的书一起看看。
接下来介绍一下技术方面的认识。
首先,JVM从一开始就不纯粹是为了Java语言而生的。我看了周老师书之后才注意到这一点。其实在Jvm规范中,人家开篇就说了”The Java Virtual Machine knows nothing of the Java programming language, only of a particular binary format, the class file format. A class file contains Java Virtual Machine instructions (or bytecodes) and a symbol table, as well as other ancillary information.“。
上面英文的意思是JVM和Java编程语言毛关系都没有。它只认class文件。这句话完全拓展了我对JVM的认识。我一直对OS很痴迷。只不过Linux Kernel被其它人上上下下左左右右前前后后都玩透了,所以我才选择设备端的JVM为目标。但我之前并未把jvm当做一个类似kernel,类似qemu这样的虚拟机。我还计划过今年什么时候花点时间好好研究下真正的虚拟机QEMU(源码都下好了)。但这句话一出,原来踏破铁鞋无觅处,JVM就是VM(虽然实际上它还是和Java语言有各种各样的关系,但我现在看待它的角度不太一样了),不要把它和Java语言绑定死。实际上,几十年来,java语言从1发展到今天的13,但字节码却只新增了一个invoke-dynamic。从这个角度看,kotlin这个语言其实也可以当做一门独立于Java之外的语言了。甚至,只要有合适的工具,我们可以把C/C++/Javascript的代码编译成class文件,转而由jvm执行了。再想多一点,某公司的某编译器,貌似其愿景和早已有几十年历史的jvm殊途同归咯?
其次,我之前一直不明白jvm规范里为什么还要有linking(链接)的概念。我早期一直从事C++的开发。在C++开发中,链接往往是编译阶段的事情。而且,这个事情是通过一个比较明确的阶段来做的。即先编译,然后链接,然后生成最终的二进制。但写java程序的时候,本身是感受不到链接的。为何JVM里会有呢?其实,我们只要从一个高层次的角度来看待链接就能明白。链接,我猜测(没考证,仅仅是根据书中的内容推断的)应该在各种语言里都存在。因为它是为了解决这样的一个基本问题:即你写的代码如何调用别人写的代码?在C++开发中,这个工作是由程序员来主动完成的。但对jvm来说,链接是由jvm来完成的。其目标都是一样,把你代码里调用的符号和真实的地方对应起来。
再有,对协程的重新认识。java里的协程我最早在2010年左右就曾经接触过,但作为一个C++开发者,我对线程的认识更熟悉,认为OS里的线程调度是经过千锤百炼的,而协程最终还是要转换成对线程的使用,自己搞个调度器实在是????。但最近这几年随着go语言的使用,协程又重新展示了它的巨大价值。周老师书中展示了一个测试比较,用协程后,系统的吞吐量大增。所以,协程肯定是有价值的。貌似java语言后续要原生支持协程,只不过自己搞个调度器的难度确实不小。大家拭目以待。
最后,周老师书中对JIT和AOT优劣势有非常详细的介绍。这个让我对某司某编译器的搞法有了不同角度的评价。仁者见仁智者见智,这里就不多说了。我总体感觉是,JVM的发展和Java语言的发展关系密切,如果仅从android这个小小的领域里想引领jvm的发展,显然是有极大和致命的不足。
以上是我基于周老师这本书的一些认识。结合我最近看《生而不凡》一书里看到的一段话。人的成长大概有两种方式,一个是来源于我们对现实认知的改变,另外一种则是来自于我们自身行为方式的升级。显然,我现在对JVM的认知有了一些新的变化,也有了更多的信心。我收集了好几本书,大概率会进一步,好好的从高层次角度把JVM摸清楚。
接下来,我简单介绍下我对profman和dex2oat的一些研究成果。
dex2oat
aosp 10中dex2oat更加清爽了,有些类的安排也更合理。相比nougat,新增了compact dex、vdex等内容。oat文件格式也发生了一些变化。
先说下如何使用dex2oat,我好不容易从编译日志文件里扒拉出主机上生成boot.oat相关的执行命令(注意,aosp 10中,如果干掉makefile里的PREOPT的话,模拟器居然不会主动生成oat文件,而是直接以解释方式执行。我之前在nougat上是没有此问题的。报错原因貌似是没有权限操作/data/dalvikvm,......)
文字版的命令和输入参数如下
out/soong/host/linux-x86/bin/dex2oatd
--avoid-storing-invocation
--write-invocation-to=out/soong/generic_x86_64/dex_apexjars/system/framework/x86_64/apex.invocation
--runtime-arg
-Xms64m
--runtime-arg
-Xmx64m
--compiler-filter=speed-profile
--profile-file=out/soong/generic_x86_64/dex_bootjars/boot.prof
--dirty-image-objects=frameworks/base/config/dirty-image-objects
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/core-oj.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/core-libart.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/okhttp.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/bouncycastle.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/apache-xml.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/framework.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/ext.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/telephony-common.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/voip-common.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/ims-common.jar
--dex-file=out/soong/generic_x86_64/dex_apexjars_input/android.test.base.jar
--dex-location=/apex/com.android.runtime/javalib/core-oj.jar
--dex-location=/apex/com.android.runtime/javalib/core-libart.jar
--dex-location=/apex/com.android.runtime/javalib/okhttp.jar
--dex-location=/apex/com.android.runtime/javalib/bouncycastle.jar
--dex-location=/apex/com.android.runtime/javalib/apache-xml.jar
--dex-location=/system/framework/framework.jar
--dex-location=/system/framework/ext.jar
--dex-location=/system/framework/telephony-common.jar
--dex-location=/system/framework/voip-common.jar
--dex-location=/system/framework/ims-common.jar
--dex-location=/system/framework/android.test.base.jar
--generate-debug-info
--generate-build-id
--oat-symbols=out/soong/generic_x86_64/dex_apexjars_unstripped/system/framework/x86_64/apex.oat
--strip
--oat-file=out/soong/generic_x86_64/dex_apexjars/system/framework/x86_64/apex.oat
--oat-location=out/soong/generic_x86_64/dex_apexjars/system/framework/apex.oat
--image=out/soong/generic_x86_64/dex_apexjars/system/framework/x86_64/apex.art
--base=0x70000000
--instruction-set=x86_64
--instruction-set-variant=x86_64
--instruction-set-features=default
--android-root=out/empty
--no-inline-from=core-oj.jar
--abort-on-hard-verifier-error
--generate-mini-debug-info
有了上述命令,各位可以魔改dex2oat,例如加日志,加上对其他CPU体系结构支持之类的,在host上运行就可以学习dex2oat的执行流程了,简直不能太棒....
dex2oat里涉及到vdex、oat的文件格式。由于时间原因,我先简单介绍下内容。以下是oat文件格式。对比nougat的oat,貌似把dex文件内容去掉了。
(以前的oat文件是完整包含dex信息的),其他的初看起来没有太大区别。
那dex文件放哪去了?答案在vdex里。以下是vdex文件格式。这是从源码注释里抠出来的。上面说,vdex包含了从apk/jar里抽取出来的dex文件。
vdex文件里有VerifierDeps,貌似和虚拟机对class文件的校验有关。属于JVM里一个比较重要的功能,我希望以后有机会单独拎出来说。这个地方也是让我之前痛苦之处。即我完全不知道Verifier到底要干啥...现在好了,我大概知道学习的方向了。
代码中,vdex文件的生成在如下代码处
另外,aosp10中,dex文件有标准dex文件和compact dex文件两种。具体我还没来得及细看。使用compact dex的话不确定能压缩多少。这一块应该属于ART本身的一种优化。
最后,dex2oat在生成机器码时,使用的寄存器分配算法支持图着色法和线性分配。下面是对应的代码截图
关于dex2oat,先说这么多。最关键的是最开始执行dex2oat的命令。通过这个命令,大家可以在host上跟踪dex2oat的执行流程。
profman
profman和生成profile文件有关,它是PGO(profile guided optimzation)的基础。简单来说,profile里统计了某个程序里的热点函数,然后机器码生成的时候可以针对这些热点函数做不同的优化。周老师一书中提到的PGO可以包含很多信息,但我从ART里看到的profile信息好像并不多。
profile文件的生成有两种方式:
从人类可读的文本文件,通过profman转换成.profile格式的文件。这个对art boot镜像以及一些系统核心服务jar包有价值。因为这样可以在做系统的时候就提前把字节码转成机器码,所谓的Ahead of Time编译(注意,周老师书中对JIT和AOT优劣势有非常详细的介绍,建议阅读完我书第六章后再看一下,要不看不懂)。
APP运行时,由JIT模块生成.profile文件。等一定时候后,dex2oat用这个profile文件来实施PGO。
先看第一个例子,framework/base/services/art-profile是一个典型案例
这个案例中,显示的是人类可读的profile信息(文本文件)。其包含的核心内容是哪个类(->之前)对应的哪个方法是热点函数。注意,每一行前面还有HSP之类的字符。HSP是Hot、Start、Poststartup的意思,代表三种含义。
Hot是热点函数,Startup和Poststartup对应虚拟机启动前/后的场景。这三个理论上可指导不同的优化方法。但我研究还不深,暂且先不对细节进行介绍。
对了,系统类对应的profile文件位于frameworks/base/config/boot-image-profile.txt里。它最终通过build/soong/java/dexpreopt_bootjars.go来指导系统类的PGO。
profman根据人类可读的文件后,生成.profile文件。.profile文件更方便程序读取。其文件格式如下:
此处我也不再多说。以后有机会再详细介绍。
最后,我们介绍下程序运行时,哪里会生成profile文件。答案在ART JIT模块里。代码如下:
jit会启动一个ProfileSaver线程,具体工作方式姑且不提,最终的profile文件由这个线程去处理。
最后,我们讲一下应用开发里经常碰到的对APP所做的性能监控。APP的性能监控和上述的Profile不是一回事。APP性能监控集中关注于某个函数调用花的时间。其采样方式如下图代码所示:
上述代码的else分支里,APP性能监控是针对方法的,在方法Entered/Exited/Unwind之处加一些时间戳,这样就能获取每个函数调用花的时间。
if分支里,还可能启动一个sampling_thread_函数来定期扫描各个线程的调用栈。跟踪栈顶的变化,即可知道函数调用的情况。总之,其目的是检测函数调用所花费的时间。
后续的安排
我想重点树立起和JVM密切有关的知识体系。有了ART源码打底子,我相信这条路走得通。对JVM的掌握是非常有必要的,我感觉国家层面在底层基础核心技术上会加大投入,JVM是一个非常合适的突破口。
最后的最后
我期望的结果不是朋友们从我的书、文章、博客后学会了什么知识,干成了什么,而应该是说,神农,我可是踩在你的肩膀上的喔。
关于学习方面的问题,我已经讨论完了。后面这个公众号将对一些基础的技术,新技术做一些学习和分享。也欢迎你的投稿。不过,正如我在公众号“联系方式”里说的那样——郑渊洁在童话大王《智齿》里有一句话令我印象深刻,大意是“我有权保持沉默,但你说的每一句话都可能成为我灵感的源泉”。所以,影响不是单向的,很可能我从你那学到的东西更多。
神农和朋友们的杂文集
长按识别二维码关注我们