天天看点

类的加载过程类的加载过程

类的加载过程

一、类的加载过程

类的加载过程类的加载过程

类的加载过程,一般分为三个比较大的阶段:加载、连接、初始化

  • 加载阶段:主要负责查找并加载class文件
  • 连接阶段:细分为如下三个阶段

    验证:验证类文件的正确性,比如class文件的魔数、版本等等

    准备:为类的静态变量分配内存,并且赋默认值(比如说 static int i = 8,在准备阶段,i的值为默认值0,而不是8)

    解析:把类中的符号引用转换为直接引用

  • 初始化阶段:为类的静态变量赋予正确地初始值(比如static int i=8,在初始化阶段,i的值为8)

1、符号引用与直接引用

类的加载过程类的加载过程

符号引用以一组符号描述所引用的目标,符号可以是任意形式的字面量,只要使用时,能够无歧义地定位到目标即可。在class文件中,它以CONSTANT_Methodref_info、CONSTANT_Class_info、CONSTANT_Fieldref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标不一定加载到内存中。在编译时,java类并不知道所引用类的实际地址,因此只能用符号引用代替。

直接引用可以是:

(1)直接指向目标的指针(比如,指向类型【class对象】、类变量、类方法的直接引用可能是指向方法区的指针)

(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

(3)一个能间接定位到目标的句柄

直接引用是和虚拟机的布局相关的,如果有了直接引用,那么引用的目标必定加载到内存中了

2、基于准备阶段和初始化阶段的面试题

public class T {
    //1
    private static int count = 2;
    //2
    private static T t = new T();
​
    public T() {
        count++;
    }
​
    public static void main(String[] args) {
        //输出的值为3
        System.out.println(T.count);
    }
}
           
public class T {
    //2
    private static T t = new T();
​
    //1
    private static int count = 2;
​
    public T() {
        count++;
    }
​
    public static void main(String[] args) {
        //输出的值为2
        System.out.println(T.count);
    }
}
           

为什么1和2调换一下位置,输出的值就不同了呢?这就涉及到准备阶段和初始化阶段了。以第二个代码为例,在准备阶段,给t赋默认值null,给count赋默认值0,此时private static T t = null; private static int count = 0; 在初始化阶段,给t赋初始值new T(),执行count++,此时count为1;然后给count赋初始值2,覆盖掉了1,此时count为2.

3、双重校验锁单例异常

上面说到了准备阶段和初始化阶段静态变量的问题,那么非静态变量会不会也有问题呢?会的,但是非静态变量的问题,并不是发生在类的加载过程,而是new一个对象的过程

public class LazyOneBean {
    private static volatile LazyOneBean lazyOneInstance = null;
​
    private LazyOneBean(){
    }
​
    public static LazyOneBean getInstance(){
        if(lazyOneInstance == null){
            synchronized (LazyOneBean.class){
                if(lazyOneInstance == null){
                    lazyOneInstance = new LazyOneBean();
                }
            }
        }
        return lazyOneInstance;
    }
}
           

若private static volatile LazyOneBean lazyOneInstance = null 不加volatile,此单例模式可能会出现异常,为什么?因为new一个对象,实际上是分为三步的:分配内存空间并将对象的字段赋默认值、调用init进行初始化(对象的字段赋初始值)、对象的内存地址赋值给局部变量。第二步与第三步不存在依赖关系,所以cpu可能对它们进行指令重排,也就是说可能先执行第三步,再执行第二步。

当第一个线程执行new LazyOneBean()时,先执行第三步,再执行第二步。在它刚执行完第三步,而没有执行第二步时,lazyOneInstance已经不为空,但是还没有初始化。假设第二个线程也调用getInstance(),发现lazyOneInstance不为空,立刻返回此对象。然后拿着这个对象去使用,就可能出现问题。假设LazyOneBean有一个实例变量private int i = 8,此时第二个线程拿到的i的值是默认值0,而不是8了(因为还没有初始化)

加一个volatile的目的,就是为了禁止指令重排。

以下为new一个对象的bytecode

public class Test {
    public static void main(String[] args) {
        Test test = new Test();
    }
}
           

Test test = new Test()的字节码如下:

0 new #2 <me/Test>
3 dup
4 invokespecial #3 <me/Test.<init>>
7 astore_1
8 return
           

二、参考资料

  • https://www.cnblogs.com/shinubi/articles/6116993.html

  • 《java高并发编程详解 多线程与架构设计》第9章类的加载过程