《深入理解 Android ART 虚拟机》笔记

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_sizeinsns 数组:指令码数组的长度和指令码的内容。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.bsssection

  • .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所示。

在这里插入图片描述
在这里插入图片描述

接着来看 AndroidRuntimestart 函数。

在这里插入图片描述

上述代码中和启动ART虚拟机密切相关的两个重要函数如下所示。

  • JnilnvocationInit函数:它将加载ART虚拟机的核心动态库。
  • AndroidRuntimestartVm函数:在ART虚拟机对应的核心动态库加载到zyogte进程后,该函数将启动ART虚拟机。

JniInvocation Init 函数介绍

先来看JniInvocationInit函数,代码如下所示。

在这里插入图片描述

由上述代码可知,我们将从 libart.so 里将取出并保存三个函数的函数指针:

  • 这三个函数的代码位于 java_vm_ext.cc 中。
  • 第二个函数JNI_CreateJavaVM用于创建Java虚拟机,所以它是最关键的。

AndroidRuntime startVm 函数介绍

接着来看AndroidRuntimestartVm函数,代码如下所示。

在这里插入图片描述

如上述代码中的注释所言, JNI_CreateJavaVM 函数并非是Jnilnovcation Initlibart.so获取的那个JNI_CreateJavaVM函数。相反,它是直接在AndroidRuntime.cpp中定义的,其代码如下所示。

在这里插入图片描述

辗转多次,终于和 libart.so关联上了。马上来看libart.so中的这个JNI_CreateJavaVM函数,代码如下所示。

在这里插入图片描述

在上述libart.soJNI_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 提供了 MutexReadWriteMutexConditionVariable 等辅助类来实现互斥锁、条件变量等常用的同步操作。它们定义于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.dexclasses2.dexclasses3.dex等,其实就是dex文件打包到jarapk里了。以后我们统称它们为dex文件,而不必理会它们是单独的.dex文件还是jarapk包中的一项)进行编译处理后得到的(该过程借助dex2oat来完成)。jarapk中可包含多个dex项(即所谓的multidex),每一个jarapk中的所有dex文件在oat文件中对应都有一个OatDexFile项,OatDexFile项存储了一些信息,比如它所对应的dex文件的路径、dex文件的校验以及其他各种信息在oat文件中的位置(offset)等
  • OatDexFile区域之后的是DexFile区域。在生成oat文件时,jarapkclasses.dex 的(如果有多个话,则包含classes2.dexclasses3.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.opeertlsPtr.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 相关类

当我们使用newmalloc等内存分配方法创建一个对象时,得到的是该对象所在内存的地址,即指针。指针本身的长度根据CPU架构的不同导致是32位长或者是64位长。如果创建1万个对象的话,那么这一万个对象的指针本身所占据的内存空间就很可观了。如何减少指针本身所占据的内存空间呢?ART采用的办法很简单,就是将对象的指针转换成一个位图里的索引,位图里的每一位指向一个唯一的指针。来看图7-12的示例。

在这里插入图片描述

在图7-12中:

  • 中间框是一个有n个比特位的位图(如果按字节计算,则该位图长度为 n/8 字节长)。这个位图本身是一块内存,由基地址pbitmap表示。其上下还有两个方框,代表两块连续的内存,起始地址分别是pbase1pbase2
  • 先来看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.jarorg.apache.http.legacy.jarokhttp.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中最常见的两个类JavaVMJNIEnv。根据笔者在《深入理解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也是结构体。其中,JNIInvokeInterfaceAttachCurrentThreadGetEnv等成员变量的数据类型都是函数指针(为方便书写,图7-19中没有展示它们的参数)。
  • JavaVM结构体的第一个成员变量指向一个JNIInvokeInterface对象。
  • JavaVMExt是一个类,它从 JavaVM中派生。

提示
JNI 或 runtime 模块里往往通过一个JavaVM *类型的指针来引用一个JavaVM对象。通过上面的介绍可知,ART中JavaVM对象的真正数据类型是JavaVMExt

JNIEnvExt

JNIEnvExt 的思路和 JavaVMExt 类似,我们直接来看代码。

在这里插入图片描述

JNINativeInterface 和上节中提到的 JNIInvokeInterface 有些类似,都是包含了很多函数指针的结构体。

在这里插入图片描述

现在来看JNIEnvExt,它是JNIEnv的派生类。其创建是通过Create 函数来完成的。

在这里插入图片描述

我们重点了解下gJniNativeInterface的内容。

在这里插入图片描述

总结

了解上述JavaVMExtJNIEnvExt代码后,外界如果通过它们的基类JavaVMJNIEnv 来操作JNI相关接口时,我们就可以很方便地找到真实的函数实现在哪了。笔者总结如下:

  • 操作JavaVM相关接口时,其实现在java_vm_ext.cc文件的JIT类中。如果需要检查JNI的话,则先通过check_jni.ccCheckJIT类对应函数处理。最终还是会调用 JIT 类的相关函数。
  • 操作 JNIEnv 相关接口时,其实现在jni_internal.cc的JNI类中。同理,如果需要检查JNI的话,也通过check_jni.ccCheckJNI类对应函数先处理。

JNI里相关的数据结构和API都定义在头文件jni.h中,来看其中的内容。

在这里插入图片描述
在这里插入图片描述

总结上述的代码可知:

  • Java中基础数据类型在JNI层中都对应为native层中的某种基础数据类型。
  • Java中引用类型在JNI层中对应为_jobject (注意,带下划线)及派生类。但是JNI的使用者只能通过_jobject和它的派生类的指针类型(即 JNI 使用者只能使用jobjectjclassjstring 等不带下划线的数据类型)来间接引用这些对象的实例。不过,鉴于上述代码明确地将_jobject定义为一个没有任何成员变量和成员函数的类。可想而知,jobjectjclass 等的作用和 void*差不多。
  • Java类中的成员变量或成员函数在JNI中也有对应的类型。对比_jobject,读者可发现_jfieldID_jmethodID甚至都没有实际的定义。不过,由于JNI使用者只能通过指针类型(jfieldIDjmethodID都是指针)来操作,所以编译不会报错。不过这也说明jfieldIDjmethodID 的作用和 void* 一样

那么,这些“void*”背后到底是谁? Java虚拟机规范(笔者参考的是《Java VirtualMachine Specification》第 7 版)把这个问题的答案留给了各种虚拟机的实现。那么,ART 虚拟机是如何处理的呢?

先来看另外一组 ART 里常用的辅助类。

ScopedObjectAccess 等辅助类

图8-1所示为ScopedObjectAccess辅助类家族。

在这里插入图片描述

在图8-1所示类家族中:

  • ValueObject是一个没有任何成员,也不允许编译器自动创建构造函数的类。
  • ScopedObjectAccessAlreadyRunnable是关键类。它包含三个重要成员变量,Self_指向当前调用线程的线程对象(类型为Thread),env_指向当前线程的JNIEnvExt对象,而vm_则指向代表虚拟机的JavaVMExt对象。

我们只要看一下 ScopedObjectAccessAlreadyRunnable的代码,上节遗留下的问题将迎刃而解。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

上述代码非常清晰得展示了jfieldIDjmethodID以及jobject在ART虚拟机实现里所对应的具体数据类型。

  • jfieldID其实就是 ArtField *
  • jmethodID 其实就是 ArtMethod *
  • jobject指向一个 mirror Object 对象,但其具体是什么,需要再由mirror Object*向下转换为指定的类型。

最后,我们来看一个使用AddLocalReference的代码段,代码如下所示。

在这里插入图片描述

常用JNI函数介绍

FindClass

FindClassJNIEnv中的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的代码,如下所示。

在这里插入图片描述

结合JavaVMExtAddGlobalRef 代码可知:

  • 一个Java进程中只有一个JavaVMExt对象,代表虚拟机本身。
  • 一个JavaVMExt对象中有一个globals_成员变量,这个变量是一个容器,可存储进程中所创建的全局引用对象。
  • 每个全局引用对象添加到globals_容器后都会得到一个IndirectRef。这个值的类型虽然是指针类型(void *),但它的值和要保存的mirror Object对象的地址以及IndirectReferenceTable内部对元素管理的方法有关。外界需通过 IndirectReferenceTableGet 函数将一个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 对象。

JavaVMJNIEnvjni.h里定义的数据结构,里边包含的都是函数指针成员变量。所以,这两个数据结构有些类似 Java 中的 interface。不同虚拟机实现都会从它们派生出实际的实现类。在 ART 虚拟机中,JavaVMJNIEnv 创建的代码如下所示。

在这里插入图片描述

再来看 ART 中JNIEnv的创建

在这里插入图片描述

JNI 中引用型对象的管理

我们先回顾一下Native层和Java层里对象的创建和销毁的过程。

  • 以C++为例,Native层中要创建一个对象的话需使用new操作符以先分配内存,然后构造对象。如果不再使用这个对象,则需要通过delete操作符先析构这个对象,然后回收该对象所占的内存。
  • Java层中也通过new操作来构造一个对象。如果后续不再使用它,则可以显式地设置持有这个对象的变量的值为null(也可以不做这一步,而交由垃圾回收来扫描和标记该对象是否有被引用)。该对象所占的内存则在垃圾回收过程中被收回。

JNI层作为Java层和Native层之间相交互的中间层,它兼具Native层和Java层的某些特性,尤其在对引用对象的创建和回收上。

  • 和C++里的new操作符可以创建一个对象类似,JNI层可以利用JNI NewObject等函数创建一个Java意义的对象(引用型对象)。这个被New出来的对象是Local型的引用对象。
  • JNI层可通过DeleteLocalRef释放Local型的引用对象(等同于Java层中设置持有这 个对象的变量的值为null)。如果不调用DeleteLocalRef的话,根据JNI规范,Local 型对象在JNI函数返回后,也会由虚拟机根据垃圾回收的逻辑进行标记和回收。
  • 除了Local型对象外,JNI层借助JNI Global相关函数可以将一个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++类,即 ArtFieldArtMethod,如图7-23所示。

在这里插入图片描述

图7-23展示了ArtFieldArtMethod类,它们用于描述类的成员变量和成员函数的信息。其中:

  • declaring_class_成员变量指向声明该成员的类是谁。
  • access_flags_成员变量描述该成员的访问权限,比如是public还是private等。
  • ArtFieldfield_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

来看下ArtFieldGetName函数,如果了解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包含该类中所有staticprivate 函数以及构造函数,而 virtual_methods包含该类中除staticfinal以及构造函数之外的函数,并且不包括从父类继承的函数(如果本类没有重载它的话)。
  • static_fieldsinstance_fields 代表该类的静态成员以及非静态成员。

最后是代表类成员的encoded_field和 encoded_method结构体。其中:

  • field_idx_diff是索引值的偏移量,通过它能找到这个成员变量的变量名,数据类型,以及它所在类的类名。
  • method_idx_difffield_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库中也存在一类没有提供任何功能函数的接口类。这些接口类大家想必还很熟悉,比如下面列出的两个非常常见的接口类。

在这里插入图片描述

CloneableSerializable 接口类中就没有定义任何函数。这样的接口也叫 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的一个重要成员变量,它的含义现在就可以解释清楚了

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/221660.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flink Window中典型的增量聚合函数(ReduceFunction / AggregateFunction)

一、什么是增量聚合函数 在Flink Window中定义了窗口分配器&#xff0c;我们只是知道了数据属于哪个窗口&#xff0c;可以将数据收集起来了&#xff1b;至于收集起来到底要做什么&#xff0c;其实还完全没有头绪&#xff0c;这也就是窗口函数所需要做的事情。所以在窗口分配器…

计算机组成原理-ATT格式vsIntel格式

文章目录 AT&T格式 vs lntel格式 x86汇编语言是lntel格式&#xff0c;还有一种汇编语言格式是AT&T AT&T格式 vs lntel格式 lntel格式中取主存地址内容未指明长度默认为32位&#xff0c;对应下图中第四行右边的指令 百分号 美元符号 小括号 可用于计算机结构体数组…

竞赛保研 python+opencv+机器学习车牌识别

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于机器学习的车牌识别系统 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;4分工作量&#xff1a;4分创新点&#xff1a;3分 该项目较为新颖&#xff0c;适…

Amazon Q:对话智能赋能企业发展

授权说明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 亚马逊云科技开发者社区, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道 。 在最近举办的亚马逊云科技大会上&#xff0c;引人瞩目的消息是A…

斑马zebra目标检测数据集VOC+YOLO格式2300张

斑马是由四百万年前的原马进化出来的&#xff0c;最早出现的斑马可能是细纹斑马。有关史前马科动物的化石现存于美国爱达荷州克文的克文化石床国家博物馆。斑马的史前马为“克文马”&#xff08;美洲斑马或者克文斑马&#xff09;&#xff0c;学名为“Equussimplicidens”&…

​ 轻量应用服务器:亚马逊云科技打造全球领先的云计算解决方案

随着“第四次工业革命”的爆炸式发展&#xff0c;众多企业都将自己的业务与迅速发展的应用开发和网站建设领域高度绑定。而对于众多有上云需求的企业和个人用户来说&#xff0c;选择一款自己的服务器配置就成为了一项至关重要的任务。而随着需求端的不断扩大&#xff0c;云服务…

Nacos-NacosRule 负载均衡—设置集群使本地服务优先访问

userservice: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则 NacosRule 权重计算方法 目录 一、介绍 二、示例&#xff08;案例截图&#xff09; 三、总结 一、介绍 NacosRule是AlibabaNacos自己实现的一个负载均衡策略&…

《教育信息化论坛》期刊杂志论文发表投稿

《教育信息化论坛》由中原大地传媒股份有限公司主管&#xff0c;河南电子音像出版社、文心出版社主办&#xff0c;我刊立足于教育信息化、教育现代化科研&#xff0c;重点介绍国内外信息化、现代化教学手段、教学方式、教学传播研究的新成果和新观点&#xff0c;推广成功的国内…

【Spring教程28】Spring框架实战:从零开始学习SpringMVC 之 请求与请求参数详解

目录 1 设置请求映射路径1.1 环境准备 1.2 问题分析1.3 设置映射路径 2 请求参数2.1 环境准备2.2 参数传递2.2.1 GET发送单个参数2.2.2 GET发送多个参数2.2.3 GET请求中文乱码2.2.4 POST发送参数2.2.5 POST请求中文乱码 欢迎大家回到《Java教程之Spring30天快速入门》&#xff…

认识缓存,一文读懂Cookie,Session缓存机制。

&#x1f3c6;作者简介&#xff0c;普修罗双战士&#xff0c;一直追求不断学习和成长&#xff0c;在技术的道路上持续探索和实践。 &#x1f3c6;多年互联网行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f389;欢迎 &#x1f44d;点赞✍评论…

一、CM4树莓派系统烧录

操作系统&#xff08;Raspberry Pi OS&#xff09;应用程序 Raspberry Pi OS系统&#xff08;树莓派推荐系统&#xff09;&#xff1a;较小的内存占用、较高的易用性以及对浮点单元的支持 浮点单元&#xff1a;浮点运算单元&#xff08;FPU&#xff09;是处理器中专门进行浮点…

windows redis 允许远程访问配置

安装好windows版本的redis&#xff0c;会以服务方式启动&#xff0c;但是不能远程访问&#xff0c;这个时候需要修改配置。redis安装路径下会有2个配置文件&#xff0c;究竟需要怎么修改才能生效呢&#xff1f;看下图 这里的redis服务指定了是redis.windows-service.conf文件&…

Docker | 发布镜像到镜像仓库

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏:Docker系列 ✨特色专栏: MySQL学习 🥭本文内容:Docker | 发布镜像到镜像仓库 📚个人知识库: [Leo知识库]https://gaoziman.gitee.io/bl…

骨灰级程序员那些年曾经告诉我们的高效学习的态度

一、背景 以前阅读陈皓老师的左耳听风专栏中关于如何高效学习的总结让我收货颇丰&#xff0c;今天总结了一下&#xff0c;分享给大家 老师说&#xff1a; 学习是一件“逆人性”的事&#xff0c;就像锻炼身体一样&#xff0c;需要人持续付出&#xff0c;会让人感到痛苦&#…

c语言堆排序(详解)

堆排序 堆排序是一种基于二叉堆数据结构的排序算法&#xff0c;它的基本概念包括&#xff1a; 建立堆&#xff1a;将待排序的列表构建成一个二叉堆&#xff0c;即满足堆的性质的完全二叉树&#xff0c;可以是最大堆或最小堆。最大堆要求父节点的值大于等于其子节点的值&#x…

基于node 安装express后端脚手架

1.首先创建文件件 2.在文件夹内打开终端 npm init 3.安装express: npm install -g express-generator注意的地方&#xff1a;这个时候安装特别慢,最后导致不成功 解决方法&#xff1a;npm config set registry http://registry.npm.taobao.org/ 4.依次执行 npm install -g ex…

Qt之Ui样式表不影响子类的配置

Qt之Ui样式表不影响子类的配置 问题 在ui界面上布局时&#xff0c;当对容器进行样试设计时&#xff0c;会对容器内其它成员对象也进行了修改 分析 对应*.ui文件内容 从这个写法来看&#xff0c;它的样式属性会影响其成员对象样式属性。 解决方法 在容器的样式表中写时适…

如何将Galaxybase图数据库应用于电力设备管理

导读 近日&#xff0c;受强冷空气影响&#xff0c;部分北方地区出现不同程度的降雪&#xff0c;并持续降温。据国家电网发布的预警通知&#xff0c;要求启动预警响应和应急机制&#xff0c;密切跟踪灾害预警信息和应急响应情况&#xff0c;滚动研判分析覆冰、积雪、低温等对电…

beyond compare文件夹比较时候文本乱码问题解决

“格式”&#xff0c;在下面的左侧编码重写和右侧编码覆盖选择 GB2312/UTF-8/GBK&#xff0c;这个可以根据自己喜好和文本自身的encode选择。

跟着我学Python基础篇:08.集合和字典

往期文章 跟着我学Python基础篇&#xff1a;01.初露端倪 跟着我学Python基础篇&#xff1a;02.数字与字符串编程 跟着我学Python基础篇&#xff1a;03.选择结构 跟着我学Python基础篇&#xff1a;04.循环 跟着我学Python基础篇&#xff1a;05.函数 跟着我学Python基础篇&#…