JAVA类加载过程

79 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情

1.java类加载过程

重新回顾了java的类的生命周期,主要有:加载、链接、初始化、使用、卸载。上述过程包括了一个java类在jvm虚拟机中声明周期的全过程。 其中,加载、链接、初始化,称为类的加载过程。 而链接又包含了:验证、准备、解析等过程。见下图: 在这里插入图片描述

1.1加载

加载既是将class文件字节码加载到内存中,并将这些静态数据转换为jvm方法区运行时数据结构。在堆中生成一个代表这个类的java.lang.Class对象,作为方法区访问对象的入口。

1.2 链接

将已读入内存的二进制数据合并到JVM运行状态中去的过程。包含验证、准备、解析等过程。 验证: 1.类文件结构检查:确保加载的类信息符合JVM规范,遵从类文件结构的固定格式。 2.语义检查:确保类本身符合Java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖。注意,语义检查的错误在编译器编译阶段就会通不过,但是如果有程序员通过非编译的手段生成了类文件,其中有可能会含有语义错误,此时的语义检查主要是防止这种没有编译而生成的class文件引入的错误。 3.字节码验证: 确保字节码流可以被Java虚拟机安全地执行。 字节码流代表Java方法(包括静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。 字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。 4.二进制兼容性验证:确保相互引用的类之间的协调一致。例如,在Worker类的gotoWork()方法中会调用Car类的run()方法,Java虚拟机在验证Worker类时,会检查在方法区内是否存在Car类的run()方法,假如不存在(当Worker类和Car类的版本不兼容就会出现这种问题),就会抛出NoSuchMethodError错误。 准备: 正式为类变量(static变量)分配内存,并设置类变量初始值的阶段。这些内存都将在方法区分配。 解析: 虚拟机常量池内的符号引用替换为直接引用的过程。 例如在Worker类的gotoWork()方法中会引用Car类的run()方法。

public void gotoWork() { 
        car.run();// 这段代码在Worker类的二进制数据中表示为符号引用 
}

在Worker类的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。 在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区内的内存位置,这个指针就是直接引用

1.3 初始化

初始化是执行类的构造器()方法的过程。 类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先触发其父类的初始化。 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。 当访问一个java类的静态域时,只有真正声明这个域的类才会被初始化。

*说明 与方法

可能出现在class文件中的两种编译器产生的方法是:实例初始化方法(名为)和类与接口初始化方法(名为)。 这两个方法一个是虚拟机在装载一个类初始化的时候调用的(clinit)。另一个是在类实例化时调用的(init) 方法:所有的类变量初始化语句和类型的静态初始化语句都被Java编译器收集到了一起,放在一个特殊的方法中。这个方法就是 方法:是在一个类进行对象实例化时调用的。实例化一个类有四种途径:调用new操作符;调用Class或java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;通过java.io.ObjectInputStream类的getObject()方法反序列化。Java编译器会为它的每一个类都至少生成一个实例初始化方法。在Class文件中,被称为"" 区别:一个是用于初始化静态的类变量, 一个是初始化实例变量!

1.4 使用

使用既是所需要的对象开始被调用。

1.5 卸载

对象被jvm回收。

示例

package com.dhb.classload;

public class InitDemo {

	static {
		System.out.println("InitDemo static init ...");
	}

	public static void main(String[] args) {
		System.out.println("InitDemo main begin");
		InitA a = new InitA();
		System.out.println(InitA.width);
		InitA b = new InitA();

	}
}

class InitBase{

	static {
		System.out.println("InitBase static init ...");
	}
}

class InitA extends InitBase {

	public static int width = 60;

	static {
		System.out.println("InitA static init ...");
		width = 30;
	}

	public InitA() {
		System.out.println(" InitA init ... ");
	}

}

运行结果:

InitDemo static init ...
InitDemo main begin
InitBase static init ...
InitA static init ...
 InitA init ... 
30
 InitA init ... 

可以看到,在执行结果中,先运行main方法所在类的初始化方法,之后运行main函数。然后运行父类InitBase的初始化方法。之后运行InitA的静态初始化。以及InitA的构造函数。此后虽然new了多个InitA,但是其静态的初始化方法只运行了一次。