代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言的一大步
一、无关性的基石
实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定,它只与Class语言这种特定的二进制文件格式所关联。任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。
1.1 Class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,当遇到8位字节以上的数据项,则按照高位在前的方式分割成若干个8位进行存储。只有两种数据类型:无符号数和表
- 无符号数
以u1、u2、u4、u8分别代表1个字节,2、4、8个字节的无符号数,可以用来描述数字、索引引用、数量值、UTF-8字符串。 - 表
由多个无符号数或者其他表作为数据项构成的复合数据类型,习惯已"_info"结尾,整个Class文件本质上就是一张表
1.2 魔数与Class文件的版本
每个Class文件的头4个字节称为魔数,唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。固定为"0xCAFEBABE",咖啡宝贝。
紧接着魔数的4个字节存储的是Class文件的版本号:第五和第六个字节是次版本号(Minor Version),第七和第八个字节是主版本号(Major Version)。高版本JDK能向下兼容以前版本的Class文件,但拒绝执行以后的版本文件。
1.3 常量池
版本号之后便是常量池,包含两大类常量
- 字面量
比较接近Java语言层面的常量概念,文本字符串,final常量值 - 符号引用
属于编译原理的概念- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
由于Class方法等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以该常量的最大长度就是Java中方法、字段名的最大长度,就是该常量的Length的最大值,65535,就是64KB。
1.4 访问标志
常量池结束后,紧接着两个字节代表访问标志,表示是类还是接口,是否为public,abstract,final等。
二、虚拟机类加载机制
2.1 概述
虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
依赖运行期动态加载和动态连接
2.2 类加载的时机
类的整个生命周期包括:
其中验证、准备、解析3个部分统称为连接,加载必须按部就班,但是解析则不一定,可以在初始化之后再开始。虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化
- 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。Java场景是:使用new实例化对象、读取或设置一个类的静态字段,调用一个类的静态方法。
- 使用java.lang.reflect包的方法对类进行反射调用的时候。如果类没有进行过初始化,则需要先触发其初始化
- 初始化一个类的时候,如果父类还没有初始化,则先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类)虚拟机会先初始化这个主类
- 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
细节问题:
- 对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
- 通过数组来引用类,不会触发此类的初始化
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
- 接口与类真正有所区别的是,第三种,当一个类在初始化时,要求其父类全部都已经初始化化过了,但是一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父类接口的时候(如引用接口中定义的常量)才会初始化
2.3 类加载的过程
2.3.1 加载
“加载”是“类加载”过程的一个阶段
将类的二进制流加载存储在方法区中,加载尚未完成,连接阶段可能已经开始,但是开始时间保持着固定的先后顺序
2.3.2 验证
这一阶段的目的是保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
2.3.3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
这里进行内存分配的仅包括类变量(static修饰)
public static int value = 123
这里的初始值是0而不是123,把value赋值为123的 putstatic指令是程序被编译后,所以是初始化阶段才会执行。
2.3.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用:符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。
- 直接应用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
- 类或接口的解析
- 字段解析
- 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
- 否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口
- 否则,如果C不是java.langObject的话,将会按照继承关系从下往上递归搜索其父类
- 否则,查找失败,抛出Java.lang.NoSuchFieldError异常
如果查找过程中返回了引用,如果权限不对,将抛出java.lang.IllegalAccessError异常
实际应用中,如果一个同名字段同时出现在C的接口和父类中,编译器可能会拒绝编译
2.3.5 初始化
类初始化是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码
初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
- <clinit>()方法与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object
- 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
- <clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也就没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法
- 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程会执行<clinit>()方法,其他线程阻塞等待,如果<clinit>()方法耗时很长,可能会造成进程阻塞。
2.4 类加载器
“通过一个类的全限定名来获取描述此类的二进制字节流” 实现这个动作的代码模块称为 类加载器
在类层次划分、OSGI、热部署、代码加密大放异彩
对于任意一个类,都需要由它的类加载器和类本身一同确立在Java虚拟机中的唯一性。
2.4.1 双亲委派模型
启动类加载器(虚拟机) 负责 <JAVA_HOME>\lib目录
拓展类加载器 开发者可以直接使用<JAVA_HOME>\lib\ext目录
应用程序类加载器 开发者可以直接使用 负责用户类路径所指定的类库
工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个请求都是如此,因此所有请求首先都会传送到启动类加载器,只有当父类无法完成加载时,子加载器才会尝试自己加载。
2.4.2 破坏双亲委派模型
- 第一次 被破坏 JDK1.2之前,因为该模型在1.2之后才被引入,为了向前兼容,添加了新的protected方法findClass()
- 第二次 如果基础类要回调用户的代码 - JNDI服务,引入了线程上下文类加载器,默认是应用程序类加载器。这个加载器,父类加载器请求子类加载器完成类加载
- 第三次 OSGI环境下,类加载器不再是双亲委派树状结构,而是复杂的网状结构
- 以java*开头的类委派给父类加载器
- 委派列表名单内的类委派给父类架子啊器
- 将Import列表中的类委派给Export这个类的Bundle的类加载器
- 查找当前Bundle的ClassPath,使用自己的类加载器
- 查找类是否在自己的Fragment Bundle中,如果在,则委派给对应Bundle的类加载器
- 查找Dynamic import列表的Bundle
- 类查找失败