注意: 装载 和 加载 的区别:
- 装载,指的是.class文件加载到初始化的整个生命周期;
- 加载,指的是.class文件装载的第一个阶段
类装载机制
jvm把class文件加载到内存中,并对数据进行校验、解析和初始化,最终形成JVM可以直接使用的java类的全过程。
ClassLoader的装载流程图
类装载的前提条件
class只有使用的时候才会被装载,java虚拟机也不会无条件的装载class类型
装载流程
- 加载类
加载类处于类装载的第一个阶段,将class文件的字节码加载到内存中,并将静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。
该过程需要ClassLoader参与。
加载类,JVM必须完成:
- 通过类的全名,获取类的二进制数据流
- 解析类的二进制数据流为方法区内的数据结构
- 创建java.lang.Class类的实例,表示该类型
- 连接
将java类的二进制代码合并到JVM的运行状态中。这一步包含三个操作:
- 验证,确保加载的类信息符合JVM规范,没有安全方面的问题
- 准备 验证通过后,虚拟机就会进入准备阶段,在这个阶段,虚拟机会为这个类变量在方法区分配相应的内存空间,并设置初始值。下图为虚拟机为各种类型变量默认的初始值:
注意:
-
java并不支持boolean类型,对于boolean类型,内部实现是Int,由于int的默认值是0,故对应的boolean默认值是false。
-
此处进行内存分配的只是类变量(static修饰的变量),不包括实例变量(实例变量会在对象实例化时随着对象一起分配在java堆中)
-
解析,该阶段的任务就是将类、接口、字段和方法的符号引用转为直接引用
符号应用,就是一些字面量的引用,和虚拟机的内部数据结构和内存布局无关。
- 初始化类
初始化是类装载的最后一个阶段。如果前面的操作没有问题,表示类可以顺利装载到系统中。此时,类才会开始执行java字节码。
该阶段的重要工作,是执行类的初始化方法<clinit>, 为类变量赋予正确的值。方法<clinit>是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。
在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的<clinit>方法总是在子类<clinit>之前被调用。
- 初始化一个类包含两个步骤:
- 如果类存在超类,先初始化超类
- 如果类存在类初始化方法,就执行此方法
- 初始化一个接口只有一个步骤: 如果该接口存在接口初始化方法,就执行此方法,接口不初始化父接口。
注意:
- java编译器并不是为所有的类都产生一个<clinit>初始化方法,以下几种情况就没有: ① 类没有申明类变量,也没有任何静态初始化语句(static代码块); ② 类申明了类变量,但是没有任何的类变量初始化语句,也没有静态初始化语句进行初始化; ③ 类仅包含静态final变量的类变量初始化语句,而且是编译时候的常量
- 初始化类的过程必须保持同步,如果有多个线程初始化一个类,仅仅允许一个线程执行初始化,其他的线程都需要等待。。
类初始化
JVM规定:一个类或者接口在初次使用时,必须进行初始化。这里的“使用”,指的是”主动使用”,包括以下几种情况:
- 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化
- 当调用类的静态方法时,即当使用了字节码invoke static指令
- 当使用类或接口的静态字段时(final常量除外),即使用了getstatic或putstatic指令
- 当使用java.lang.reflect包中的方法反射类的方法时
- 当初始化子类时,必须先初始化父类
- 作为启动虚拟机、含有main方法的那个类
除了以上情况属于主动使用外,其他均属于被动使用,被动使用不会引起类的初始化。
主动使用示例
public class Parent {
static {
System.out.println("Parent init.");
}
}
public class Child extends Parent {
static {
System.out.println("Child init.");
}
}
public class InitMain {
public static void main(String[] args) {
Child child = new Child();
}
}
上述示例声明了三个类:Parent、Child(extends Parent)、InitMain。若Parent被初始化,static被执行将打印“Parent Init”;若Child被初始化,将会打印“Parent init”、"Child init"。执行InitMain,打印结果为:
Parent init.
Child init.
总结:
根据上述示例可知,系统首先加载Parent类,接着装载Child类。符合主动装载中的两个条件,使用new关键字创建类的实例会装载相关的类,以及在初始化子类时,必选先初始化父类。
被动装载示例
public class Parent {
static {
System.out.println("Parent init");
}
public static int v = 100;
}
public class Child extends Parent {
static {
System.out.println("Child init.");
}
}
public class InitMain {
public static void main(String[] args) {
System.out.println(Child.v);
}
}
说明:Parent类中定义了类变量v,在InitMain测试类中,使用子类Child调用父类中的类变量v。
执行结果:
Parent init
100
总结:
在InitMain测试类中,通过子类Child直接访问了Parent类中的static变量v,但是子类Child并未初始化,只有父类Parent完成初始化。所以,在引用一个字段时,只有直接定义该字段的类,才会被初始化。
注意:虽然子类Child没有被初始化,但是此时Child类已经被系统加载,只是没有进入到初始化阶段。
引用final常量
public class FinalFieldClass {
public static final String CONST_STR = "hello world";
static {
System.out.println("FinalFieldClass init");
}
}
public class FinalFieldTest {
public static void main(String[] args) {
System.out.println(FinalFieldClass.CONST_STR);
}
}
运行结果: hello world.
分析:FinalFiledClass类没有因为其常量字段CONST_STR被引用而初始化,这是因为在Class文件生成时,final常量由于其不变性,做了适当的优化。
总结:
编译后的FinalFieldClass.class中,并没有引用FinalFieldClass类,而是将其final常量直接存放在常量池中,因此FinalFiledClass类自然不会被加载。javac在编译时,将常量直接植入目标类,不再使用被引用类。
注意:并不是在代码中出现的类,就一定会被加载或者初始化,如果不符合主动使用的条件,类就不会被初始化。