深入理解JVM之虚拟机执行子系统

类文件结构、ClassLoader

Posted by Wanglizhi on July 12, 2016

第六章 类文件结构

Java一次编写,到处运行,跨平台以及JVM上支持多种语言,其基础都是虚拟机和字节码的存储格式,统一编译为Class文件。

Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,当遇到需要占用8位字节以上空间的数据项时,则按照高位在前分割成若干8位字节存储。根据JVM规范,Class文件格式采用类似C语言结构体的伪结构,只有两种数据类型:无符号数和表。

u1、u2、u4、u8分别代表1、2、4、8个字节的无符号数,可以用来描述数字、索引引用、数量值;表是由多个无符号数或其他表作为数据项构成的符合结构的数据。整个Class文件本质上就是一张表。

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,通常会使用一个前置的容量计数器加若干个连续的数据项的形式,称为某一类型的集合。

1、魔数与Class文件的版本

u4 magic 1 :魔数即文件类型标识,如图片gif或jpg文件头都存在魔数,因为文件扩展名可以很随意地改动。Class文件魔数颇具浪漫气息,值为:0xCAFEBABE(咖啡宝贝?)

u2 minor_version 1 :次版本号

u2 major_version 1 :主版本号,Java的版本号从45开始,JDK1.1后大版本发布主版本号加1,高版本JDK能向下兼容以前版本的Class文件,但不能运行以后的Class文件。JDK1.7的主版本号为51

2、常量池(constant_pool)

常量池是Class文件结构中与其他项目关联最多的数据类型。常量池之中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。

3、访问标志(access_flags)

这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话是否被声明为final等等。access_flasg一种32个标志位可以使用,当前只定义了其中8个,没有使用到的一律为0.

4、类索引(this_class)、父类索引(super_class)与接口索引集合(interfaces)

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,除了Object类所有Java类都有父类,父类索引都不为0。接口索引集合即implements语句后的接口从左到右依次排列。

5、字段表集合(field_info)

用于描述接口或类中声明的变量。字段(field)包括了类级变量或实例级变量,但不包括方法内声明的变量,字段表内的信息有:字段的作用域(public、private、protected)、是否static修饰、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。

6、方法表集合(methods)

方法表的结构如同字段表一样,一次包括了访问标志(access_flag)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)等。方法里的Java代码,经过编译后,存放在方法属性表集合中一个名为“code”的属性里。

7、属性表集合(attributes)

在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。属性表集合不在要求严格顺序,并且可以自定义属性信息,JVM规范预定义了9项虚拟机实现应当能识别的属性。

  • Code:方法表,Java代码编译成的字节码指令。
  • ConstantValue:字段表,final关键字定义的常量值
  • Deprecated:类、方法表、字段表,被声明为deprecated的方法和字段
  • Exceptions:方法表,方法可能抛出的异常(throws后的异常)
  • InnerClasses:类文件,内部类列表
  • LineNumberTable:Code属性,Java源码的行号与字节码指令的对应关系
  • LocalVariableTable:Code属性,方法的局部变量描述
  • SourceFile:类文件,源文件名称
  • Synthetic:类、方法、字段表,标识方法或字段为编译器自动生成的。

Class文件结构的发展

自Java虚拟机规范第一版Class文件结构已有十多年的历史,一直相对稳定,主体结构几乎没有发生过变化,对Class文件格式的改进都集中在向访问标志、属性表这些可以扩展的数据结构中添加内容。

第七章 虚拟机类加载机制

类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

JVM规范严格规定了有且只有四种情况必须立即对类进行“初始化”(而加载、验证、准备 自然在此之前开始)

  • 遇到new、get static、putstatic或invokestatic这4条字节码指令时,如果类没有初始化,则先初始化。生成这4条指令最长见的Java代码场景是:使用new实例化对象时、读取或设置一个类的静态字段(被final修饰、已在编译期放入常量池的静态字段除外)时、调用一个类的静态方法时
  • 使用java.lang.reflect包的方法对类进行反射调用时
  • 当初始化一个类时,如果发现其父类还没有初始化,则需要先触发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类

接口的加载过程与类加载过程有些不同,接口也有初始化过程,但是并不要求其父类接口全部都完成了初始化,只有在真正使用到父类接口的时候才会初始化。

类加载的过程

1、加载

在加载阶段,虚拟机需要完成三件事情:(1)通过一个类的全限定名来获取定义此类的二进制字节流(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

二进制流获取方式是开放的:

  • 从ZIP包中读取,JAR、EAR、WAR格式的基础
  • 从网络中获取,Applet
  • 运行时计算生成,动态代理技术,在java.lang.reflect.Proxy中,用ProxyGenerator.generateProxyClass来为特定接口生成Proxy代理类的二进制字节流
  • 由其他文件生成,JSP应用
  • 从数据库读取,代码在集群间分发

2、验证

验证是连接的第一步,目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。如果不符合Class文件存储格式,就抛出一个java.lang.VerifyError异常或子类异常。验证大致分为以下四个阶段:

  • 文件格式验证:是否一个魔数0xCAFEBABE开头、主次版本号是否在虚拟机处理范围内、常量池是否有不被支持的常量类型等等
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范要求,如是否有父类、是否继承了不允许被继承的类(final类)、如果类不是抽象类,是否实现了父类或接口中的所有方法、类中的字段方法是否与父类产生矛盾
  • 字节码验证:验证过程最复杂的一个阶段,主要进行数据流和控制流分析。如:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作;保证跳转指令不会跳刀方法体以外的字节码指令;保证方法体中的类型转换是有效的……
  • 符号引用验证:可以看做是对类自身以外的信息进行匹配性的校验,通常包括以下内容:符号引用中通过字符串描述的全限定名能否找到对应的类;指定类是否存在符合方法的字段描述符;类、字段和方法的访问性是否可被当前类访问

3、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注意!一是仅包含类变量,实例变量将随对象一起分配在Java堆中;二是初始值通常是类型的零值

public static int value = 123;

准备阶段后初始值是0,而不是123,把value赋值为123是在初始化阶段执行的。

public static final int value = 123;

如果类字段属性表中存在ConstantValue属性,那在准备阶段value就会被初始化为ConstantValue属性指定的值,所以准备阶段后value值为123

4、解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用(Symbolic Reference)可以是任何形式的字面量,只要能无歧义地定位到目标即可,引用目标不一定已经加载到内存;直接引用(Direct Reference)可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,如果有了直接引用,那么引用目标必定已经在内存中存在。

解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

5、初始化

初始化阶段是执行类构造器< clinit >()方法的过程。

  • < clinit >()方法是由编译器自动收集类中所有类变量的复制动作和静态语句块中的语句合并产生的,顺序由源文件出现顺序决定
  • < clinit >()方法与类构造函数不同,不需要显式调用父类构造器,虚拟机保证子类的< clinit >()方法执行前父类的< clinit >()方法已经执行完毕
  • 由于父类的< clinit >()方法先执行,意味着父类中的静态语句块优先于子类的变量赋值操作
  • < clinit >()方法对于类或接口并不是必须的,如果没有静态语句块或对变量赋值的话,编译器可以不生成该方法
  • 接口中不能使用静态语句块,但仍有变量初始化赋值,所以也会生成< clinit >()方法,当时与类不同的是,接口的< clinit >()方法不需要先执行父接口的该方法
  • 虚拟机保证一个类的< clinit >()方法在多线程环境中被正确地加锁和同步。多个线程同时初始化一个类,可能会进程阻塞。

类加载器

类加载器就是通过一个类的全限定名来获取描述此类的二进制字节流这个动作的模块。类加载器在类层次划分、OSGi、热部署、代码加密等领域大放异彩。

1、类与类加载器

比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提之下才有意义,包括类的Class对象的equals方法、isAssignableFrom()方法、isInstance()方法。

2、双亲委派模型

类加载器划分:

  • 启动器加载器(Bootstrap ClassLoader):这个类加载器使用C++语言实现负责将< JAVA_HOME >/lib目录中的或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
  • 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载< JAVA_HOME >\lib\ext目录中的,或被java.ext.dirs系统变量指定路径中的所有类库,开发者可用直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader来实现,这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以称它为系统类加载器,如果应用程序没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。其工作过程是:如果一个类加载器收到了类加载的请求,它首先把请求委派给父类加载器去完成,每一个层次的类加载器都是如此;只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

好处:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载。

3、破坏双亲委派模型

双亲委派模型主要出现过三次较大规模的“被破坏”情况。

  • 第一次:JDK1.2引入双亲委派模型时做出妥协,向前兼容
  • 第二次:JNDI服务,它的代码有启动类加载器去加载,但JNDI需要调用ClassPath下的代码,启动类加载器可能不认识。解决:加入线程上下文类加载器,JNDI使用这个线程上下文类加载器加载所需要的SPI代码。
  • 第三次:程序动态性。OSGi实现模块化热部署,每一个程序模块都有一个自己的类加载器,当需要更换一个Bundle时,就把这个Bundle连同类加载器一起换掉以实现代码热替换。

第八章 虚拟机字节码执行引擎

物理机的执行引擎是直接建立在处理器、硬件、指令和操作系统层面上的;而虚拟机的执行引擎则是由自己实现的,可以自行制定指令集与执行引擎的体系结构,并且能够执行那些不被硬件直接支持的指令集格式。

运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。栈帧需要多大的局部变量表、多深的操作数栈都已经完全确定了,并写入到方法表的Code属性中,在编译程序时确定。

1、局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以Slot为最小单位,一个Slot可以存放一个32位以内的数据类型,包括八种:boolean、byte、char、short、int、float、reference和returnAddress。reference是对象的引用,returnAddress是指向了一条字节码指令的地址。对于64位数据类型,虚拟机会以高位在前的方式分配两个连续的Slot空间,如long和double,由于局部变量表建立在线程的堆栈上,是线程私有的数据,如论读写两个连续的Slot是否是原子操作,都不会引起数据安全问题。

局部变量不存在“准备阶段”,如果局部变量定义了但没赋值是不能使用的。

2、操作数栈

例如在做算术运算时是通过操作数栈进行的,在调用其他方法的时候也是通过操作数栈进行参数传递的。

3、动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

4、方法返回地址

方法退出有两种方式:正常完成和异常完成。方法退出的过程实际就是把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

5、附加信息

方法调用

1、解析

将常量池中的符号引用转化为直接引用,与之对应的JVM提供的四条方法调用字节码指令:

  • invokestatic:调用静态方法
  • invoke special:调用实例构造器方法、私有方法和父类方法
  • invoke virtual:调用所有的虚方法
  • invokeinterface:嗲用接口方法,会在运行时再确定一个实现此接口的对象。

2、分派

  • 静态分派:所有依赖静态类型来定位方法执行版本的分派动作都成为静态分派。最典型的就是方法重载。静态分派发生在编译阶段。
  • 动态分派:在运行期根据实际类型确定方法执行版本的分派过程。典型的就是方法重写
  • 单分派与多分派:方法的接收者和方法的参数成为方法的宗量,单分派是根据一个宗量对目标方法进行选择,多分派则是根据多个宗量对目标方法进行选择。

基于栈的字节码解释执行引擎

1、解释执行

Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程,这部分动作在JVM之外执行的,而解释器在虚拟机内部,所以Java程序的编译就是半独立的实现。

2、基于栈的指令集与基于寄存器的指令集

Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流里面的指令大部分都是零地址指令,它们依赖操作数栈进行工作。

基于栈的指令集最主要的优点是可移植性;缺点是执行速度稍慢。

3、基于栈的解释器执行过程

第九章 类加载及执行子系统的案例与实战

字节码生成技术与动态代理的实现

IHello hello = (IHello) new DynamicProxy().bind(new Hello());

这个方法返回一个实现了IHello的接口,并且代理了new Hello()实例行为的对象。跟踪这个方法源码,可以看到程序进行了验证、优化、缓存、同步、生成字节码和显式类加载等操作,最后在bind()方法调用了sun.misc.ProxyGenerator.generateProxyClass()方法完成生成字节码的动作,产生一个描述代理类的字节码byte[]数组。这个代理类代码也很简单,它为传入接口中的每一个方法,以及从Object继承的equals、hashCode、toString方法都生成了对应的实现,并且统一调用了InvocationHandler对象的invoke()方法。各个方法的区别不过是传入的参数和Method对象有所不同而已

!!!重要,注意理解:所以,无论调用动态代理的哪一个方法,实际上都是在执行InvocationHandler.invoke()中的代理逻辑。

第十章 早期(编译期)优化

Javac编译器

1、解析与填充符号表(词法、语法分析,填充符号表)

2、注解处理器,JDK1.5之后,Javac提供了对Annotation的支持

3、语义分析与字节码生成:标注检查、数据控制流分析、解语法糖、字节码生成

Java语法糖的味道

语法糖可以看做编译器实现的一些“小把戏”

1、泛型与类型擦除

Java语言的泛型只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原生类型了,并且在相应的地方插入了强制转型代码。

2、自动装箱、拆箱与遍历循环

3、条件编译

根据布尔常量值的真假,编译器会把分支中不成立的代码块消除掉。

第十一章 运行期优化 略

参考:《深入理解Java虚拟机》