一、什么是虚拟机的类加载机制:
代码在编译后,就会生成java虚拟机能够识别的二进制字节流class文件,class文件中描述的各种信息,都需要加载到虚拟机之中才能运行和使用。
虚拟机把类的数据从class文件加载到内存,并对数据进行校检,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,就是类的加载机制。
类从加载到虚拟机内存开始,到卸载出内存结束,整个生命周期包括七个阶段:加载->验证->准备->解析->初始化->使用->卸载。如下图:
二、类加载全过程:
1、加载: “加载”是“类加载”过程的一个阶段,这阶段的虚拟机需要完成三件事情:
(1)通过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。对于Hotspot虚拟机而已,Class对象比较特殊,它虽然是对象,但是存放在方法区里面,作为程序访问方法区中的这些数据类型的外部接口。
2、验证:这一阶段是为了确保class文件的字节流包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。这个阶段分为4个校检动作:
(1)文件格式验证:验证字节流是否符合class文件格式的规范,并且能被当前版本的虚拟机处理,通过该阶段后,字节流会进入内存的方法区中进行储存。
(2)元数据验证:对字节码描述的信息进行语言分析,对类的元数据信息进行语义校验,确保其描述的信息符合java语言规范要求。
(3)字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法进行校验分析,保证类的方法在运行时不会做出危害虚拟机安全的事件。
(4)符号引用验证:对类自身以外的信息(常量池中各种符号引用)的信息进行校检,确保解析动作能正常执行(该动作发生在解析阶段中)
3、准备:正式为类变量分配内存空间并设置类变量初始值,这些类变量所使用的内存都将在方法区进行分配。这个阶段进行内存分配的仅包括类变量(static修饰),不包括实例变量,实例变量会在对象实例化时随对象一起分配在java堆。设置的初始值一般指数据类型的零值(特殊:static final)。
public static int value= 123 ; //变量value在准备阶段过后的初始值是0,不是123.
public static final int value = 123 ; //特殊情况:会生成ConstantValue属性,初始值是123.
3-1 final、static、static final修饰的字段赋值的区别:
(1)static修饰的字段在类加载过程中的准备阶段被初始化为0或null等默认值,而后在初始化阶段(触发类构造器)才会被赋予代码中设定的值,如果没有设定值,那么它的值就为默认值。
(2)final修饰的字段在运行时被初始化(可以直接赋值,也可以在实例构造器中()赋值),一旦赋值便不可更改。
(3)static final修饰的字段在Javac时生成ConstantValue属性,在类加载的准备阶段根据ConstantValue的值为该字段赋值,它没有默认值,必须显式地赋值,否则Javac时会报错。可以理解为在编译期即把结果放入了常量池中。
4、解析:解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、和调用限定符7类符号引用。
对同一符号引用进行多次解析请求是很常见的事情,除invokedynamic指令以外,虚拟机可以对第一次解析的结果进行缓存,从而避免解析动作重复进行。invokedynamic对应的引用称为“动态调用限定符”,必须等到程序实际运行到这条指定的时候,解析动作才能进行。因此,当碰到由前面的invokedynamic指令触发过的解析的符号引用时,并不意味着这个解析结果对其他的invokedynamic指令也同样生效。
(1)符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何字面量,只要使用时无歧义定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以不相同,但是他们能接受的符号引用必须都是一致的,符号引用的字面量形式明确地定义在java虚拟机规范的calss文件格式中。
(2)直接引用:直接引用是可以直接定位到目标的指针、相对偏移量或是一个能间接定位目标的句柄。直接引用是与虚拟机的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
5、初始化:初始化阶段才真正执行类中定义的java代码。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员指定的主观计划去初始化类变量和其他资源,初始化阶段是执行类构造器()方法的过程。
5-1 类的主动引用:
在初始化阶段,有且只有5种场景必须立即对类进行“初始化”,称为主动引用:
(1)遇到new、getstatic、putstatic、invokestatic这4条指定时。对应的场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已经在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候。
(2)使用java.lang.reflect包的方法对类进行发射调用的时候。
(3)当初始化一个类的时候,如果发现其父类还没进行初始化,则必须对父类进行初始化。(与接口的区别:接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候,才会初始化)
(4)当虚拟机启动时,用户指定的要执行的主类(包含main方法的类)。
(5)java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个句柄所对应的类没有进行过初始化,则需要触发其初始化。
5-2 类的被动引用:
除了主动引用,其他引用类的方式都不会触发初始化,称为被动引用:
(1)对于静态字段,只有直接定义这个字段的类才会被初始化,通过其子类来引用父类中定义的静态字段,只会触发其父类的初始化而不会触发子类的初始化。
//父类
public class SuperClass {
//静态变量value
public static int value =123456;
//静态块,父类初始化时会调用
static{
System.out.println("父类初始化!");
}
}
//子类
public class SubClass extends SuperClass{
//静态块,子类初始化时会调用
static{
System.out.println("子类初始化!");
}
}
//主类、测试类
public class InitTest {
public static void main(String[] args){
System.out.println(SubClass.value); //输出结果:父类初始化! 123456
}
}
(2)通过数组定义来引用类,不会触发此类的初始化。
//父类
public class SuperClass {
//静态变量value
public static int value = 123456;
//静态块,父类初始化时会调用
static{
System.out.println("父类初始化!");
}
}
//主类、测试类
public class InitTest {
public static void main(String[] args){
SuperClass[] test = new SuperClass[10]; //输出结果:没有任何输出结果
}
}
(3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
//定义常量的类
public class ConstClass {
static{
System.out.println(“常量类初始化!”);
}
public static final String HELLOWORLD = "hello world!";
}
//主类、测试类
public class InitTest {
public static void main(String[] args){
System.out.println(ConstClass.HELLOWORLD); //输出结果:hello world
}
}
5-3 ()方法的特点:
(1)()方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,收集的顺序是由语句在源文件中出现的顺序决定的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不可以访问。
public class Test {
static{
i=0; //给变量赋值可以正常编译通过
System.out.println(i); //编译器会提示非法向前引用
}
static int i=1;
}
(2)()方法与实例构造器()方法不同,它不需要显示调用父类构造器,虚拟机会保证子类的()方法执行之前,父类的()方法已经执行完毕,所以父类中定义的静态语句块要优先于子类的变量赋值操作,虚拟机中第一个被执行的()方法的类是java.lang.Object。
(3)()方法对于类或接口并不是必需的,如果一个类中没有静态语句块,也就没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。
(4)接口中不能使用静态语句块,仍然有变量初始化操作,因此仍然会生成()方法,与类不同的是,执行接口中的()方法不需要先执行父接口的()方法。只有父接口中定义的变量被使用是,才需要初始化父接口,同时,接口实现类在初始化时也不会执行接口的()方法。
(5)()方法在多线程环境中被正确的加锁、同步,多个线程同时去初始化一个类,只会有一个线程执行()方法,其他线程则需要阻塞等待,直到活动线程执行()方法完毕,活动线程执行完毕后,其他线程唤醒后被不会再次进入()方法,因为同一个类加载器下,一个类型只会被初始化一次。