类的加载连接初始化

时间:2022-03-20 14:52:52

自己是在看视频的过程中看到一个比较有意思的面试题然后学习了一下关于jvm中类的加载连接和初始化部分的内容,感觉很有收获,所以在博客中记录一下。

首先贴代码:

class SingleTon {
public static int count1;
public static int count2 = 0;
private static SingleTon singleTon = new SingleTon;

private SingleTon {
count1++;
count2++;
}

public static SingleTon getInstance {
return singleTon;
}
}

public class ClassLoadTest {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance;
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}

}

这时的输出是:

count1=1count2=1

当然了这个输出是我们意料之中的调用构造方法把count1和count2的值都加1所以输出是两个1,但是我们简单的改一下程序,如下:

class SingleTon {
private static SingleTon singleTon = new SingleTon;
public static int count1;
public static int count2 = 0;

private SingleTon {
count1++;
count2++;
}

public static SingleTon getInstance {
return singleTon;
}
}

public class ClassLoadTest {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance;
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}

}

输出的结果是:

count1=1count2=0

只是把private static SingleTon singleTon = new SingleTon; 这个语句放到程序的上边怎么结果就不一样了呢?我想在实际开发中是很少这样写代码的,只有面试官才会想这么折磨人的问题。想要搞清楚为什么会发生不同的结果我们得从类的加载连接和初始化说起。

一、类的生命周期

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和清理这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。我们在这里只讨论到初始化这个阶段。

接下来我们依次说一说这几个阶段,之后我们再回头看那个面试题就很easy了。

二、类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

类的加载连接初始化

类的加载的最终产品是位于堆区内的class对象。

Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

三、类的验证

类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

类的验证的内容

1.类文件的结构检查(确保类文件遵从java类文件的固定格式)

2.语义检查(确保类文件符合java的语法规定,比如接口没有实现,final修饰的类没有子类等)

3.字节码验证(确保字节码流可以被java虚拟机安全的执行)

4.二进制兼容性的验证(确保类的相互引用是否正确,比如在Test类中speak()方法中会调用Person类中的say()方法,如果Person类中没有say()方法就会抛出NoSuchMothodError错误)

四、类的准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。注意是只有静态变量在这个时候分配内存并设置默认值。

五、类的解析

在解析阶段,java虚拟机会把类二进制数据中的符号引用替换为直接引用。

举一个例子:

public void go {
car.run;
}

其中这个car.run;这段代码在这个类的二进制数据中就是符号引用。那什么是直接引用呢,其实说白了就是指针。在类的解析阶段java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run方法在方法区内的内存位置,而这个指针就是直接引用!

六、类的初始化

(一)初始化这块是一块重要的内容,java尽力保证所有变量在使用之前都得到恰当的初始化,对于方法的局部变量,java以编译时错误的形式来贯彻这种保证(这是因为未初始化的局部变量更有可能是程序员的疏忽),所以局部变量并不会分配默认值。而成员变量java虚拟机会在初始化的时候给他分配空间并赋值,如果程序员写的程序没有赋值的就会给他一个默认值。

(二)还有一点初始化说的是变量,对于方法并没有初始化这一说。。。

(三)对于初始化顺序就是先静态后非静态,静态域按定义的先后顺序决定了初始化顺序,非静态域也是同理。

(四)初始化的时机(主动使用):

1.创建类的实例。

2.访问某个类或接口的静态变量(编译期常量除外,因为java编译器在处理编译常量的时候直接就把值写到了class文件里),或者对该静态变量赋值。

3.调用类的静态方法。

4.反射(如Class.forName(“com.shengsiyuan.Test”))。

5.初始化一个类的子类。

6.Java虚拟机启动时被标明为启动类的类(Java Test)。

除了上述六种情形,其他使用Java类的方式都被看作是被动使用,不会导致类的初始化。

(六)当java虚拟机初始化一个类时,要求他所有的父类都已经初始化,但是这条规则并不适合接口。

(七)调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。只是加载(把clss的数据加载到内存),并没有涉及到初始化。

七、回头看我们遇到的问题

第二次为什么输出是1和0呢,就直接看第二段程序了哈,第一段略。我们一条条分析,在main方法中调用了getInstance方法,开始了对SingleTon类的加载过程。把SingleTon.class里的数据加载到内存之后,开始类的连接首先验证我们认为是正确的,然后下一步是准备,为静态变量赋默认值按定义的先后顺序依次是singleTon = null,count1 = 0,count2 = 0。然后接下来解析然后初始化,开始赋初值。这是执行singleTon = new SingleTon;调用构造方法count1 = 1,count2 = 1。然后执行count2 = 0; 因为count1没有赋值操作所以还是默认值0。所以最后的输出结果是:

count1=1count2=0

对于类的清理我打算另外花时间再写一篇。欢迎讨论!