虚拟机类加载机制——深入理解Java虚拟机

时间:2023-01-03 12:11:47

虚拟机类加载机制

一、类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型(Class对象)。这里的Class文件指的是一串二进制的字节流,并非一定要是存在于具体磁盘的文件。

二、类加载的时机

虚拟机类加载机制——深入理解Java虚拟机

1.类从被记载到虚拟机内存中开始,到卸载出内存,生命周期一共包括:加载、验证、准备、解析、初始化、使用、卸载这7个阶段。验证、准备、解析3个部分称为连接。

加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始(相互交叉地混合式进行),但解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

2.初始化的时机(对一个类进行主动引用,有且仅有这几种方式)

  • 遇到newgetstaticputstaticinvokestatic这4条字节码指令时,如果类没有经过初始化,则需要先触发其初始化。生成这4条指令最常见的场景是:使用new关键字实例化对象的时候、读取或者设置一个类的静态字段(final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用**反射**API的方法对类进行反射调用的时候,如果累没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果父类还没进行过初始化,则需要父类先进行初始化。
  • 当虚拟机启动时,用户需要制定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。
  • 动态语言支持

3.被动引用
(1)通过子类引用父类的静态字段,不会导致子类初始化,但会加载。
对于静态字段,只有直接定义这个字段的类才会被初始化,所以通过子类引用父类的静态字段,只会触发父类的初始化而不会触发子类的初始化。
(2)通过数组定义引用类,不会触发此类初始化。

public class NotInitialization{
public static void main(String[] args){
SuperClass[] sca = new SuperClass[10]; //不会初始化该类
}
}

(3)常量在编译阶段会存入调用类的常量池中,本质上不会直接引用定义该常量的类,因此不会触发定义常量的类的初始化。

public class ConstClass {
static {
System.out.println("ConstClass init");
}

public static final String HELLOWORLD = "hello world";
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD); //常量已经在编译阶段存到NotInitialization 类的常量池中,是对自身常量池的引用。
}
}

4.接口的加载与初始化
接口中不能用static{}代码块,但编译器仍然会为接口生成<clinit>()类构造器,用于初始化接口中定义的成员变量。
注意:当类在初始化时,要求其父类全部都已经初始化,但是接口在初始化时,并不要求父接口全部都完成了初始化,只有在真正用到父接口的时候,如引用接口中定义的常量,才会初始化。

三、类加载的过程

1.加载
(1)通过一个类的全限定名来获取定义此类的二进制字节流

1.(非数组类)这个过程可以由开发人员自己定义获取方式。既可以使用系统提供的引导类加载器,也可以使用用户自定义的类加载器去完成。开发人员可以通过自定义的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。
2.(数组类)由虚拟机直接创建,但数组类与类加载器仍然关系密切,其元素类型(去掉数组维度的类型)要靠类加载器取创建。

(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

方法区的数据存储结构由虚拟机自定义实现,虚拟机规范未规定此区域的具体数据结构。

(3)在内存中生成一个代表这个累的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

在内存中实例化一个java.lang.Class对象,并不一定是在Java堆中,对于HotSpot虚拟机,Class对象存放在方法区中,这个对象作为程序访问方法区中的这些数据类型的外部接口。

2.验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
(1)文件格式验证(基于字节流)
这个阶段要验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理。(P216) 主要目的是保证输入的字节流能够正确地解析并存储于方法区中,格式上符合描述一个Java类型信息的要求。
这个阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储,后面的3个验证阶段是基于方法区中的存储结构进行的,不会在直接操作字节流

(2)元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。(p217)
主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。

(3)字节码验证
主要目的是通过数据流和控制流分析,确保程序语义是合法的、符合逻辑的。这个阶段会对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。(p218)

(4)符号引用验证
这个阶段在解析阶段发生,在虚拟机中将符号引用转化为直接引用的时候进行校验。对类自身以外常量池中的各种符号引用的信息进行匹配性校验。
主要目的是确保解析动作能正常执行,如果无法通过符号引用验证,将抛出异常。

3.准备
这个阶段正式为类变量分配内存,并设置类变量初始值,这些变量所使用的内存都在方法区进行分配。(非堆)
(1)这个阶段进行内存分配的仅包含类变量(static修饰的变量),不包括实例变量,实例变量将会在对象实例化的时候随对象一起分配在Java堆中
(2)初始值指的是”通常情况”下是数据类型的零值

public static int value = 123;

value在准备阶段过后的初始值为0,不是123.在初始化阶段的时候,赋值为123。
(3)特殊情况
如果类字段的字段属性表存在ContantValue属性,那在准备阶段value变量会被初始化为ContantValue属性指定的值。

public static final int value = 123; //准备阶段虚拟机将value设置为123

4.解析
过程:虚拟机将常量池中的符号引用替换为直接引用。虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才被解析(操作符号引用的 字节码指令)。

(a)符号引用——以一组符号描述所引用的目标,与虚拟机实现的内存分布无关,引用的目标不一定已经加载到内存中。
(b)直接引用——直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。与虚拟机内存分布相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,引用的目标一定存在于内存中。

  • 类或接口的解析(CONSTANT_Class_info)
  • 字段解析(CONTANT_Fieldref_info)
  • 类方法解析(CONTANT_Methdoref_info)
  • 接口方法解析(CONTANT_InterfaceMethodref_info)

5.初始化
初始化是类加载阶段的最后一步。这个阶段,真正开始执行类中定义的Java代码(或者说是字节码)。
在初始化阶段,根据程序员通过程序制定的主观计划初始化类变量和其他资源。初始化是执行类构造器<clinit>()方法的过程。

<clinit>()注意点:

  • <clinit>()方法由编译器自动收集类中的所有类变量的复制动作和静态代码块(static{})中的语句合并产生,编译器收集的顺序是语句在源文件中出现的顺序决定的。
  • <clinit>()与类的构造函数不同,他不需要显式的调用父类构造器,虚拟机保证在子类的<clinit>()方法执行之前,父类的<clinit>()已经执行完毕。在虚拟机中第一个被执行的<clinit>()方法的类是java.lang.Object
  • 父类中定义的静态代码块优先于子类的变量赋值动作。
  • <clinit>() 不是必须的
  • 虚拟机保证一个类的<clinit>()方法在多线程中正确的加锁、同步。

四、类加载器

1.类与类加载器
对于任意一个类,需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间。所以,比较两个类是否相等,只有在这两个类由同一个类加载器加载的前提下才有意义,否则即使两个类来源于同一个Class文件,被同一个虚拟机加载,但只要加载他们的类加载器不同,那么这两个类就不相等。

2.双亲委派模型
(1)从Java虚拟机的角度看,Java虚拟机存在两种不同的类加载器。一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++编写,是虚拟机自身的一部分;另一种是所有其他的类加载器,由Java实现,独立于虚拟机,并且全部基础自java.lang.ClassLoader

(2)从开发人员角度,类加载器分为3类。
启动类加载器Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
扩展类加载器Extension ClassLoader):负责加载\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
应用程序类加载器Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

(3)双亲委派模型
虚拟机类加载机制——深入理解Java虚拟机

双亲委派模型除了顶层的启动类加载器外,其余类加载器都应当有自己的父类加载器。
工作过程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

(4)实现
(a)loadCLass()方法

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { //父类不为空,转为父类处理
c = parent.loadClass(name, false);
} else { //父类为空,说明是启动类加载器,由他加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父类抛出异常,说明父类无法完成加载
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); //父类无法加载,则自己去加载

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

1.首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
2.如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
3.如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载

(b)findClass()方法

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}