前言
我们知道系统加载.class文件分别有这几个步骤:加载、连接、初始化三个阶段。
其中加载阶段会通过全类名获得到对应文件的二进制字节流,再通过native方法产生一个Class对象,这一步主要由类加载器来完成。也就是说Class对象在第一步就产生了。
连接阶段又分为验证、准备、解析三个步骤。其中JVM会在准备阶段为类的所有静态变量赋上初值。
最后的初始化阶段,JVM会执行在编译时(javac命令生成.class时)生成的<clinit> ()
方法,这个方法是用来初始化所有类的静态变量的,也就是说类的静态变量在这里才真正的初始化,之前的准备阶段并不会初始化静态变量,只是对静态变量赋初始值,比如int类型赋值为0,具体的值需要要初始化阶段才会赋上。
但是静态常量,即被final修饰的变量,会在准备阶段就进行初始化操作。
初始化步骤分析
根据上面的结论,我们知道初始化阶段其实就是初始化类静态变量的时期。
我们知道类名.class
和Class.forName("类名")
都能得到对应类的Class对象,但是前者不会进行初始化操作,而后者会进行初始化操作。现在来看一个例子,体会这个差异:
import org.junit.Test;
public class MyTest {
@Test
public void test1(){
System.out.println(Apple.class);
}
@Test
public void test2() throws ClassNotFoundException {
System.out.println(Class.forName("Apple"));
}
}
class Apple {
static {
System.out.println("static init");
}
}
test1输出如下:
test2输出如下:
可以发现类名.class
由于不会进行初始化步骤,确实没有执行<cinit>()
方法,走静态代码块。
Class.forName(String name, boolean initialize, ClassLoader loader)方法参数分析
我们知道Class.forName()
方法其实还有一个重载方法,可以传入一个布尔值和一个ClassLoader,其实这个布尔值就是告诉JVM是否进行初始化步骤。
在刚才的类中再添加test3:
@Test
public void test3() throws ClassNotFoundException {
System.out.println(Class.forName("Apple", false, Apple.class.getClassLoader()));
}
执行该方法,输出如下:
注意到由于设置了false,这次forName方法并没有对Apple类进行初始化操作。
Main方法的初始化
其实如果是main()
方法所在类的话,还有一些小细节值得注意。
假设有如下代码:
public class MyTest {
public static void main(String[] args) throws ClassNotFoundException {
}
public Apple apple(){
return new Apple();
}
}
运行main()
方法,即使里面没有任何代码,如果我们在运行前把路径下的Apple.class文件删除,那么就会报如下错误:
可以注意到即使main()
中不运行任何代码,但是也会去加载其所在类MyTest中引用的所有类,这里就是Apple类。并且这里的加载也不会执行初始化步骤。
修改上面代码如下:
public class MyTest {
public static void main(String[] args) throws ClassNotFoundException {
new Banana();
}
}
class Banana{
public Apple apple(){
return new Apple();
}
}
在运行前把路径下的Apple.class文件删除,发现不会报任何错误,程序正常退出。
由此可以得出JVM在运行main()
所在类时,会将该类引用的所有类型的Class对象全部加载进内存。