一、类加载概念
类的加载,指的就是将类对应的.class二进制字节码文件加载到我们内存当中,将其放在运行时数据区的方法区中,同时会在堆中创建一个代表这个类的Class对象,用来封装这个类在方法区内的数据结构,并且对外提供了访问方法区内的数据结构的接口,外部可以通过Class对象来访问该类。
二、类加载过程
如上图,就是一个类加载的全过程,其中包括:加载、链接(验证、准备、解析)、初始化、使用、卸载等阶段。注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。下面分别总结一下每个阶段都做了哪些事情:
【a】加载:查找并加载该类对应的.class二进制字节码文件。
在加载阶段,一般要完成下面三个事情:
(1) 通过该类的全限定名(即包名+类名)找出编译之后对应的.class字节码文件;
(2) 将字节码中的静态数据转换为方法区运行时的动态数据;
(3) 在堆内存中生成该类对应的Class对象,可以通过该Class对象访问方法区动态运行时数据(反射)。
【b】链接
(1)链接 - 验证:验证加载的类的信息是否正确,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要验证的动作有:文件格式验证、元数据验证、字节码验证、符号引用验证
(2)链接 - 准备:为类变量分配内存,并将其初始化为默认值。注意点:
- 该阶段进行内存分配的仅包括类变量(static修饰的变量),而不包括实例变量(new 出来的对象)。
- 初始化为默认值所赋值的是数据类型对应的默认值,如int赋值为0,float赋值为0L,boolean赋值为false等等。如下代码:
public static int a = 3;
public static long b = 5.0l;
public static boolean c= true;
经过准备阶段后,a被赋予的值是0,而不是3(注意3是在初始化阶段赋值的), b被赋予的值是0L,c被赋予的值是false.
仔细观察下面代码:如果类字段的字段属性同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
public static final int value = 3;
经过准备阶段后,value就被赋值为3。
(3)链接 - 解析:将常量池中的符号引用(抽象)变为直接引用(具体)。
【c】初始化:初始化阶段是执行类构造器(clinit)方法的过程,类构造器主要是由编译器自动收集类变量的赋值动作和static静态代码块中的语句合并生成的。主要是为类变量赋予真正的初始值,如下代码:
public static int a = 3;
public static boolean b = true;
在初始化阶段,a就会被初始化为3,b会被初始化为true。
看下面代码:
public class JVM01 {
public static void main(String[] args) {
Cat cat = new Cat();
System.out.println(Cat.value);
}
}
class Cat {
static int value = 100;
static {
System.out.println("静态初始化类Cat...");
value = 200;
}
public Cat() {
System.out.println("创建Cat类对象...");
}
}
从输出结果可以看到,首先执行Cat类的静态初始化块,然后执行构造方法。上述代码类加载的时机如下图所示:
类初始化步骤大概如下:
- (1) 当初始化一个类的时候,如果发现其的父类没有被初始化,则会先初始化其父类;
public class JVM01 {
static {
System.out.println("静态初始化类JVM01...");
}
public static void main(String[] args) {
System.out.println("执行main方法...");
Cat cat = new Cat();
System.out.println(Cat.value);
Cat cat1 = new Cat();
}
}
class Animal {
static {
System.out.println("静态初始化Animal类...");
}
public Animal() {
System.out.println("创建Animal类对象...");
}
}
class Cat extends Animal {
static int value = 100;
static {
System.out.println("静态初始化类Cat...");
value = 200;
}
public Cat() {
System.out.println("创建Cat类对象...");
}
}
通过观察控制台输出,可见,必须先加载JVM01类,才能使用这个类,所以JVM01静态代码块部分第一个执行,接着执行main()方法,然后加载Animal父类和初始化静态代码块,接着加载子类Cat和静态代码块;注意,当第二次创建Cat对象时,父类Animal和Cat类中静态代码块不会再执行,但是构造方法还是会执行的。
- (2) 虚拟机会保证一个类的类构造方法在多线程环境能够正确加锁和同步;
- (3) 当访问一个java类的静态域时,只有真正声明这个域的类才会被初始化;
对于上面第三点,具体说明可见下面类加载时机的示例。
【d】结束生命周期
•在如下几种情况下,Java虚拟机将结束生命周期:
- 执行了System.exit()程序退出方法;
- 程序正常执行结束;
- 程序在执行过程中遇到了异常或错误而异常终止;
- 操作系统出现错误而导致Java虚拟机进程终止;
四、类初始化时机
只有当对类的主动使用的时候才会导致类的初始化。以下是类初始化时机的分类以及场景:
- 主动引用:
* 1. new一个类的对象 * 2. 调用类的静态属性(除了final修饰的常量)或者静态方法 * 3. 通过反射调用类 * 4. 当虚拟机启动,java Cat,则一定会初始化类Cat,说白了就是先启动main()方法所在的类
- 被动引用:
* 1. 引用常量并不会出发该类的初始化(常量在编译阶段已经存入调用类的常量池中)
* 2. 通过子类引用父类的静态变量,不会导致子类的初始化
* 3. 通过数组定义类引用,不会导致类的初始化
public class JVM01 {
public static void main(String[] args) throws ClassNotFoundException {
/**
* 主动引用: 只有主动引用才能初始化类Cat
*
* 1. new一个类的对象
* 2. 调用类的静态属性(除了final修饰的常量)或者静态方法
* 3. 通过反射调用类
* 4. 当虚拟机启动,java Cat,则一定会初始化类Cat,说白了就是先启动main()方法所在的类
*/
new Cat();
System.out.println(Cat.value);
Class.forName("com.wsh.jvm.jvm03.Cat");
/**
* 被动引用
*
* 1. 引用常量并不会出发该类的初始化(常量在编译阶段已经存入调用类的常量池中)
* 2. 通过子类引用父类的静态变量,不会导致子类的初始化
* 3. 通过数组定义类引用,不会导致类的初始化
*/
System.out.println(Cat.val);
Cat[] cats = new Cat[10];
/**
* 通过子类(BSCat)引用父类(Cat)的静态变量,不会导致子类(BSCat)的初始化
*/
System.out.println(BSCat.value);
}
}
class Cat {
static int value = 100;
static final int val = 20;
static {
System.out.println("静态初始化类Cat...");
value = 200;
}
public Cat() {
System.out.println("创建Cat类对象...");
}
}
class BSCat extends Cat {
static {
System.out.println("静态初始化类BSCat...");
}
}
通过上面的示例可以观察类的主动引用和被动引用,注意只有主动引用情况下才会触发类的初始化。
五、总结
本节主要总结了类加载的整个过程,对类加载有了初步的认识和了解,并通过一些示例说明了类初始化以及类加载的过程和特性,本文只是笔者的一些初步总结,仅供大家学习参考,希望对大家有所帮助,不对之处敬请指点。