1. 内存结构概述
- 如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢?
- 类加载器
- 执行引擎
2. 类加载器与类的加载过程
2.1 类加载器子系统的作用
- 在 Java 中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
- 类加载器的作用:将字节码文件加载到内存中并生成一个Class类型的对象。
- 类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识(CA FE BA BE)。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由执行引擎Execution Engine决定。
- 加载的类信息存放在一块称为方法区的内存空间。除了类的信息外,方法区中还会存放静态域信息和运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)。
- 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括7个阶段:加载、验证、准备、解析、初始化、使用、卸载 。
2.2 类加载器ClassLoader的角色
- class file存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
- class file加载到JVM中,被称为DNA元数据模板,放在方法区。
- 在.class文件 → JVM → 最终成为元数据模板(Car Class),此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
2.3 类的加载过程
public class HelloLoader {
public static void main(String[] args) {
System.out.println("我已经被加载啦!");
}
}
2.3.1 加载
- 所谓加载,简而言之就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象。
- 在加载类时,Java 虚拟机必须完成以下 3 件事情:
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(Java类模板)。
- 在堆内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。该Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象。
- 外部可以通过访问代表 Order 类的 Class 对象来获取 Order 的类数据结构。
- Class 类的构造方法是私有的,只有 JVM 能够创建。java.lang.Class 实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过 Class 类提供的接口,可以获得目标类所关联的.class 文件中具体的数据结构:方法、字段等信息。
- 加载 Class 文件的方式:
- 从本地文件系统中直接加载 class 后缀的文件;
- 通过网络获取;
- 从zip压缩包中读取,成为日后jar、war格式的基础;
- 运行时计算生成,使用最多的是:动态代理技术;
- 由其他文件生成,典型场景:JSP应用;
- 从专有数据库中提取.class文件,比较少见;
- 从加密文件中获取,典型的防Class文件被反编译的保护措施。
- 数组类的加载:数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建数组类(下述简称 A)的过程:
- 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组A的元素类型;
- JVM使用指定的元素类型和数组维度来创建新的数组类。
- 如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public。
2.3.2 链接
1. 验证
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。如果出现不合法的字节码文件,那么将会验证不通过。
- 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
- 文件格式校验:基于二进制字节流,验证是否以魔数 OxCAFEBABE 开头,主版本和副版本号是否在当前 Java 虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等。只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面的3个阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。格式验证会和加载阶段一起执行,格式验证之外的验证操作将会在方法区中进行。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。目的是保证不存在不符合 Java 语言规范的元数据信息。比如:是否所有的类都有父类的存在(在Java里,除了Object外,其他类都应该有父类);是否一些被定义为final的方法或者类被重写或继承了;非抽象类是否实现了所有抽象方法或者接口方法;是否存在不兼容的方法(比如方法的签名除了返回值不同,其他都一样,这种方法会让虚拟机无从下手调度;abstract情况下的方法,就不能是final的了)。
- 字节码验证:该阶段主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。比如:在字节码的执行过程中,是否会跳转到一条不存在的指令;函数的调用是否传递了正确类型的参数;变量的赋值是不是给了正确的数据类型。该过程只是尽可能地检査出可以预知的明显的问题,如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的。
- 符号引用验证:最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在链接的第三个阶段——解析阶段中发生。符号引用验证的目的是确保解析动作能正常执行。Class 文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,因此,在验证阶段,虚拟机就会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据,如果一个需要使用的类无法在系统中找到,则会抛出NoClassDefFoundError,如果一个方法无法被找到,则会抛出NoSuchMethodError。
- 可以通过安装IDEA的jclasslib Bytecode viewer插件,来查看我们的.class文件。安装完成后,我们编译生成一个.class文件后,点击IDEA上方的View即可显示我们安装的插件来查看字节码了。也可以使用Binary Viewer工具查看.class字节码文件。
2. 准备
- 当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机会为类变量(静态变量)分配内存并且设置该类变量的默认初始值,即零值。
- 这里不包含基本数据类型的字段用 static final 修饰的情况,因为 final 在编译的时候就会分配了,准备阶段会显式赋值;这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。在这个阶段并不会像初始化阶段中那样会有初始化块或者代码被执行。
public class HelloApp {
private static int a = 1; // 准备阶段:a = 0; 初始化阶段:a = 1;
public static void main(String[] args) {
System.out.println(a);
}
}
3. 解析
- 解析阶段将常量池内的类、接口、字段和方法的符号引用转换为直接引用。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
2.3.3 初始化
成员变量的赋值顺序:
- ① 默认初始化
- ② 显式初始化 / 代码块初始化
- ③ 构造器初始化
- ④ 对象.属性 = xxx
- 非静态代码块:随着对象的创建而执行,每创建一次对象就执行一次非静态代码块
- 静态代码块:随着类的加载而执行,且只执行一次
- 静态方法/类方法:随着类的加载而加载
- 静态属性/类属性:随着类的加载而加载
- 类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行 Java 字节码。(即:到了初始化阶段,才真正开始执行类中定义的 Java 程序代码,比如:静态代码块随着类的加载而执行)
- 初始化阶段就是执行类构造器方法<clinit>()的过程。此方法无需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。Java 编译器并不会为所有的类都产生<clinit>() 初始化方法,哪些类在编译为字节码后,字节码文件中将不会包含<clinit>() 方法呢?
- 一个类中并没有声明任何的类变量,也没有静态代码块时;
- 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时;
- 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式。
- 类构造器方法<clinit>() 中指令按语句在源文件中出现的顺序执行。
- <clinit>()不同于类的构造器。关联:类的构造器是虚拟机视角下的<init>()。
- 任何一个类在声明后,内部至少存在一个类的构造器,默认是空参构造器。
package com.atguigu.java;
/*
JVM的架构模型:
基于栈的指令集架构
重新编译:
Build -> Recompile 'StackTest.java'
字节码反编译过程:
D:\workspace_idea\jvm\out\production\chapter01\com\atguigu\java>javap -verbose -p StackTest.class > test.txt
*/
public class StackTest {
private static int num = 1;
static {
num = 2;
number = 20;
System.out.println(num);
// System.out.println(number); //报错:非法的前向引用 ---> Illegal forward reference
}
/*
链接阶段之准备阶段:number = 0;
初始化阶段:number = 20; ---> number = 10; //类构造器方法<clinit>()中指令按语句在源文件中出现的顺序执行。
*/
private static int number = 10;
public static void main(String[] args) {
System.out.println(StackTest.num); //2
System.out.println(StackTest.number); //10
}
}
- 在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的<clinit>() 总是在子类<clinit>() 之前被调用。也就是说,父类的 static 块优先级高于子类。口诀:由父及子,静态先行。
package com.atguigu.java;
public class <