1、概述
类从加载到虚拟机内存,到卸载出内存,分为:加载、验证、准备、解析、初始化、使用、卸载。
一般编程人员只用关注:加载、连接(分为验证、准备、解析)、初始化即可。
加载、验证、准备、解析、初始化、卸载这5个阶段顺序是确定的。而解析阶段不一定:可以在初始化之后再开始,这是为了支持Java的动态绑定。
什么时候进行初始化?
以上四条为主动引用,而被动引用,如下所示,不会引发类初始化
1. 子类引用父类静态变量,子类不会初始化
public class ClassInitTest {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
class SuperClass{
static{
System.out.println("SuperClass Init");
}
public static int value = 123;
}
class SubClass extends SuperClass{
static{
System.out.println("SubClass Init");
}
}
输出:
SuperClass Init
123
子类没有初始化
2. 定义一个该类的数组,不会引发该类初始化
//SuperClass不会进行初始化
SuperClass[] sps = new SuperClass[];
3. 引用一个类的常量(final修饰),不会引发初始化
public class ClassInitTest2 {
public static void main(String[] args) {
System.out.println(ConstClass.H);
}
}
class ConstClass{
static{
System.out.println("ConstClass Init");
}
public static final String H = "hi";
}
输出:
hi
ConstClass没有进行初始化
2、类加载过程
1)加载
指的是类加载的过程的“加载”阶段。完成以下三件事:
2)验证
保证Class文件字节流包含的信息符合虚拟机要求,且不会破坏虚拟机
1. 文件格式验证
2. 元数据验证:对Java语言语义的验证
3. 字节码验证:方法体的验证
4. 符号引用的验证:为解析阶段作些预处理
3)准备
为类变量分配内存,全初始化为0值。
准备与初始化的区别?
public static int value = 123;
对于以上语句,在准备阶段时,会为value分配内存空间,并且二进制全置为0,准备阶段完成后,value值为0。只有在类被引发初始化时,value才会被赋值为123。
注:对于以下语句
public static final int value = 123;
此值为常量,那么在准备阶段就会被初始化为123。
4)解析
把符号引用替换为直接引用
分为四种解析:
1. 类或接口的解析
2. 字段解析
3. 类方法解析
4. 接口方法解析
5)初始化
初始化阶段是执行类构造器方法< clint >()的过程。< clint >方法执行过程中的特点(以下统称为clint方法):
-
clint方法由编译器自动收集类中的所有类变量的赋值动作和静态代码块中语句合并产生的,编译器收集的顺序是按照程序的顺序,所以,静态代码块只能访问到定义在静态代码块之前的变量,定义在之后的变量,只能赋值,不能访问。
public class Test{
static{
i = 0;//给变量赋值可以编译通过
System.out.print(i);//编译器提示“非法向前引用”
}
static int i = 0;
} - clinit方法与类的构造函数(即,实例的init方法)不同,不需要显示调用父类构造器, 虚拟机会保证子类在clinit方法执行完之前,父类得clinit方法 执行完毕。因此,在虚拟机中第一个被执行clinit 方法的肯定是java.lang.Object.
- 父类中定义的静态语句块以及类变量赋值要先于子类执行,因为父类中的clinit方法会先调用。
- clinit方法对于类或者接口来说并不是必须的,如果一个类中没有静态代码块,也没有类变量的赋值操作,那么编译器就不会生成clinit方法,为这个类。
- 接口不能使用静态语句块,但他仍然有变量赋值操作,因此,接口也会生成clinit方法。不同点:a、执行接口的clinit方法时,不需要先执行父接口的clinit方法。b、只有父接口中的变量被使用时, 父接口才会被初始化。c、接口的实现类在初始化时也一样不会执行接口的clinit方法
- 虚拟机会保证一个类的clinit方法在多线程环境下被正确的加锁、同步,多个线程同时去初始化一个类,只会有一个线程执行clinit方法,其他的线程都会阻塞等待。
注:虽然其他线程会阻塞,但是,初始化完成这个类后,就不必再进行初始化了,也就是其他线程不用再执行clinit方法。
3、类加载器
1)类与类加载器
对于任意一个类,由 加载它的类加载器和类本身一同确定在JVM中的唯一性。
比较两个类相等(equals、isAssignableFrom、isInstance、instanceof),只有他们由同一类加载器加载才有意义,否则,即使两个类来自同一个Class文件,他们也是不相等的。
2)双亲委派模型
绝大部分Java程序都会使用到以下三中系统提供的类加载器:
启动类加载器
扩展类加载器
应用程序类加载器
双亲委派模型的工作过程:如果一个类加载器收到一个类加载请求,他不会首先自己加载这个类,而是把请求委派给父类加载器去执行,每一层的类加载器都是如此。只有当父类加载器反馈自己无法完成加载请求时,子类加载器才会尝试自己去加载。保证了类的唯一性,因为不同类加载器加载相同的类会在虚拟机产生多个不同的类。