类加载过程的三个阶段
加载阶段
主要负责查找并且加载类的二进制文件,就是class文件。
连接阶段
验证:主要负责类文件的正确性,比如class的版本,class文件的魔术因子是否正确。
准备:为类的静态变量分配内存,并初始化默认值。
解析:把类中的符号引用转换为直接引用。
初始化阶段
为类的静态变量赋予正确的初始值(代码编写给定的值)。
JVM对类的初始化是一个延迟加载的机制,即是使用lazy方式,当一个类在首次使用的时候才会被初始化,在同一运行包下,一个class只会被初始化一次。
类的主动使用和被动使用
每个类或者接口被J=java程序 首次主动使用 才会对其进行初始化。
主动使用类的场景:
- 使用 new 关键字创建对象。
- 访问类的静态变量,读取和更新会导致类的初始化。
- 访问类的静态方法。
- 对类进行反射操作。
- 初始化子类会导致父类的初始化。(通过子类调用父类的静态变量只会初始化父类,不会初始化子类)
- 启动类
被动使用部分场景:
- 构造某个类的数组并不会导致类的初始化。
- 引用类的静态常量不会导致初始化。
//不会初始化
public final static int MAX = 10;
//虽然也是静态常量,但是会初始化
public final static int COUNT = new Random().nextInt();
类加载过程详解
类的加载阶段
是将class文件中的二进制数据读取到内存中,然后将字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存中生成一个该类的class对象,作为访问方法区的数据结构入口。
类加载的最终产物就是堆内存中的class对象,不管一个类被加载几次,对应在堆内存中的对象只有一个。虚拟机规范指出类的加载是通过全限定名(包名+类名)来获取二进制数据流。
类的连接阶段
-
验证:
确保class文件的字节流所包含内容符合当前JVM的规范,不会出现危害JVM自身安全的代码,当信息不符合要求时会抛出VerifyError的异常或者子类异常。验证文件格式:
验证class文件的魔术因子,魔术因子决定了文件类型,class文件魔术因子是0xCAFEBABE。
验证版本号,高版本JDK编译后的class文件不能被低版本的JVM兼容。
验证class文件字节流是否存在残缺或者其他附加信息。
验证常量池中的常量是否存在不被支持的变量类型。
验证常量中的引用是否指向了不存在的常量或者改常量的类型不被支持,等一些其他信息。元数据验证:
保证class字节流符合JVM规范。
检查类是否存在父类或者实现某些接口,验证这些接口和父类是否合法或者是否存在。
检查该类是否继承了被final修饰的类,被final修饰的类不能被继承并且方法不能被重写。
检查该类是否为抽象类,如果继承了抽象类或者实现接口,是否实现了里面的抽象方法或者实现接口中的方法。
检查方法重载的合法性,比如方法名与参数相同返回类型不同的是不被运行的。等其他语义验证。字节码验证:
保证当前线程在程序计数器中的指令不会跳到不合法的字节码指令中去。
保证类型转换是否合法。
保证任何时候,虚拟机栈中的操作栈类型与指令代码都能正确的被执行。等其他验证。符号引用验证;
验证符合引用转换为直接引用时的合法性。确保解析动作顺利执行,若某个类不存在则会抛出NoSuchFieldError,方法不存在抛出NoSuchMethodError。
验证通过符号引用描述的字符串全限定名称是否能顺利找到相关的类。
验证符合引用中的类,字段,方法是否对当前类可见。等其他验证。 -
准备
为静态变量分配内存,并且设置初始值,类变量内存分配到方法区中,实例变量分配到堆内存中。这里的设置初始值不是代码里面的初始值,而是给定类型变量的初始值。
//准备阶段值为0 不是10
private static int x =10;
//为静态常量不会导致类的初始化,在编译阶段就赋值了,所以值为10
private final static int y=10 ;
数据类型 | 初始值 |
---|---|
Byte | 0 |
Char | \u0000’’ |
Short | 0 |
Int | 0 |
Float | 0 .0f |
Double | 0 .0d |
Long | 0 L |
Boolean | false |
引用类型 | null |
- 解析
在常量池中寻找类,接口,字段和方法的符号引用,并且将符号引用替换成直接引用的过程。
public class ClassResolve{
static Simple simple = new Simple();
static Simple[] simple1 = new Simple[10];
public static void main(String[] args){
System.out.println(simple)
}
}
类接口解析:
simple 加载中需要先完成对Simple 类的加载,同样需要经历所有的类加载阶段。
simple1 是一个数组,则JVM不需要对simple类进行加载,只需要在虚拟机中生成一个能够代表该类型的数组对象,并在堆内存中开辟一片连续的地址空间即可。
类接口解析完成后,还需要进行符号引用验证。
字段解析:
如果simple类中包含某个字段,则直接返回这个字段的引用,也会对该字段所属的类提前进行类加载。
如果simple类中没有改字段,则会根据继承关系自下而上的查找,找到就返回,同样也需要提前对找到的字段进行类的加载过程。若找到最上层的Object还是没有,则表示查找失败,直接抛出NoSuchFieldError异常。
这就是为什么重写了父类的字段能生效的原因,因为找到子类的字段就直接返回了。
类方法解析:
在类的方法表中发现class_index中索引的Simple是一个接口而不是一个类,则直接返回错误。
在simple类中查找有没有和目标方法一致的方法,有就直接返回方法的引用;否则会根据继承关系自上而下的查找,找到就返回,查找失败则抛出NoSuchMethodError异常。
如果在当前类或者父类中找到了目标方法,但是一个抽象方法则会抛出AbstractMethodError异常。
接口方法的解析:
在接口的方法表中发现class_index中索引的Simple是一个类而不是一个接口,则直接返回错误。
和类方法解析比较类型,自下而上的查找,找到就直接返回,查找失败则抛出NoSuchMethodError异常。
类的初始化阶段
类的初始化阶段是整个类加载的最后阶段,主要是执行()方法的过程,在方法中所有的类变量都会被赋予正确的值,也就是程序中指定的值。
()方法是在编译阶段生成的,包含了所有类变量的复赋值动作和静态语句块的执行代码,编译器收集顺序是由执行语句在源文件中的顺序决定的,静态语句块只能对后面的静态变量进行赋值,不能进行访问。
//能访问,能赋值
private static int x =10;
static {
System.out.println("static"+x);
x=30;
}
//不能访问x,能进行赋值操作,值会被10所覆盖
static {
System.out.println("static"+x);
x=30;
}
private static int x =10;
//不能访问x,能进行赋值操作,不会被覆盖
static {
System.out.println("static"+x);
x=30;
}
private static int x ;
JVM保证了<clinit>()方法在多线程环境下执行的同步语义,静态代码块在多线程环境下只会执行一次,因此单列模式可在静态代码块中完成对象的实例化。
public class ThreadTest1 {
private static int y ;
private static int x=0 ;//1
private static ThreadTest1 t = new ThreadTest1();//2
private ThreadTest1() {
x++;
y++;
}
public static ThreadTest1 getInstance() {
return t;
}
public static void main(String[] args) {
ThreadTest1 tt = ThreadTest1.getInstance();
System.out.println(tt.x);
System.out.println(tt.y);
}
}
输出为 1 ,1;如果把1和2换位子输出则为0,1;因为换位置后x在构造方法赋值后再一次显示的赋值为0了。