- 我们知道类的加载就是把class文件中的二进制数据读取到jvm中,然后把该二进制数据所代表的静态存储结构转化为方法区中的运行时结构,并且在堆内存中生成一个该类的
java.lang.Class
对象,这个对象是访问方法区该类结构的入口。
所以类加载过程的最终产物是堆内存中该类的java.lang.class
对象。 - 在连接过程中,会首先对该class文件进行各种校验,比如校验模数码等,确保加载进来的class文件是正确完整并且符合JVM规范的。
然后在连接工作的第二部分内容是为静态变量分配存储空间,并设置为默认值。
但是要注意一下的区别
//在连接阶段,x的值为0,因为在连接阶段只会为其分配存储空间,并设置为默认值
public static int x = 10;
//因为有final修饰,导致访问该变量并不会导致类的初始化,所以在编译阶段该变量就直接被赋值10了。
//所以在连接阶段,y的值还是10
public static final int y = 10;
连接阶段的第三部分工作是把符号引用转化为直接引用,在运行中只有直接引用才能找到正确的内存地址进行操作。比如在 类A中,有B b = new B()
,那么在A类的连接过程中,要把符号引用b
转换为b的直接引用,从而可以找到b真正的内存地址,并且对b进行操作。
把符号引用转换为直接引用还包含一些其他的工作,比如字段的解析,如果一个类包含了该字段,则直接返回该字段,如果该类不包含该字段,继续往该类的父类中进行递归寻找,找到就返回那个字段。如果最终到java.lang.Object
中还没有找到该字段,则抛出异常。
这个过程也说明了为什么我们可以在子类中覆盖父类中的一个字段,因为在我们使用子类中的这个字段的时候,在子类中找到该字段就直接返回了。
- 第三个阶段就是类的初始化阶段。
初始化阶段会根据我们在java文件中的 书写顺序 来执行 所有静态变量的赋值动作 和 静态语句块中的代码。所以初始化阶段是有顺序的。这个顺序说的是 静态变量的赋值有顺序,静态语句块的执行有顺序。但是在静态语句块中给静态变量赋值,却没有要求静态语句块一定要出现在静态变量之后。比如
public class Test1{
static {
//这是可以编译并运行的,在静态块中可以给静态变量赋值,并不要求静态块出现在静态变量之前。
//但是不建议这么做
x = 100;
}
public static int x = 10;
}
public class Test2{
static{
//这样是错误的,无法编译通过
//在静态块中给静态变量赋值不要求顺序,但是访问的话是需要顺序的,此时会出错
//出错的原因是找不到y
System.out.print(y);
}
public static int y = 10;
}
了解了这些,我们来看一下在单例模式中容易出现的一个诡异问题
public class Test{
//此处是位置1
public static x = 10;
public static y;
//此处是位置2
public static Test sInstance = new Test();
private Test(){
x++;
y++;
}
public void print(){
System.our.print("x is "+x+" y is "+y);
}
public static void main(String[] args){
Test.sInstance.print();
}
}
上面的程序会输出什么,这个很明显会输出" x is 11 y is 1"。
但是如果把public static Test sInstance = new Test()
这句话放在位置1会输出什么呢?
这时候会输出什么呢?
这个时候会输出x is 10 y is 1
.
因为如果把public static Test sInstance = new Test()
放在位置1,会先出事话Test,在Test的构造方法中,执行x++和y++。此时x和y都是1. 在往下会执行x的初始化语句,把x设置为10,所以此时x是10,由于y没有静态的赋值语句,所以不对y进行初始化,y原来的值是什么样子还是什么样子,所以此时y的值是1.
因为初始化顺序的原因导致的问题一般比较难以被发现,所以我们要养成良好的编程习惯,在使用单例模式的时候,最后把对单例的初始化放在所有静态变量初始化之后。
网友评论