Dex文件格式、指令码
一个Class
文件对应一个Java源码文件,而一个Dex
文件可对应多个Java源码文件。开发者开发一个Java模块(不管是Jar包还是Apk)时:
-
在PC平台上,该模块包含的每一个Java源码文件都会对应生成一个同文件名(不包含后缀)的
.class
文件。这些文件最终打包到一个压缩包(即Jar
包)中。 -
而在Android平台上,这些Java源码文件的内容最终会编译、合并到一个名为
classes.dex
的文件中。不过,从编译过程来看,Java源文件其实会先编译成多个.class
文件,然后再由相关工具将它们合并到Jar
包或Apk
包中的classes.dex
文件中。
Dex文件的这种做法有什么好处呢?笔者至少能想出如下两个优点:
-
虽然
Class
文件通过索引方式能减少字符串等信息的冗余度,但是多个Class
文件之间可能还是有重复字符串等信息。而classes.dex
由于包含了多个Class
文件的内容,所以可以进一步去除其中的重复信息。 -
如果一个
Class
文件依赖另外一个Class
文件,则虚拟机在处理的时候需要读取另外一个Class
文件的内容,这可能会导致 CPU 和存储设备进行更多的 I/O 操作。而classes.dex
由于一个文件就包含了所有的信息,相对而言会减少 I/O 操作的次数。
字节序
Java平台上,字节序采用的是 Big Endian。所以,Class
文件的内容也采用Big Endian字 节序来组织其内容。而 Android 平台上的Dex
文件默认的字节序是Little Endian(这可能是因为ARM CPU(也包括X86 CPU)采用的也是Little endian字节序的原因吧)。
Dex 文件格式的概貌
图3-3所示为Dex文件格式的概貌。其各个成员解释如下。
- 首先是Dex文件头,很重要,类型为
header_item
。 string_ids
:数组,元素类型为string_id_item
,它存储和字符串相关的信息。type_ids
:数组,元素类型为type_id_item
。存储类型相关的信息(由TypeDescriptor
描述)。field_ids
:数组,元素类型为field_id_item
,存储成员变量 信息,包括变量名、类型等。method_ids
:数组,元素类型为method_id_item
,存储成员函数信息包括函数名、参数和返回值类型等。class_defs
:数组,元素类型为class_def_item
,存储类的信息。data
:Dex文件重要的数据内容都存在data
区域里。一些数据结构会通过如xx_off
这样的成员变量指向文件的某个位置,从该位置开始,存储了对应数据结构的内容,而xx_off
的位置一般落在data
区域里。link_data
:理论上是预留区域,没有特别的作用。
和Class
文件格式比起来,Dex
文件格式的特点如下:
- 有一个文件头,这个文件头对正确解析整个
Dex
文件至关重要。 - 有几个
xxx_ids
数组,包括string_ids
(字符串相关)、type_ids
(数据类型相关)、proto_ids
(主要功能就是用于描述成员函数的参数、返回值类型,同时包含 ShortyDescriptor信息)、field_ids
(成员域相关)和method_ids
(成员函数相关)。 data
区域存储了绝大部分的内容,而data
区域的解析又依赖于header
和相关的数据项。
Dex 指令码介绍
Dex 指令码的条数和 Class 指令码差不多,都不超过 255 条,但是 Dex 文件中存储函数内容的insns数组(位于code_item结构体里)却比Class文件中存储函数内容的code 数组(位于 Code 属性中)解析起来要有难度。其中一个原因是 Android 虚拟机在执行指令码的时候不需要操作数栈,所有参数要么和 Class 指令码一样直接跟在指令码后面,要么就存储在寄存器中。对于参数位于寄存器中的指令,指令码就需要携带一些信息来表示该指令执行时需要操作哪些寄存器。此外,虽然官方文档详细介绍了所有Dex指令码的格式和含义,但是它采用了一种特别的语法来描述它们,所以初学者读官方文档时会感觉比较难懂。
insns_size
和insns
数组:指令码数组的长度和指令码的内容。Dex 文件格式中 JVM 指令码长度为 2 个字节,而 Class 文件中 JVM 指令码长度为 1 个字节。
由图3-9可知:
-
Dex指令码的长度还是1个字节,所以指令码的个数不会超过255条。但是和Class指令码不同的是,Dex指令码与第一个参数混在一起构成了一个双字节元素存储在insns内。在这个双字节中,低8位才是指令码,高8位是参数。笔者称这种双字节元素为 [参数+操作码组合]。
-
[参数+操作码组合] 后的下一个
ushort
双字节元素可以是新一组的 [参数+操作码组合],也可以是 [纯参数组合]。 -
参数组合的格式也有要求,不同的字符代表不同的参数,参数的比特位长度又是由字符的个数决定。比如AA表示一个参数,这个参数占8位,而其中每一个A都代表4位比特长。
提示:
关于图3-9中的参数格式,根据官方文档,下面几点内容需要读者了解。
(1)不同的字符代表不同的参数,比如A、B、C代表三个不同的参数。
(2)参数的长度由对应字符的个数决定,1个字符占据4个比特。比如:A表示一个占4比特的参数,AA代表一个占8比特的参数,AAAA代表一个16比特长的参数。
(3)代表一个特殊的参数,该参数取值为0。比如ØØ表示这样一个参数,这个参数长度为8位,每位的取值都是0。
在图 6-57 中:
-
右边是 C/C++ 源码编译成目标机器码后,该目标机器码编译,运行时只和目标的操作系统和相关库有关。
-
而 Dex/Java 字节码虽然也编译成目标机器码,但是它的编译和运行不仅仅依赖操作系统,还依赖具体的虚拟机实现。比如Dex字节码编译成机器码的话就依赖ART虚拟机。
这个道理很容易理解,但是也很容易被忽视。很多人以为 Java 字节码编译成机器码后就能和那些 C/C++ 编译得到的机器码一样无所羁绊地直接在OS上运行了,殊不知在Java字节码编译为机器码的过程中,虚拟机会添加一些必要和特殊的指令,使得得到的机器码在运行过程中实际上离不开虚拟机的管控。这里不妨举一个例子加以说明。
- Java虚拟机的垃圾回收器做对象标记前,往往会设置一个标志,表示自己要做对象标记了。
- 其他线程运行时要经常检查这个标志,发现这个标志为
true
时,这些线程就得等待,好让垃圾回收器能安全地做对象标记。否则的话,垃圾回收器一边做对象标记,其他线程同时又去创建对象或更改对象间的引用关系,这将导致对象标记不准确,影响垃圾回收。
显然,程序员在代码中是不会主动加上这个检查标记的动作。实际上,这是由编译器来主动完成的。它会在两个地方添加标记检查指令,一个是在 Entry 基本块里,另一个是在 loopheader 基本块里。
Java 字节码编译得到的机器码是离不开虚拟机的,它的编译也依赖与具体的虚拟机实现。
Android 在 Dalvik 时代采用的是 Java 虚拟机技术中较为成熟的 Just-In-Time(即时编译, 简写为 JIT)编译方案,JIT 会将热点 Java 函数的字节码转换成机器码,这样可提升虚拟机的运行速度。而 Android 虚拟机换为 ART 后,Google 最初却非常激进地抛弃了 JIT,转而采用了 Ahead-Of-Time(预编译,简写为 AOT)编译方案。AOT 导致系统在安装应用程序之时就会尝试将 APK 中大部分 Java 函数的字节码转换为机器码,其尽一切可能提升虚拟机运行速度的努力用心良苦。但 AOT 却带来了应用程序安装时间过长,编译生成的 oat 文件过大等一系列较为影响用户体验的副作用。为此,Android 在 7.0(Nougat)中对 ART 虚拟机进行了改造,综合使用了 JIT、AOT 编译方案,解决了纯 AOT 的弊端,同时还达到了预期目标。
ELF 文件
和.class
及.dex
文件对应,.oat
文件是 Android ART 虚拟机上的“可执行文件”。虽然 Android 官方没有明确解释oat
表示什么意思,但通过相关源码和一些工具我们发现它其实是一种经 Android 定制的 ELF 文件。ELF 文件是 oat 文件的基础,其难度较大,本章先来学习ELF。
概述
ELF 是 Executable and Linkable Format 的缩写,它是 Unix(包括Linux这样的类Unix) 平台上最通用的二进制文件格式。那些使用 Native 语言比如 C/C++ 开发的程序员几乎每天都会和 ELF 文件打交道,比如:
- C/C++ 文件编译后得到的
.o
(或.obj
)文件就是 ELF 文件。 - 动态库
.so
文件是 ELF 文件。 .o
文件和.so
文件链接后得到的二进制可执行文件也是 ELF 文件。
提示
.oat
是一种定制化的 ELF 文件,所以 EFL 文件是 oat 文件的基础,但是 oat 文件包含的内容和 art 虚拟机密切相关。
传统Java虚拟机的可执行文件是
.class
文件,Dalvik虚拟机的可执行文件是.dex
文件,而ART虚拟机的可执行文件是.oat
文件。
ELF文件格式介绍
如前述内容可知,ELF 是 Executable and Linkable Format 的缩写。其名称中的 “Executable”和“Linkable”表明ELF文件有两种重要的特性。
- Executable:可执行。ELF文件将参与程序的执行(Execution)工作。包括二进制程序的运行以及动态库
.so
文件的加载。 - Linkable:可链接。ELF文件是编译链接工作的重要参与者。下面来看ELF文件格式的内容,如图4-1所示。
图4-1表明,我们从不同角度(View)来观察ELF的话,将会看到不同的信息。
- Linking View:链接视图,它是从编译链接的角度来观察一个ELF文件应该包含什么内容。
- Execution View:执行视图,它是从执行的角度(可执行文件或动态库文件)来观察一个ELF文件应该包含什么信息。
介绍几个关键的.text
和.bss
等section
。
.text
section:用于存储程序的指令。简单点说,程序的机器指令就放在这个section中。根据规范,.text section的 sh_type 为 SHT_PROGBITS(取值为1),意为 Program Bits,即完全由应用程序自己决定(程序的机器指令当然是由程序自己决定的),sh_flags 为 SHF_ALLOC(当ELF文件加载到内存时,表示该Section会分配内存)和 SHF_EXECINSTR(表示该Section包含可执行的机器指令)。.bss
section:bss 是 block storage segment 的缩写。ELF规范中,.bss section包含了一块内存区域,这块区域在ELF文件被加载到进程空间时会由系统创建并设置这块内存的内容为 0。注意,.bss section 在 ELF 文件里不占据任何文件的空间,所以其 sh_type 为 SHF_NOBITS(取值为8),它只是在ELF加载到内存的时候会分配一块由 sh_size 指定大小的内存。.bss 的sh_flags 取值必须为 SHF_ALLOC 和 SHF_WRITE(表示该区域的内存是可写的。同时, 因为该区域要初始化为0,所以要求该区域内存可写)。什么样的数据应该属于 .bss section 呢?如果读者在 main.c 中定义一个全局的"int a = 0
"之后,生成的 main.o 就包含有效的 .bss section了。.data
section:.data 和 .bss 类似,但是它包含的数据不会初始化为0。这种情况下就需要在文件中包含对应的信息了。所以 .data 的 sh_type 为 SHF_PROGBITS,但 sh_flags 和 .bss 一样。读者可以尝试在 main.c 中定义一个比如"char c = 'f'
"这样的变量就能看到 .data section 的变化了。.rodata
section:包含只读数据的信息,比如 main.c 中 printf 里的字符串就属于这一类。 它的 sh_flags 只能为 SHF_ALLOC。
虚拟机的创建和启动
在 Android 系统中,Java 虚拟机是借由大名鼎鼎的 Zygote 进程来创建的。Zygote 是 Java 世界的创造者,即 Android 中所有 Java 进程都由 Zygote 进程 fork 而来,而 Zygote 进程自己又是 Linux 系统上的 init 进程通过解析配置脚本来启动的。假设目标设备为32位CPU架构,zygote 进程对应的配置脚本文件是system/core/rootdir/init.zygote32.rc,该文件描述了init该如何启动zygote进程,如图7-1所示。
接着来看 AndroidRuntime
的 start
函数。
上述代码中和启动ART虚拟机密切相关的两个重要函数如下所示。
Jnilnvocation
的Init
函数:它将加载ART虚拟机的核心动态库。AndroidRuntime
的startVm
函数:在ART虚拟机对应的核心动态库加载到zyogte
进程后,该函数将启动ART虚拟机。
JniInvocation Init 函数介绍
先来看JniInvocation
的Init
函数,代码如下所示。
由上述代码可知,我们将从 libart.so
里将取出并保存三个函数的函数指针:
- 这三个函数的代码位于
java_vm_ext.cc
中。 - 第二个函数
JNI_CreateJavaVM
用于创建Java虚拟机,所以它是最关键的。
AndroidRuntime startVm 函数介绍
接着来看AndroidRuntime
的startVm
函数,代码如下所示。
如上述代码中的注释所言, JNI_CreateJavaVM
函数并非是Jnilnovcation
Init
从 libart.so
获取的那个JNI_CreateJavaVM
函数。相反,它是直接在AndroidRuntime.cpp
中定义的,其代码如下所示。
辗转多次,终于和 libart.so
关联上了。马上来看libart.so
中的这个JNI_CreateJavaVM
函数,代码如下所示。
在上述libart.so
的JNI_CreateJavaVM
代码中,我们见到了ART虚拟机的化身Runtime
(即 ART虚拟机在代码中是由Runtime
类来表示的)。其中:
Runtime::Create
将创建一个Runtime
对象。Runtime::Start
函数将启动这个Runtime
对象,也就是启动Java虚拟机。
先来看 Runtime 对象的创建。
VM 和 Runtime:
虚拟机一词的英文为Virtual Machine(简写为VM)。Runtime则是另外一个在虚拟机 技术领域常用于表示虚拟机的单词。Runtime也被翻译为运行时,在本书中,笔者使用虚拟机来表示它。在ART虚拟机Native层代码中,Runtime
是一个类。而JDK源码里也有一个Runtime
类(位于java.lang
包下)。这个Java Runtime
类提供了一些针对整个虚拟机层面而言的API,比如exit
(退出虚拟机)、gc
(触发垃圾回收)、load
(加载动态库)等。
内存映射
MemMap
是一个辅助工具类,它封装了和内存映射(memory map)有关的操作。
MemMap
使用mmap、msync、mprotect
等系统调用来完成具体的内存映射、设置内存读写权限等相关操作。它可创建基于文件的内存映射以及匿名内存映射。mmap
系统调用的返回值只是一个代表地址的指针,而MemMap
则提供了更多的成员变量来辅助我们更好地使用mmap
得到的这块映射内存。比如,每一个MemMap
对象都有一个名称。- 另外,对于非x86_64的64位平台,如果要想映射内存到进程的低2G空间地址的话(即想在非x86_64的64位平台上使用
mmap
的MAP_32BIT标志),MemMap需要做一些特殊处理。
与线程同步相关的辅助类
ART 提供了 Mutex
、ReadWriteMutex
、ConditionVariable
等辅助类来实现互斥锁、条件变量等常用的同步操作。它们定义于mutex.h
中,不同平台的实现略有不同。另外,ART 还借助一种称之为 Lock Hierarchies 的方法来解决线程同步时经常出现的因为使用锁的顺序不一样导致死锁的问题(即线程应该按相同的顺序抢占互斥锁,比如先锁住互斥锁A,接着再锁住互斥锁B,否则极易出现死锁的情况)。在 Lock Hierachies 体系下,互斥锁可以设置一个优先级,如果某个资源需要多个锁来保护的话,只有先拿到高优先级的锁之后才能去抢占低优先级的锁。如果顺序反了,运行时可以采取报错或程序退出的方式来处理。注意,LockHierachies 只是提供了解决死锁问题的思路,读者可结合 ART 代码以及下文的资料 http://www.drdobbs.com/parallel/use-lock-hierarchies-to-avoid-deadlock/204801163来加深对它的认识。
OAT文件
OatFileManager 介绍
OatFileManager
用于管理虚拟机加载的oat
文件。dex
字节码编译成机器码后,相关内容会存储在一个以.oat
为后缀名的文件里。我们先来简单认识 OAT 文件的格式。
OAT 文件格式简介
图7-3所示为OAT文件的部分内容。
图7-3展示了OAT文件的部分内容及格式。
- 一个OAT文件包含一个
OatHeader
头结构。注意,这个OatHeader
信息并不存储在OAT文件的头部。OAT文件其实是一个ELF格式的文件,相关信息存储在ELF对应的段中。 - Oat文件是怎么来的呢? 它是对
jar
包或apk
包中的dex
项(名为classes.dex
、classes2.dex
、classes3.dex
等,其实就是dex
文件打包到jar
或apk
里了。以后我们统称它们为dex
文件,而不必理会它们是单独的.dex
文件还是jar
或apk
包中的一项)进行编译处理后得到的(该过程借助dex2oat
来完成)。jar
或apk
中可包含多个dex
项(即所谓的multidex
),每一个jar
或apk
中的所有dex
文件在oat
文件中对应都有一个OatDexFile
项,OatDexFile
项存储了一些信息,比如它所对应的dex
文件的路径、dex
文件的校验以及其他各种信息在oat
文件中的位置(offset)等。 OatDexFile
区域之后的是DexFile
区域。在生成oat
文件时,jar
或apk
中classes.dex
的(如果有多个话,则包含classes2.dex
、classes3.dex
)内容会完整地拷贝到Oat文件中对应的DexFile
区域。简单点说,OAT文件里的一个DexFile
项包含一个.dex
文件的全部内容。通过在OAT文件中包含dex文件的内容,ART虚拟机只需要加载OAT文件即可获取相关信息,而不需要单独再打开dex文件了。当然,这种做法也使得 OAT 文件尺寸较大。- 现在回过头来看
OatDexFile
。每一个OatDexFile
对应一个DexFile
项。OatDexFile
中有一个dex_file_offset
成员用于指明与之对应的DexFile
在 OAT 文件里的偏移量。当然,OatDexFile
还有其他类似的成员(以offset_
做后缀)用于指明其他信息在OAT文件里的偏移量。
最后,笔者简单说一下关于boot oat文件里所包含的系统基础类。在 frameworks/base
下有一个preloaded-classes
文件,其内容是希望加载到 zygote
进程里的类名(按照JNI格式定义),这些类包含在不同的 boot oat 文件里。图7-5展示了其中的部分内容。
图7-5中 “…” 号是由笔者添加的省略号。由于zygote是Java世界的第一个进程,其他APP进程(包括 system server 进程)均由zygote进程fork而来。所以:
- 这些加载到 zygote 进程里的类也叫预加载类,即所谓的 preloaded classes。
- 根据 linux 进程 fork 的机制,其他 APP 进程从 zygote fork 后,将继承得到这些预加载的类。
信号处理和 SignalAction 介绍
信号处理
Linux系统中,一个进程可以接收来自操作系统或其他进程发送的信号(Signal)。简单点说,信号就是事件(event),代表某个事件发生了,而接收进程可以对这些事件进行有针对性的处理。
- Linux 系统支持POSIX中的标准和实时两大类信号。ART只处理标准信号。
- 信号由信号 ID(一个正整数)来唯一标示。每一个信号都对应有一个信号处理方法。信号处理方法是指当某个进程接收到一个信号时该如何处理它。进程可以为某些信号设置特定的处理方法。如果不设置的话,操作系统将使用预先规定好的办法来处理这些信号,也就是所谓的默认处理。
- 一个进程可以阻塞某些信号(block signal)。阻塞的意思是指这些信号只要发生的话还是会由操作系统投递到目标进程的信号队列中,只不过OS不会通知进程进行处理而已。这些被阻塞的信号(pending signal)将存储在目标进程的信号队列中,一旦进程解除它们的阻塞,OS就会通知进程进行处理。
如果想要为某个信号设置信号处理结构体的话,需要使用系统调用sigaction
,其定义如下。
SignalAction 类介绍
现在来看看ART基于上述内容所提供的封装类 SignalAction
。由上述代码可知,我们可以为一个SignalAction
对象:
- 直接设置一个特殊的信号处理函数。该步骤借助
SetSpecialHandler
函数来完成。 - 设置一个信号处理结构体。该步骤借助
SetAction
来完成。
使用第一种方法的话必须调用 SetSpecialSignalHandlerFn
函数。
FaultManager介绍
FaultManager的初始化
FaultManager的初始化步骤中涉及FaultManager的构造函数以及Init函数。先来看 FaultManager 的构造函数,代码如下所示。
接着来看Init函数,代码如下所示。
线程
Attach 函数介绍
接着来看Attach
的代码,如下所示。
Attach 内部又包含三个关键函数,先来看第一个,即Thread的构造函数。
Thread 构造函数
Thread的构造函数并不复杂,主要是完成对某些成员变量的初始化,来看代码。
Init函数介绍
Thread Init的代码如下所示。
InitStackHwm
本函数用于设置线程的线程栈。我们先回顾下一个线程的栈空间是怎么设置的。在Android平台上,我们可通过调用pthread_create
来创建一个线程,来看看pthread_create
的函 数,重点考察其中对线程栈的处理,代码如下所示。
这里再次特别说明,栈只有一个出入口,即栈顶,而栈底是不动的。线程的栈空间由 allocate_thread
分配。
通过上面pthread_create
的代码,我们可知 Android 平台上线程栈的创建过程如下。
mmap
得到一块内存,其返回值为该内存的低地址(stack_base
)。- 设置该内存从低地址开始的某段区域(由
guard_size
)为不可访问。 - 得到该内存段的高地址,将其作为线程栈的栈底位置传递给
clone
系统调用。
总结上述内容可知,
- 在ART虚拟机中,每一个Thread 对象代表一个线程。
- 每个线程在
InitCpu
中都会将代表自己的Thread对象的地址设置GDT中,并且将关联的GDT表项的索引保存到FS寄存器里。这么做的目的是当这个线程执行 generatedcode的时候,如果需要调用quick entrypoints 等虚拟机提供的函数时,均可借助图7-8所示的流程进行跳转。 - 而要达到这个目的所必需的前提条件是FS的内容会随着线程切换而做相应的切换。因为FS只有一个,而线程A的Thread对象和线程B的Thread对象不会是同一个,所以FS的内容应该随着线程的切换而相应进行调整。好在这部分工作由操作系统来完成。
Thread FinishSetup
接着来看Thread FinishSetup函数,代码如下所示
Thread CreatePeer
研究代码之前,笔者先简单介绍下和 Java Thread 有关的一些背景知识。
-
我们知道,在 Java 世界里,线程的概念包装在 Thread 类里。创建一个 Thread 实例,并start它,则会启动一个操作系统概念中的线程。
-
通过上面的描述可知,一个 Java Thread实例是需要和操作系统中的某个线程关联到一起的。光有一个JavaThread实例,而没有操作系统里对应的线程来支持它,那这个Thread 对象充其量也就是一块内存罢了。
在Java Thread类中有一个名为 nativePeer
的成员变量,这个变量就是该Thread实例所关联的操作系统的线程。当然,出于管理需要, nativePeer
并不会直接对应到操作系统里线程ID这样的信息,而是根据不同虚拟机的实现被设置成不同的信息。
下面的CreatePeer
的功能包括两个部分:
-
创建一个
Java Thread
实例。 -
把调用线程(操作系统意义的线程,即此处的
art Thread
对象)关联到上述Java Thread
实例的nativePeer
成员。
简单点说,ART 虚拟机执行到这个地方的时候,代表主线程的操作系统线程已经创建好了,但 Java 层里的主线程 Thread 示例还未准备好。而这个准备工作就由CreatePeer
来完成。
上述代码执行后,我们总结相关信息于图8-3。
在图8-3中:
- Java Thread 的
nativePeer
成员(括号中的为该成员的数据类型)指向一个 ART Thread 对象。 - ART Thread 对象中的
tlsPtr.opeer
和tlsPtr.jpeer
都指向同一个 Java Thread 实例。opeer
的类型为mirror Object*
,jpeer
的类型为jobject
。以后我们将看到,这两个成员变量 只是使用场景不同。
ThreadList 和 ThreadState
Java 虚拟机中往往运行了多个 Java 线程。为了方便管理,ART 设计了一个 ThreadList 类来统一管理这些 Java 线程。这里请读者注意:
- 每一个Java线程都对应为ART虚拟机中的一个Thread对象。
- Native 线程可通过JavaVM::AttachCurrentThread 接口将自己变成一个 Java 线程。而这 就会创建对应的一个 Thread 对象。
Heap
HeapBitmap 相关类
当我们使用new
或malloc
等内存分配方法创建一个对象时,得到的是该对象所在内存的地址,即指针。指针本身的长度根据CPU架构的不同导致是32位长或者是64位长。如果创建1万个对象的话,那么这一万个对象的指针本身所占据的内存空间就很可观了。如何减少指针本身所占据的内存空间呢?ART采用的办法很简单,就是将对象的指针转换成一个位图里的索引,位图里的每一位指向一个唯一的指针。来看图7-12的示例。
在图7-12中:
- 中间框是一个有n个比特位的位图(如果按字节计算,则该位图长度为 n/8 字节长)。这个位图本身是一块内存,由基地址
pbitmap
表示。其上下还有两个方框,代表两块连续的内存,起始地址分别是pbase1
和pbase2
。 - 先来看
pbase1
对应的内存块。这块内存中存储的是指针,p0指向对象0(object0),p1指向对象1(object1)。p0和p1本身占据的内存长度为sizeof(指针)
字节。显然,如果有很多个对象的话,内存块1会占用不小的空间。优化的办法很简单,就是将 p0、p1 的值借助位图索引来计算。比如,第x
个对象的地址就是pbase1 + x * sizeof(指针)
。 - 除了可以保存对象的指针外,还可以用位图存储更大块的空间。比如
pbase2
对应的内存块,其内部又可细分为以4KB
为单位的空间。那么,第y
个4K内存空间的起始位置就是pbase2 + y * 4KB
。 - 不管是
pbase1
还是pbase2
所对应的内存,如果我们想知道第x
个对象是否存在的话,该怎么处理呢?答案很简单,设置中间那个位图框中第x个索引位的值即可。如果第x
个索引位的值为1
,则表明第x
个对象存在,比如pbase1 + x * sizeof(指针)
处的内存被占用了,否则表示该对象不存在,即pbase1 + x * sizeof(指针)
处的内存空间空闲。
art文件
.art 文件格式介绍
一个包含 classes.dex
项的 jar 或 apk 文件经由 dex2oat 进行编译处理后实际上会生成两个结果文件,一个是.oat
文件,另外一个是.art
文件。图7-15简单展示了这个过程。
图7-15中,当用 dex2oat 对一个 jar 包或 apk 进行编译处理后,其输出文件包含两个文件。
- 一个是
.oat
文件。 值得再次指出的是,jar 或 apk 中的classes.dex
内容将被完整拷贝到 oat 文件里。 - 另外一个文件是
.art
文件。它就是 ART 虚拟机代码里常提到的Image
文件。art 文件的格式在官方文档中没有介绍,相关资料也很少。所以学习art文件格式相对会困难一些。art文件和oat 文件密切相关。
根据art文件的来源(比如它是从哪个jar包或 apk包编译得来的),Image
分为boot
镜像(boot image)和 app
镜像(app image)。
- 来源于某个 apk 的 art 文件称为 App 镜像。
- 来自 Android 系统里 /system/framework 下那些核心 jar 包的 art 文件统称为 boot 镜像。
这些核心 jar 包包含了 Android 系统最基础和很重要的类。注意,系统核心 jar 包有多个,比如core-oj.jar
(oj 是 open jdk 的简称。jdk所包含的类几乎都在其中)、framework.jar
、org.apache.http.legacy.jar
、okhttp.jar
等。由于这些核心类在 ART 虚拟机启动时就必须加载,所以称它们为 boot 镜像文件。
为什么叫 Image?
笔者在很长一段时间内都非常困惑为什么 art 文件会被称为 Image。随着研究的深入,笔者对这个问题有了一个较为粗浅的认识。首先,art 文件加载到虚拟机里都是通过 mmap 的方式来完成的,加载到内存里的位置在 art 文件的 ImageHeader 结构体中有描述。其次,art 文件的内容布局是有严格组织的,这些内容将加载到内存里的不同的位置。最后,这些信息从文件中映射到内存后,可以直接转换成对应的对象。就好像我们事先将对象的信息存储到文件中,后续只不过再将其从文件中还原出来一样。
另外,一般而言,针对核心库的编译都会生成 boot.art 镜像文件,而针对 app 的编译则通过 dex2oat 相关选项来控制是否生成对应的 art 文件。
就本章而言,art 文件结构中的 ImageHeader 最为关键,图7-16展示了它的部分信息。
图7-16展示了art文件格式的部分内容,它分为左中右三个部分。
- 左边是art文件的组成结构,图中只绘制了位于文件头部的关键数据结构 ImageHeader。
- 右边是ImageHeader结构体的各个成员变量。
magic_
数组存储的是art文件格式的魔幻数,取,值为['a', 'r', 't', '\n']
,version_
数组为art文件格式的版本号,取值为['0', '2', '9', '\0']
。image_begin_表示该art文件期望自己被映射到内存的什么位置,image_size_则表示映射多大空间到内存。ImageHeader中sections_是一个非常重要的成员,它是一个数组,数组大小固定为kSectionCount(取值为9),数组成员的数据类型为ImageSection。art 文件中包含9个section,每个section存储了不同的信息。ImageSection就是用来描述 一个section在内存里什么位置(基于image_begin_的偏移量)以及该section有多大。 storage_mode_表示文件内容(除ImageHeader外)是否为压缩存储。 - 中间是art文件加载到内存里的情况。image_begin_是这块内存的起始位置。特别注意,ImageHeader的内容被包括在sections_[kSectionObjects]中(取值为0),即该section从 image_begin_开始)。另外,image_size_只覆盖到 sections_[kSectionImageBitmap-1], 而 sections_的最后一个元素 sections_[kSectionImageBitmap]则从image_size_之后某 个按页大小对齐的位置处开始。结合上文对HeapBitmap的介绍,读者可知道sections_[kSectionImageBitmap]应该是一个位图空间。
JNI
JavaVMExt 和 JNIEnvExt
本节讨论JNI中最常见的两个类JavaVM
和JNIEnv
。根据笔者在《深入理解Android卷1》一书中对JNI知识的介绍可知:
- JavaVM 在 JNI 层中表示 Java 虚拟机。它的作用有点像 Runtime。只不过 JNI 作为一种规范,它必须设定一个统一的结构,即此处的JavaVM。不同的虚拟机实现里,真实的虚拟机对象可以完全不一样,比如 art 虚拟机中的 Runtime 才是当之无愧的虚拟机。另外,一个 Java 进程只有一个 JavaVM 实例,在 ART 虚拟机中,JavaVM 实际代表的是 JavaVMExt 类。
- JNIEnv 代表 JNI 环境,每一个需要和 Java 交互(不管是Java层进入Native层,还是 Native层进入Java层)的线程都有一个独立的 JNIEnv 对象。 同理,JNIEnv 是 JNI 规范里指定的数据结构,不同虚拟机有不同的实现。在 ART 虚拟机中,JNIEnv 实际代表的是 JNIEnvExt 类。
JavaVM 是跟着进程走的,JNIEnv 是跟着线程走的。
JavaVMExt
现在来看 JavaVMExt 对象的创建,先回顾它在 Runtime Init 中的代码。
在图7-19中:
JavaVM
是一个结构体(当然,在C++中,结构体也是一种类的类型)。当定义了CPLUSPLUS宏时(按C++来编译),JavaVM 还有一个类型别名,即 JavaVM。所以, JavaVM的真实数据类型是_JavaVM
。JNIInvokeInterface
也是结构体。其中,JNIInvokeInterface
的AttachCurrentThread
、GetEnv
等成员变量的数据类型都是函数指针(为方便书写,图7-19中没有展示它们的参数)。JavaVM
结构体的第一个成员变量指向一个JNIInvokeInterface
对象。JavaVMExt
是一个类,它从JavaVM
中派生。
提示
JNI 或 runtime 模块里往往通过一个JavaVM *
类型的指针来引用一个JavaVM
对象。通过上面的介绍可知,ART中JavaVM
对象的真正数据类型是JavaVMExt
。
JNIEnvExt
JNIEnvExt
的思路和 JavaVMExt
类似,我们直接来看代码。
JNINativeInterface
和上节中提到的 JNIInvokeInterface
有些类似,都是包含了很多函数指针的结构体。
现在来看JNIEnvExt
,它是JNIEnv
的派生类。其创建是通过Create
函数来完成的。
我们重点了解下gJniNativeInterface
的内容。
总结
了解上述JavaVMExt
和JNIEnvExt
代码后,外界如果通过它们的基类JavaVM
和JNIEnv
来操作JNI相关接口时,我们就可以很方便地找到真实的函数实现在哪了。笔者总结如下:
- 操作
JavaVM
相关接口时,其实现在java_vm_ext.cc
文件的JIT
类中。如果需要检查JNI的话,则先通过check_jni.cc
的CheckJIT
类对应函数处理。最终还是会调用JIT
类的相关函数。 - 操作
JNIEnv
相关接口时,其实现在jni_internal.cc
的JNI类中。同理,如果需要检查JNI的话,也通过check_jni.cc
的CheckJNI
类对应函数先处理。
JNI里相关的数据结构和API都定义在头文件jni.h
中,来看其中的内容。
总结上述的代码可知:
- Java中基础数据类型在JNI层中都对应为native层中的某种基础数据类型。
- Java中引用类型在JNI层中对应为
_jobject
(注意,带下划线)及派生类。但是JNI的使用者只能通过_jobject
和它的派生类的指针类型(即 JNI 使用者只能使用jobject
、jclass
、jstring
等不带下划线的数据类型)来间接引用这些对象的实例。不过,鉴于上述代码明确地将_jobject
定义为一个没有任何成员变量和成员函数的类。可想而知,jobject
、jclass
等的作用和void*
差不多。 - Java类中的成员变量或成员函数在JNI中也有对应的类型。对比
_jobject
,读者可发现_jfieldID
和_jmethodID
甚至都没有实际的定义。不过,由于JNI使用者只能通过指针类型(jfieldID
和jmethodID
都是指针)来操作,所以编译不会报错。不过这也说明jfieldID
、jmethodID
的作用和void*
一样
那么,这些“void*
”背后到底是谁? Java虚拟机规范(笔者参考的是《Java VirtualMachine Specification》第 7 版)把这个问题的答案留给了各种虚拟机的实现。那么,ART 虚拟机是如何处理的呢?
先来看另外一组 ART 里常用的辅助类。
ScopedObjectAccess 等辅助类
图8-1所示为ScopedObjectAccess
辅助类家族。
在图8-1所示类家族中:
ValueObject
是一个没有任何成员,也不允许编译器自动创建构造函数的类。ScopedObjectAccessAlreadyRunnable
是关键类。它包含三个重要成员变量,Self_
指向当前调用线程的线程对象(类型为Thread
),env_
指向当前线程的JNIEnvExt
对象,而vm_
则指向代表虚拟机的JavaVMExt
对象。
我们只要看一下 ScopedObjectAccessAlreadyRunnable
的代码,上节遗留下的问题将迎刃而解。
上述代码非常清晰得展示了jfieldID
、jmethodID
以及jobject
在ART虚拟机实现里所对应的具体数据类型。
jfieldID
其实就是ArtField *
。jmethodID
其实就是ArtMethod *
。jobject
指向一个mirror Object
对象,但其具体是什么,需要再由mirror Object*
向下转换为指定的类型。
最后,我们来看一个使用AddLocalReference
的代码段,代码如下所示。
常用JNI函数介绍
FindClass
FindClass
是JNIEnv
中的API,用于查找指定类名的类信息。由7.7.2节的介绍可知,该函数的真正实现(不考虑checkJni的情况)位于jni_internal.cc
中,代码如下所示。
RegisterNativeMethods
RegisterNativeMethods
用于将native层的函数与Java层中标记为native的函数关联起来, 该函数是每一个JNI库(Linux平台上以so文件的方式提供)使用前必须调用的。
上面代码内容比较简单,不过有一个小地方需要解释,即 fast jni模式。
- 从函数调用Java层进入JNI层时,虚拟机会将执行线程的状态从Runnable转换为Native。如果JNI层里又调用Java层相关函数时,执行线程的状态又得从Native转为Runnable。
- 线程状态的切换会浪费一点执行时间。所以,对于某些特别强调执行速度的JNI函数可以设置为fast jni模式。这种模式下执行这个native函数时将不会进行状态切换,即执行线程的状态始终为Runnable。当然,这种模式的使用对GC有一些影响,所以最好在那些本身执行时间短,又不会阻塞的情况下使用。另外,这种模式目前在ART虚拟机内部很多 java native 函数有使用。为了和其他native函数进行区分,使用fast jni模式的函数的签名信息字符串必须以 “!"(感叹号)开头。
LocalRef、GlobalRef 和 WeakGlobalRef 相关函数
JNI层的代码虽然是用native语言(C++或C)开发的,但Java中和GC相关的一些特性在JNI中依然有所体现。
- JNI层中创建的
jobject
对象默认是局部引用(Local Reference)当函数从JNI层返回后,Local reference的对象很可能被回收。 所以,不能在JNI层中永久保存一个LocalReference
的对象。 - 有时候JNI层确实需要长期保存一个
jobject
对象。但如上条规则所言,JNI函数返回后,相关的jobject
对象都可能被回收。该如何保存一个需要长期使用的jobject
对象呢?答案很简单,就是将这个Local Reference
对象转换成Global Reference
(全局引用) 对象。 而全局引用对象不会被GC回收,而是需要使用者主动释放它。当然,为了减少内存占用,进程能持有的全局引用对象的总个数有所限制。 - 如果觉得
Global Reference
对象用起来不方便(比如,需要主动释放它们),则可将局部引用对象变成所谓的弱全局引用对象。弱全局引用对象有可能被回收,所以使用前需要调用JNIEnv
提供的IsSameObject
函数,将一个弱引用对象与nullptr
进行比较。
下面是JNI提供的操作这三种引用类型的三组API。
我们重点研究NewGlobalRef
的代码,如下所示。
结合JavaVMExt
的AddGlobalRef
代码可知:
- 一个Java进程中只有一个
JavaVMExt
对象,代表虚拟机本身。 - 一个
JavaVMExt
对象中有一个globals_
成员变量,这个变量是一个容器,可存储进程中所创建的全局引用对象。 - 每个全局引用对象添加到
globals_
容器后都会得到一个IndirectRef
值。这个值的类型虽然是指针类型(void *
),但它的值和要保存的mirror Object
对象的地址以及IndirectReferenceTable
内部对元素管理的方法有关。外界需通过IndirectReferenceTable
的Get
函数将一个IndirectRef
值还原为对应的mirror Object
对象。
上述代码是全局引用对象的创建,它是借助JavaVMExt
对象的AddGlobalRef
来完成的。与之相似:
- 如果是创建局部引用对象的话,将会使用
JNIEnvExt
对象的AddLocalRef
函数来完成。 - 每一个
JNIEnvExt
对象都包含一个locals_
成员变量,用于存储在这个JNIEnvExt
环境里创建的局部引用对象。
JavaVM 和 JNIEnv
如上文所述,JNI 是帮助 Java 层和 Native 层交互的接口。JNI 中有两个关键数据结构。
- JavaVM:它代表Java虚拟机。每一个 Java 进程有一个全局唯一的 JavaVM 对象。
- JNIEnv:它是JNI运行环境的含义。每一个 Java 线程都有一个 JNIEnv 对象。Java线程在执行 JNI 相关操作时,都需要利用该线程对应的
JNIEnv
对象。
JavaVM
和 JNIEnv
是jni.h
里定义的数据结构,里边包含的都是函数指针成员变量。所以,这两个数据结构有些类似 Java 中的 interface
。不同虚拟机实现都会从它们派生出实际的实现类。在 ART 虚拟机中,JavaVM
和 JNIEnv
创建的代码如下所示。
再来看 ART 中JNIEnv
的创建
JNI 中引用型对象的管理
我们先回顾一下Native层和Java层里对象的创建和销毁的过程。
- 以C++为例,Native层中要创建一个对象的话需使用
new
操作符以先分配内存,然后构造对象。如果不再使用这个对象,则需要通过delete
操作符先析构这个对象,然后回收该对象所占的内存。 - Java层中也通过
new
操作来构造一个对象。如果后续不再使用它,则可以显式地设置持有这个对象的变量的值为null
(也可以不做这一步,而交由垃圾回收来扫描和标记该对象是否有被引用)。该对象所占的内存则在垃圾回收过程中被收回。
JNI层作为Java层和Native层之间相交互的中间层,它兼具Native层和Java层的某些特性,尤其在对引用对象的创建和回收上。
- 和C++里的
new
操作符可以创建一个对象类似,JNI层可以利用JNINewObject
等函数创建一个Java意义的对象(引用型对象)。这个被New
出来的对象是Local
型的引用对象。 - JNI层可通过
DeleteLocalRef
释放Local
型的引用对象(等同于Java层中设置持有这 个对象的变量的值为null
)。如果不调用DeleteLocalRef
的话,根据JNI规范,Local
型对象在JNI函数返回后,也会由虚拟机根据垃圾回收的逻辑进行标记和回收。 - 除了
Local
型对象外,JNI层借助JNIGlobal
相关函数可以将一个Local
型引用对象转换成一个Global
型对象。而Global
型对象的回收只能先由程序显式地调用Global
相关函数进行删除,然后,虚拟机才能借助垃圾回收机制回收它们。
Mirror Object、ArtField、ArtMethod
关键类介绍
ClassLinker 中涉及常多的关键类,认识它们将极大帮助后续的代码理解。先来看Mirror Object家族。
Mirror Object 家族
ART源码文件夹中有一个子文件夹叫mirror
。这个mirror
子文件夹下代码所定义的类都位于mirror命名空间中。mirror的中文含义是镜子,那么,这面镜子里外都是什么呢?
原来,在ART虚拟机的实现中,Java 的某些类在虚拟机层也有对应的 C++ 类,比如图7-20所示的 Mirror Object类家族图谱。
图7-20展示了Mirror Object家族中几个主要的类。其中:
- Object对应Java的Object类,Class对应Java的Class类。以此类推,DexCache、String、 Throwable、StackTraceElement 等与同名Java类相对应。
- Array对应Java Array类。对基础数据类型的数组,比 如
int[]
,long[]
这样的Java类则对应图中的PrimitiveArray<int>
以及PrimitiveArray<long>
。图7-20中的 PointArray 则可与 Java层中的IntArray或LongArray对应。对于其他类型的数组,则可用ObjectArray<T>
模板类来描述。
注意,IfTable在Java 层中没有对应类。
ArtField,ArtMethod 等
我们知道,Java 源码中的 class 可以包含成员变量和成员函数,当class 经过 dex2oat 编译转 换后,一个类的成员变量和成员函数的信息将转换为对应的C++类,即 ArtField
和 ArtMethod
,如图7-23所示。
图7-23展示了ArtField
和ArtMethod
类,它们用于描述类的成员变量和成员函数的信息。其中:
declaring_class_
成员变量指向声明该成员的类是谁。access_flags_
成员变量描述该成员的访问权限,比如是public
还是private
等。ArtField
的field_dex_idx_
为该成员在dex
文件中field_ids
数组里的索引。field_ids
数组的元素的类型可由field_id_item
来描述。
同理,ArtMethod
的几个成员变量也和dex
文件格式密切相关,如dex_code_item_offset_
为该函数对应字节码在 dex
文件里的偏移量,dex_method_idx_
为该成员在dex文 件中 method_ids
数组里的索引,该数组的元素的数据类型为method_id_item
。
来看下ArtField
的GetName
函数,如果了解Dex文件格式的话,这段代码几乎没有难度。
在图8-6中:
- DexCache、PointArray、IfTable和Class都是mirror Object家族的。这里要特别注意 IfTable,它在Java层中没有对应类。
- LengthPrefixedArray是模板数组容器类,其数组元素的个数以及每个元素的大小(即SizeOf的值)在创建之初就必须确定。使用过程中不允许修改总的元素个数。该类的实现相当简单,笔者不拟介绍它。
- ArtField和ArtMethod 在ART虚拟机代码中分别用于描述一个类的成员变量和成员函数。
图8-7展示了四个关键信息的数据组织结构,首先是代表类的基本信息的class_def
结构体,其中的关键内容如下所示。
class_idx
:实际上是一个索引值,通过它可找到代表类类名的字符串。在某些书籍的术语中,它们也叫符号引用(Symbol Reference)。与之类似,superclass_idx
代表该类的父类的类名。interfaces_off
:它指向的数据结构由 type_list表示。type_list里包含一个type_item数 组。该数组的每一个成员对应描述了该类实现的一个接口类的类名(通过type_item的type_idx可找到类名)。class_data_off
:它指向的数据结构由class_data_item
表示,里边包含了这个类的成员变量和成员函数的信息。
接着看 class_data_item
结构体。其中:
direct_methods
数组和virtual_methods
数组代表该类所定义的方法以及它继承或实现 的方法。根据dex文件格式的说明,direct_methods
包含该类中所有static
、private
函数以及构造函数,而virtual_methods
包含该类中除static
、final
以及构造函数之外的函数,并且不包括从父类继承的函数(如果本类没有重载它的话)。static_fields
和instance_fields
代表该类的静态成员以及非静态成员。
最后是代表类成员的encoded_field
和 encoded_method结构体。其中:
field_idx_diff
是索引值的偏移量,通过它能找到这个成员变量的变量名,数据类型,以及它所在类的类名。method_idx_diff
和field_idx_diff
类似,通过它能找到这个成员函数的函数名、函数签 名信息(由参数类型和返回值类型组成)以及它所在类的类名。encoded_method
中的code_off
指向该成员方法对应的dex指令码内容。
初识 ArtField 和 ArtMethod
接下来先介绍 ArtField 和 ArtMethod 这两个分别代表类的成员变量和成员方法的数据结构。
如上所述,一个 ArtField 对象代表类中的一个成员变量。比如,一个 Java 类 A 中有一个名为 a 的 long 型变量。那么,在 ART 虚拟机中就有一个 ArtField 对象表示这个 a。不过,请读者务必注意, a 这个变量需要的用来存储一个 long 型数据(在Java中,long型数据占据8个字节)的空间在哪里?上面展示的 ArtField 的成员变量也没有看出来哪里有地方存储这 8 个字节。是的,一个 ArtField 对象仅仅是代表一个 Java 类的成员变量,但它自己并不提供空间来存储这个Java 成员变量的内容。
提示:下文介绍Class LinkFields时我们将看到这个Java成员变量所需的存储空间在什么地方。
接着来看 ArtMethod 的成员变量。
一个 ArtMethod 代表一个 Java 类中的成员方法。对一个方法而言(也就是一个函数),它的入口函数地址是最核心的信息。所以,ArtMethod 通过成员 ptr_size_fields_
结构体里相关变量直接就能存储这个信息。
初识 Class
接着来看Class类,先关注它的成员变量。
上述Class的成员变量较多,如下几个成员变量尤其值得读者关注。我们先介绍它们的情况,下文将详细分析它们的来历和作用。
-
iftable_
:保存了该类所直接实现或间接实现的接口信息。直接实现是指该类自己implements的某个接口。间接实现是指它的继承关系树上有某个祖父类implements了某个接口。另外,一条接口信息包含两个部分,第一部分是接口类所对应的Class对象,第二部分则是该接口类中的接口方法。 -
vtable_
:和iftable_类似,它保存了该类所有直接定义或间接定义的virtual方法信息。比如,Object类中有耳熟能详的wait、notify、toString等的11个virtual方法。所以, 任意一个派生类(除interface类之外)中都将包含这11个方法。 -
methods_
:methods_只包含本类直接定义的direct方法、virtual方法和那些拷贝过来 的诸如Miranda这样的方法(下文将介绍它)。一般而言,vtable_包含的内容要远多于methods_。 -
embedded_imtable_
、embedded_vtable_
和fields_
为隐含成员变量。其中,前两个变量只在能实例化的类中才存在。实例化是指该类在Java层的对应类可以通过new
来创建一个对象。举个反例,基础数据类、抽象类、接口类就属于不能实例化的类。
接下来我们介绍三个小知识点。
Interface default method
从Java 1.8开始,interface接口类中可以定义接口函数的默认实现了(其英文描述为Javainterface default method)。来看一段示例代码。
注意,以上所说的 interface default/static method 等只在Java 1.8上支持。
Miranda Methods
接着来认识Miranda methods。这是什么东西呢?原来,Miranda方法和美国的Miranda rights(中文译为米兰达权利或米兰达规则)有关。米兰达规则中说,如果你负担不起请律师的费用的话,法院将为你提供一个律师。放到Java世界中来,米兰达规则则变成了如果有个类没有定义某个函数的话,编译器将为你提供这个函数。为什么需要Miranda方法呢?这和JavaVM早期版本中的一个缺陷有关。
提示
关于这个自动生成的Miranda方法,资料上说是由编译器生成,但并没有说是否在编译得到的,class文件中能看到它。在Android平台上,.dex文件中并不会包含Miranda方法。而是在ART虚拟机为MirandaAbstract类设置虚拟函数表时,将拷贝来自Miranda- Interface 接口的inInterface到自己的虚拟函数表(下文介绍LinkClass时将见到),这和 在代码中主动为MirandaAbstract 类声明inInterface 函数是一样的效果。
Marker Interface
一般而言,Interface中会定义相关功能函数的,然后由实现类来实现。不过,Java库中也存在一类没有提供任何功能函数的接口类。这些接口类大家想必还很熟悉,比如下面列出的两个非常常见的接口类。
Cloneable
和Serializable
接口类中就没有定义任何函数。这样的接口也叫 Marker Interface,即起标记作用的接口。它只是说明实现者支持 Cloneable 或 Serializable,而实际的 Clone 或 Serialize 功能则是由其他函数来完成,比如下面的代码。
从 Object clone 函数可知,Marker Interface 确实只是个标记罢了。
另外,读者会好奇为什么ART不遗余力地要把类的所有virtual方法都组织到VTable中呢?要知道,这可是LinkMethods 中所调用的LinkVirtualMethods 函数的一个很主要的工作。这个问题我们可以反过来问,如果Class中没有保存这个VTable,会出现什么情况?举个例子:
-
假设我们要调用类A的wait方法(也就是Object 11个virtual方法中的某个wait方法)。搜索类A的methods_数组(读者还记得它吗?它保存了本类明确定义的所有方法),其中是没有wait方法。是的,因为类A不会直接定义这个方法,所以在类A中找不到它。
-
那么,我们就该沿着类A的派生关系或实现关系一路向上搜索它们的methods_数组了。显然,这个过程非常耗时,难以接受。
最后,回顾上文对ArtMethod成员变量的介绍可知,它有一个名为method_index_的成员变量,该参数非常重要,此处先简单总结它的取值如下:
-
如果这个ArtMethod对应的是一个static或direct 函数,则 method_index_是指向定义它的类的methods_中的索引。
-
如果这个ArtMethod是virtual函数,则method_index_是指向它的VTable中的索引。注 意,可能多个类的VTable都包含该ArtMethod对象(比如Object的那11个方法),所以要保证这个method_index_在不同VTable中都有相同的值,这也是LinkMethods中那三个函数比较复杂的原因。
如上所述,Class的大小除了包含sizeof(Class)之外,还包括IMTable、VTable(如果该类 是可实例化的话)所需空间以及静态变量的空间。如果要想知道一个Class中存储用于存储静态变量的位置时,可利用下面这个函数获取。
LinkFields 代码介绍
先来看ART虚拟机实现中,一个Java类以及这个类的实例分别需要多大的内存。如图8-13所示
图8-13展示了一个Java Class类对象以及这个类对应实例所需的内存大小。
-
左边是Java Class类对象所需内存大小。它由三部分组成,首先是sizeof(Class)。然后是(如果有的话)IMTable和VTable所需空间,最后是该类静态变量所需空间。注意,引用类型排在最前面,然后是long/double 类型、int/float类型、short/char类型,最后 是byte/boolean类型变量所需空间。
-
右边是某个Java类对应实例对象所需空间。它包含两部分,首先是父类对象的大小,紧接其后的是非静态成员变量所需空间。内存布局与静态成员变量在Class中的一样。
提示
在OOP中,我们经常会提及的一个知识点是类的成员函数和静态成员变量是类属性的,即它们归属于类的财产。而类中定义的非静态成员变量则属于该类对应实例对象的。这个知识点在图8-13所示的Java Class和Java Object的内存布局中得到了印证。
接着我们再回顾ArtField的一个重要成员变量,它的含义现在就可以解释清楚了