类文件结构
Class文件是一组以8位字节为基础的二进制流,中间没有添加任何分隔符。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以描述数字、索引引用、数量值或者按照utf-8编码构成的字符串值。
整个Class文件本质上就是一张表,如下:
PS:Android使用的并非class而是dex,dex其实可以是class的一个优化变种,让他更适合内存有限的移动设备。还有一点区别就是class文件是一个Java文件生成一个class文件,而dex则是把所有的class文件都进行合并,优化,然后生成一个最终的class.dex
PS:odex 即Optimized dex,即优化后的dex,用以提高Dalvik的运行速度。odex不是简单的、通用的优化,而是在不同手机、不同版本、不同手机OS上的优化,它的生成依赖于BOOTCLASSPATH提供的系统核心库。dex 利用 dexopt 得到 odex。
odex的三种生成方式:
preopt:即OEM厂商(比如手机厂商),在制作镜像的时候,就把那些需要放到镜像文件里的jar包,APK等预先生成对应的odex文件,然后再把classes.dex文件从jar包和APK中去掉以节省文件体积。
installd:当一个apk安装的时候,PackageManagerService会调用installd的服务,将apk中的class.dex进行处理。当然,这种情况下,APK中的class.dex不会被剔除。
dalvik VM:preopt是厂商的行为,可做可不做。如果没有做的话,dalvik VM在加载一个dex文件的时候,会先生成odex。所以,dalvik VM实际上用得是odex文件。以后我们研究dalvik VM的时候会看到这部分内容。
cp_info 常量池表 field_info 字段表 method_info 方法表 attribute_info 属性表
PS: 几个名词
全限定名: 描述路径,如com/android/test/TestClass;
简单名:字段的名称或方法的名称,如int a=0;简单名为a void getName(){} 简单名为getName
描述符:描述字段的数据类型 或 方法的参数列表 或 方法返回值
常量池表 cp_info
eg:13abc 表示:tag=1(CONSTANT_Utf8_info utf8编码字符串),length=3(字符串长度3),bytes=abc
字段表 field_info
访问标志
字段表
字段表集合中也有属性表
方法表 method_info
属性表 attribute_info
类加载机制
类生命周期
PS:
- 这几个阶段按顺序开始(解析可以在初始化之后),互相交叉混合运行。
- 与编译时需要进行连接工作的语言不同,Java中类型的加载、连接、初始化实在程序运行期间完成的。这种策略虽令类加载增加性能开销,但是大大提高了它的灵活性;Java冬天扩展的语言特性就是依赖于此。
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存(不一定是在Java堆中生成,HotSpot虚拟机的实现就是在方法区里生成的)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但仍有密切关系。
- 如果数组的组件类型是引用类型,那就递归采用类加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识(一个类必须与类加载器一起确定唯一性)
- 如果数组的组件类型不是引用类型(例如int[]),Java虚拟机将会把数组标记为与引用类加载器关联
- 数组类的可见性与它的组件类型的可见性一致。如非引用类型,则默认为public
验证
Class文件的来源可以多样化不一定是Java语言编译过来,可能存在安全等问题,验证阶段就是检验这个Class文件是否符合Java虚拟机的要求,是否会危害虚拟机自身的安全。
- 文件格式验证:主次版本号是否在合理范围,常量池中是否有不被支持的常量类型…
- 元数据验证:这个类是否有父类(除了Object其他都有父类),非抽象类是否实现了父类或接口中的方法…
- 字节码验证:保证跳转指令不会跳转到方法体以外的字节码指令上…
设置-Xverify:none 参数可以关闭大部分的类验证措施,以缩短虚拟机类加载的时间
准备
准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类变量初始值(内存置0阶段)的阶段
解析
解析阶段是虚拟机将方法区常量池的符号引用替换为直接引用的过程
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机内存分布无关,引用目标不一定加载到内存中;且各虚拟机内存实现各不相同,但它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中
- 直接引用:直接引用可以是指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机内存布局相关;如果有了直接引用,那引用目标必定已经在内存中存在。
解析动作主要针对类和接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符(后三种与动态语言息息相关)
- 类和接口C:
- 如果C不是数组类型,那虚拟机会把符号引用的全限定名传递给类加载器去加载C
- 如果C是数组类型,那虚拟机会让类加载器去加载相关类(Integer[] 则去加载Integer类),接着由虚拟机生成一个代表此数维度和元素的数组对象
- 如果以上无异常,此时C在虚拟机中已经是一个有效的类或接口了,后续还会确认代码所处的类对C的访问权限,如不具备则抛出java.lang.IllegalAccessError异常。
- 字段:解析字段主要对字段表内class_index项中索引的CONSTANT_Class_info(即字段所属的类或接口)符号引用进行解析。解析后会检验类或接口是否具备字段的访问权限,如不具备则抛出java.lang.IllegalAccessError异常。字段所属的类或接口C查找过程如下:
- 如C本身包含字段,则返回这个字段的引用,end
- 否则,如C中实现了接口则按照继承关系从下往上递归搜索各接口和其父接口,找到则返回引用,end
- 否则,如C不是java.lang.Object,则会按照继承关系从下往上递归搜索父类,找到则返回引用,end
- 否则,查找失败,抛出java.lang.NoSuchFieldError异常
- 类方法:解析类方法主要对类方法表的class_index项中索引的方法所属的类或接口的符号引用进行解析。后续还会确认类或接口是否具备此方法的访问权限,如不具备则抛出java.lang.IllegalAccessError异常。类方法所属类C查找过程如下:
- 类方法和接口方法符合引用的常量类型定义是分开的,如果发现class_index中索引的C是个接口抛出java.lang.IncompatibleClassChangeError异常
- 在类C中查找是否有简单名称和描述符都与目标匹配的方法,end
- 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标匹配的方法,end
- 否则,在类C实现的接口列表以及它们的父接口中递归查找是否有简单名称和描述符与目标匹配的方法,end
- 否则,查找失败,抛出java.lang.IllegalAccessError
- 接口方法解析:解析接口方法主要对接口方法表的class_index项中索引的方法所属的类或接口的符号引用进行解析。后续还会确认类或接口是否具备此方法的访问权限,如不具备则抛出java.lang.IllegalAccessError异常。类方法所属接口C查找过程如下:
- 与类方法解析不同,class_index中的索引C是个类而不是接口则抛出java.lang.IncompatibleClassChangeError异常
- 在接口C中查找是否有简单名称和描述符都与目标匹配的方法,end
- 否则,在接口C的父接口中递归查找是否有简单名称和描述符都与目标匹配的方法,end
- 否则,查找失败,抛出java.lang.IllegalAccessError
初始化
初始化阶段是执行类构造器< clinit>() 方法的过程。
< clinit>()介绍
- < clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
- < clinit>()和类构造函数(或者说实例构造器< init>())不同,< clinit>()不需要显式调用父类构造器,虚拟机会保证子类的< clinit>()方法执行前,父类的< clinit>()已经执行完毕。< init>()则需要我们显式的调用super方法才会执行父类的构造方法
- 由于父类的< clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
- < clinit>()对于类或接口来说并不是必有的,没有类变量和静态语句块的类编译器可以不为这个类生成< clinit>()方法
- 接口中不能使用静态语句块,但可以有变量初始化的赋值操作,所以会有< clinit>()方法生成。但接口的< clinit>()执行不需要先执行其父接口的< clinit>()
- 虚拟机会保证一个类的< clinit>()方法在多线程的环境下的安全性(被正确的加锁、同步,多线程去初始化一个类其他线程会阻塞,而且同一个类加载器下一个类只会初始化一次,这样第一个线程执行< clinit>()方法释放后其他线程也不会再次进去< clinit>())
此外,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问在它之前已经定义的变量,不能访问在它之后定义的变量但可以赋值
public class Test{
static {
i = 0;//正常通过,可以给在它之后的变量赋值
Log.i(TAG,i);//编译器提示"非法向前引用"
}
static int i = 1;//定义i
}
对象的创建过程、内存布局、访问定位
创建
Person person = new Person();
我们看下一个对象的大致创建过程:
- 在方法区常量池中查看是否有Person类的符号引用,若无,加载、解析、初始化Person类
- 在Java堆中分配所需内存(加载Person类后其所需内存大小便以可知)
- 初始化内存空间(内存置0,类变量如果没有赋值则会有一个默认初始值就是因为这一步)
- 初始化头信息(类原数据指针 + 哈希码 + GC分代年龄 + 锁状态 …)
- 对象初始化
PS:关于上述第2点钟分配内存有如下两个要点
内存分配方法:
1.内存规整,移动指针至空间足够的区域进行内存分配
2.内存不规整,这时候会有一个空闲内存表来记录内存使用情况,在表中寻找足够大的内存空间进行分配。何种分配方法取决于使用何种垃圾回收机制(即看所选择的垃圾回收机制是否具有压缩整理功能)
保证原子性:对象分配是非常频繁的,Java堆又是线程共享的,多线程情况下会有线程安全问题。解决方法有两种:
1.同步处理
2.堆中分配一块本地线程分配缓冲,每个线程在自己的缓冲区分配后再移动到堆内存中,移动过程也需要同步处理
对象的内存布局
上面说到对象的创建,创建完成后对象在Java堆中的内存分布如下:
- 对象头
1.1: 存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态…
1.2: 类型指针,不一定有(因为还有其他方法可以确认),指向方法区中的类信息,用以确认该对象是哪个类的实例 - 实例数据:自身定义的数据和父类继承下来的数据,默认会以类型为顺序一一分配。
- 对齐填充:不是必然存在(刚好填充满了就不需要了),无特殊含义,起占位符作用对齐填充:不是必然存在(刚好填充满了就不需要了),无特殊含义,起占位符作用
对象的访问定位(两种方式)
Java栈-本地变量表 存 Java堆-句柄池(对象实例数据指针+对象类型数据指针),句柄池指针指向堆实例地址和方法区类信息地址(稳定,堆实例经常移动只需改变句柄池地址 变量表无需更改)
Java栈-本地变量表 存 Java堆-实例地址,Java堆-实例地址 存 方法区类信息地址(减少一次指针定位的时间和内存)
字节码执行引擎
虚拟机是相对于物理机的一个概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和系统层面上;虚拟机的执行引擎则可以自行制定。Java虚拟机的执行引擎都是,输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
方法的调用(方法的选择)
回想一下Java虚拟机的内存模型,上面介绍的Java虚拟机的类加载过程,下面介绍方法的调用
方法调用阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法,非方法内部的具体运行过程)。一切的方法调用在Class文件里存储的都只是符号引用,而不是直接引用;而这个过程主要就是把符号引用替换为直接引用(符号引用和直接引用的概念上面类加载过程的解析阶段有讲)。而这个替换有一部分在类加载过程的解析阶段就已经完成,有的则需要在运行期间才能确定。
解析调用:解析调用一定是个静态的过程,在编译期间就完全确定,在类加载过程的解析阶段就会把涉及的符合引用全部替换为直接引用,这部分方法有静态方法、私有方法、实例构造器、父类方法。
分派调用:Java是静态多分派,动态单分派的语言
Human man = new Man();//Human为静态类型,Man为动态类型
- 静态分派:所有依赖静态类型来定位方法执行版本的分派动作都是静态分派,静态分派的典型应用是重载,静态分派发生在编译阶段。
eg:
public class StaticDispatch {
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
static abstract class Human { }
static class Man extends Human { }
static class Woman extends Human { }
public void sayHello(Human guy) {
System.out.println("hello, guy");
}
public void sayHello(Man guy) {
System.out.println("hello, man");
}
public void sayHello(Woman guy) {
System.out.println("hello, woman");
}
}
/*
输出
hello, guy
hello, guy
*/
ps:类加载过程解析阶段的方法如果有重载方法,重载方法的选择也是通过静态分派来完成的。
- 动态分派:运行期根据是积累下确定方法执行版本的分派过程称为动态分派。
eg:
public class DynamicDispatch {
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
}
/*
输出
man say hello
woman say hello
woman say hello
*/
方法的执行(基于栈的字节码解释执行引擎)
Java虚拟机的执行引擎在执行Java代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器JIT产生本地代码执行)两种选择,这里只讨论解释执行。
Java虚拟机的指令集是基于栈的,而Dalvik虚拟机则是基于寄存器的(x86也是基于寄存器的)。前者优点是可移植,后者优点速度快。
eg:
//1+1的执行过程
//1.基于栈的指令集
iconst_1
iconst_1
iadd
istore_0
//2.基于寄存器的指令集
mov eax,1
add eax,1
//结果保存在eax寄存器中
最后了解下基于栈的大致的执行过程
ps:这个过程只是一个大致的模型,实际相差甚大,因为会做一些优化操作。
备注:http://blog.csdn.net/xtayfjpk/article/category/2759219/1
类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性(即比较两个类只有在这两个类是同一个类加载器加载的情况下才有意义,否则比较结果必然为否。这里的比较包括Class对象的equals()、isAssignableFrom()、isInstance()、instanceof)
类加载器的种类:
- 启动类加载器Bootstrap ClassLoader,C++语言实现,虚拟机自身的一部分;主要加载核心类库(<JAVA_HOME>\lib目录下的rt.jar resources.jar charsets.jar 和class等)
- 其他类加载器,Java语言实现,独立于虚拟机外部
- 扩展类加载器Extension ClassLoader,由sun.misc.Launcher$ExtClassLoader实现,加载<JAVA_HOME>\lib\ext目录下的jar包和class文件
- 应用程序类加载器/系统类加载器App ClassLoader ,由sun.misc.Launcher$AppClassLoader实现,getSystemClassLoader()返回的就是这个类加载器,负责加载用户类路径上所指定的类库(即当前应用的claapath的所有类)。
- 自定义的类加载器
双亲委派:
由于不同的类加载器加载同一个Class文件是没有对比意义的,如果java.lang.Object类被不同的类加载器加载很多次,jvm中存在了多个不同的Object类,那么java类型体系中最基础的行为也无从保证,应用程序会一片混乱(we know,all the class extends from java.lang.Object,如果Object存在多个品种,那绝对是灾难)。为了解决这类问题Java设计者推荐给开发者一种类加载器的实现方式,即双亲委派模型
双亲委派模型的工作过程:类加载器之间以组合的方式来复用父类的加载器;当需要加载一个类时,当前类加载器会将它交给父类加载器来执行任务,如果父类完成不了再由自己完成。这样每个java.lang.Object无论是哪个类加载器加载的最终都是由启动类加载器来加载。
PS:
dex和class的区别:
- dvm执行的是.dex格式文件 jvm执行的是.class文件 android会把.class文件处理成.dex文件,然后把资源文件和.dex文件等打包成.apk文件。
- dvm是基于寄存器的虚拟机 而jvm执行是基于虚拟栈的虚拟机。寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。
- .class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度
Dalvik与Art(Android Runtime)的区别:
- Dalvik每次都要编译再运行,Art只有首次启动会编译
- Art占用空间比Dalvik大,安装更耗时。(空间换时间)
- Art减少编译次数,减少CPU使用频率,改善电池续航
- Art使得应用启动更快、运行更快、体验更流畅