我们知道的虚拟机有很多,运行Java的JVM虚拟机,运行Android程序的Davlik和Art虚拟机,运行C#的CLR虚拟机,那么什么是虚拟机呢,虚拟机的作用又是什么呢?运行JavaScript的v8引擎或者运行Python的引擎是否也是虚拟机呢?带着这几个问题,我们开始对虚拟机的学习。
虽然现在很多人都认为运行JavaScript的V8或运行Python的VirtualEnv,都不是虚拟机,而是解释器,主要原因是因为V8或者VirtualEnv不仅仅能执行字节码文件,还能将源文件编译成字节码文件,而传统上定义的虚拟机只是用来运行字节码文件的,如果将源文件编译成字节码,则需要编译器来帮忙,比如在JVM虚拟机上运行的文件都是已经编译成字节码的class文件,但是V8或者Python,都能一边编译源代码,一边执行编译后的字节码文件。但是现在这个规范已经越来越宽松了,也有不少大神认为V8或者VirtualEnv也是虚拟机。
那么一个虚拟机具备什么样的能力呢?我们下面就来具体看看吧。
- 将源码编译成字节码(编译器能力)
- 装载字节码文件(加载,链接,初始化)
- 内存管理
- 运行时内存区域
- 垃圾回收
- 指令解析和执行
接下来主要以JVM,Davlik和Art三款虚拟机为例,分别介绍上述的能力。
将源码编译成字节码
class字节码
java的字节码文件是通过java编译器来生成的,我们下载jdk后,通过javac命令,就可以将一个java源文件生成java字节码文件,这个字节码文件就可以直接在JVM上面运行了。
编译器通过对源代码进行词法,语法,语义分析,生成抽象语法树(AST),然后根据AST,生成目标文件。
词法,语法,语义这一流程不是java编译器独有的,是所有的编译器都共有的能力,不管是llvm编译c文件,或者是我们解析如html,xml等dsl文件,都是这样的步骤。解析完成后的字节码文件如下。
我简单介绍一下class字节码文件的内容结构
- Header:文件头包含了magic(魔数)——“验证是否是class格式文件”;minor_version,major_version——“该class文件支持的版本等数据信息”
- Constant Pool:常量池包含了类中所有的资源信息,如字面量常量——”字符串,被final修饰的常量等“;符号引用——”类和接口的全限定(绝对路径)名;字段的名称和描述符;方法的名称和描述符“
- Access Flag:类访问标志在常量池后面,标识类和接口的访问信息,如该Class文件是类还是接口,是否为public,是否为abstract等
- Class :类索引,包含当前类的索引(this_class)父类索引(super_class),接口索引(interfaces),通过这个索引,我们可以去常量池找这个类的全限定描述符
- fields:字段表集合,记录了类中每个变量的变量名, 变量类型, 访问标识, 属性等
- method:方法表集合,方法表和字段表的结构比较类似,包含了访问标识,名称索引,描述符索引,属性表索引等信息
- attributes:属性表,属性表非常庞大,包含方法的字节码指令,方法表里面的属性表索引就是指向该方法的字节码指令,常量值,方法抛出的异常等数据
Dex字节码
说完了class字节码,接下来对比说一下Dex字节码文件,我们知道class字节码文件只能在JVM上面运行,无法在Android虚拟机上运行,只有dex文件才能在Android虚拟机上运行,那么dex文件又是什么呢?它和class文件的区别是什么呢?
Android项目通过gradle构建生成apk文件,apk文件就是Android的安装包,安装包主要由dex文件,so文件,资源文件,manifest文件组成,如果有使用kotlin的话,apk包里面还会有kotlin的编译产物。
我这里只讲dex文件,Android的编译器会将java文件编译成dex,编译流程如下:
SourceCode(.java) — javac → Java Bytecode(.class) — Proguard → Optimized Java bytecode(.class) — Dex → Dalvik Optimized Bytecode(.dex)
从上面的流程看到,编译器第一步同样是将java文件转换成了class字节码文件,之后便是Android编译器所特有的部分:
- Proguard流程会对字节码文件进行压缩,优化和混淆,我们可以在gradle中开启配置proguradFiles的规则来开启我们的Proguard流程
- 当Proguard优化字节码文件后,dx编译器(AndroidStudio3.0之后开始采用D8编译器)会将优化后的字节码文件生成dex文件。
java8中引入了lambda等一些语法糖新特性,所以为了兼容这些语法糖,Android编译器在编译的途中会经历拖糖的操作,在Android Gradle Plugin3.1版本之前是用的第三方的插件进行脱糖操作,将所有的流程串起来,它的步骤如下图:
我们接着看一下dex文件的文件结构
- Header:dex文件的头文件同样包含了magic魔数,用来标识是否是dex文件,还包含了checksum和signature等文件校验和签名信息码,file_size,header_size文件和头大小以及其他数据的大小等信息等等
- String_ids:字符串偏移数组,表示每个字符串在 data 区的偏移量,根据偏移量在Data区拿到数据
- Type_ids:数据类型索引,表示所有引用的数据类型在字符串中的索引
- Protos_ids:方法声明索引
- Fields:记录了所属类,类型以及方法名
- Methods:方法表
- Classes:类信息索引,记录了类信息,包括接口,超类,类数据偏移量
- Data:数据区,保存了dex文件中所有类的数据
dex的文件和class文件存放的数据是一样的,只是结构会有些不一致,而且dex文件是多个class文件的集合,所有会有数据去重,重排列等优化处理处理。
我们接着来看看虚拟机的第二个能力,如何装载上面的字节码文件
装载字节码文件
class字节码文件
java编译器将源文件编译成class字节码文件后,jvm就直接可以运行了,但想要运行,首先要将这个字节码文件加载进内存,jvm通过ClassLoader来加载指定路径的字节码文件,字节码的文件可以通过网络下载,也可以通过本地读取。我们看一下ClassLoader类加载class的实现。
//java.lang.ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//查找.class是否被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();