《深入理解 Java 虚拟机 3》类加载机制与字节码执行引擎
类比到现实:小明想买一个玩具推土机,可他又不好意思直接张口。所以,发生了下面的对话。
小明去问他爸爸:爸爸你有挖土机吗?
爸爸说:没有哎
接着爸爸问爷爷:爸爸爸爸,你有挖土机吗?
爷爷说:没有哎
接着爷爷问太爷爷:爸爸爸爸,你有挖土机吗?
太爷爷说:我也没有。让重孙子去买一个吧。
结果小明就高高兴兴地自己去买了一个玩具挖土机。
问题来了:如果爷爷有一台挖土机怎么办?那小明只能玩爷爷那个了,不能自己去买了。类比到类加载机制里,就是如果某广父类能对此类进行加载,那应用程序类或自定义这些子类就不用自己加载了。
4、分类
启动类加载器是使用 C++实现的,是虚拟机自身的一部分。
其它类加载器是由 java 语言实现的,独立于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader。
5、好处
以 string 类为例,用户自己写了一个 string 类的实现,对此类进行加载时,只会委派给启动类加载器来对 JDK 中原本的 string 类进行加载,而自定义的 string 类永远不会被调用,这样保证了系统的安全。
二、什么时候进行类加载
只有以下 5 中方式必须立即对类进行加载
1、使用 new 实例化对象的时候;读取或配置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候;调用一个类的静态方法的时候。
2、使用 java.lang.reflect 包的方法对类进行反射调用的时候。如果类没有进行过初始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类还没进行过初始化,则需要先触发其父类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含 main 方法的类),虚拟机会先初始化这个主类。
三、类加载过程详述
类加载过程分为 5 步。大部分都是由虚拟机主导和控制的,除了以下两种情形:
在加载阶段
开发人员可以通过自定义类加载器参与
在初始化阶段
会执行开发人员的代码去初始化变量和其它资源
1、加载
虚拟机需要完成的事情:
(1)通过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存区生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
2、验证
验证的目的是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。
其分为 4 个步骤:文件格式验证,元数据验证,字节码验证,符号引用验证。
其中文件格式验证是直接对字节流进行操作的,其余 3 项是在方法区中进行的。
3、准备
此阶段时正式为类变量分配内存并设置类变量初始值的阶段。其是在方法区中进行分配。有两个注意点:
(1)此时只是对类变量(static 修饰的变量)进行内存分配,而不是对象变量。给对象分配内存是在对象实例化时,随着对象一起分配到 java 堆中。
(2)如果一个类变量没有被 final 修饰,则其初始值是数据类型的零值。比如 int 类型时 0,boolean 类型时 false。举个例子说明:
public static int value = 123;
在准备阶段过后的初始值为 0 而不是 123,因为这个时候尚未开始执行任何 java 方法,而把 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类加载器<clinit>()方法之中。所以把 value 赋值为 123 的动作将在初始化阶段才会执行。
public static final int value=123;
此时因为 final,所以在准备阶段 value 就已经被赋值为 123 了。
4、解析
解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。可对类或接口、字段、类方法、接口方法等进行解析。
(1)符号引用是什么
符号引用即使包含类的信息,方法名,方法参数等信息的字符串,它供实际使用时在该类的方法表中找到对应的方法。
(2)直接引用是什么
直接引用就是偏移量,通过偏移量可以直接在该类的内存区域中找到方法字节码的起始位置。
符号引用时告诉你此方法的一些特征,你需要通过这些特征去找寻对应的方法。
直接引用就是直接告诉你此方法在哪。
5、初始化
此阶段时用于初始化类变量和其它资源,是执行类构造器<clinit>()方法的过程,此时才真正开始执行勒种定义的 java 程序代码。
第八章:字节码执行引擎
===============
JVM 中的执行引擎在执行 java 代码的时候,一般有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。
一、栈帧
1、基本概念
(1)、定义
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它位于虚拟机栈里面。
(2)、作用
每个方法从调用开始到执行完成的过程中,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
(3)、特点
栈帧包括了局部变量表,操作栈等,到底是需要多大的局部变量表,多深的操作栈是在编译期确定的。因为一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响。
两个栈帧之间的数据共享。在概念模型中,两个栈帧完全独立,但是在虚拟机的实现里会做一些优化处理,令两个栈帧出现一部分重叠。这样在进行方法调用时,就可以共用一部分数据,无须进行额外的参数复制传递。
2、局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
//方法参数
max(int a,int b)
局部变量和类变量(用 static 修饰的变量)不同
//全局变量
int a;
//局部变量
void say(){
int b=0;
}
类变量有两次赋初始值的过程:准备阶段(赋予系统初始值)和初始化阶段(赋予程序定义的初始值)。所以即使初始化阶段没有为类变量赋值也没关系,它仍然有一个确定的初始值。
但局部变量不一样,如果定义了,但没赋初始值,是不能使用的。
3、操作栈
当一个方法刚刚开始执行的时候,这个方法的操作栈是空的,在方法的执行过程中,会有各种字节码指令往操作栈中写入和提取内容,也就是出栈、入栈操作。
例如,计算:
int a = 2+3;
当执行 iadd 指令时,会将 2 和 3 出栈并相加,然后将相加的结果 5 出栈。
4、动态链接
Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用分为两个部分:
(1)静态解析:在类加载阶段或第一次使用的时候就转化为直接引用。
(2)动态链接:在每一次运行期间转化为直接引用。
5、返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法:正常退出、异常退出。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。
(1)正常退出:调用者的 PC 计数器作为返回地址,栈帧中一般会保存这个计数器值。
(2)异常退出:返回地址是通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
二、方法调用
1、解析
对“编译器可知,运行期不可变”的方法进行调用称为解析。符合这种要求的方法主要包括:
(1)静态方法,用 static 修饰的方法
(2)私有方法,用 private 修饰的方法
2、分派
分派讲解了虚拟机如何确定正确的目标方法。分派分为静态分派和动态分派。讲解静动态分派之前,我们先看个多态的例子。
Human man=new Man();
在这段代码中,Human 为静态类型,其在编译期是可知的。Man 是实际类型,结果在运行期才可确定,编译期在编译程序的时候并不知道一个对象的实际类型时什么。
(1)静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。它的典型应用是重载。
public class StaticDispatch{
static abstract class Human{
}
static class Man extends Human{
}
static class Woman extends Human{
}
public void say(Human hum){
System.out.println("I am human");
}
public void say(Man hum){
System.out.println("I am man");
}
public void say(Woman hum){
System.out.println("I am woman");
}
public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.say(man);
sr.say(woman);
}
}
运行结果是:
I am human
I am human
为什么会产生这个结果呢?
因为编译器在重载时,是通过参数的静态类型而不是实际类型作为判断依据的。在编译阶段,javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以两个对 say()方法的调用实际为 sr.say(Human)。
(2)动态分派
在运行期根据实际类型确定方法执行版本的分派过程,它的典型应用是重写。
public class DynamicDispatch{
static abstract class Human{
protected abstract void say();
评论