上回我们通过一个 Hello World 程序,分析了class文件结构,这回我们来分析下JVM是如何加载class文件中的类。
我们在运行java程序之前,肯定需要将程序用到的类加载到虚拟机中,那么虚拟机是多会加载的类呢。首先虚拟机中规定了java类的整个生命周期如下(顺时针):
在JVM规范中并没有规定何时开始一个类的加载,但是规定了五种情况发生时需要对类立即进行初始化操作,由于初始化是在加载操作之后,所以这五种情况发生时肯定会进行类的加载操作。
下面我们通过几个简单的例子来总结是哪五种情况。我们知道一个类在初始化时会先执行static块和static域变量中的内容,所以下面的代码通过执行static块来验证类初始化情况。
public class StaticGrandfather {
static {
System.out.println("grandfather init");
}
}
public class StaticFather extends StaticGrandfather {
static {
System.out.println("father init");
}
public static int FATHER_VALUE = 1;
}
public class StaticSon extends StaticFather {
static {
System.out.println("son init");
}
public static int SON_VALUE;
public static final int SON_FINAL_VAL = 0;
}
public class StaticMethod extends StaticFather {
static {
System.out.println("method init");
}
public static void invokeStaticMethod() {
System.out.println("run static method");
}
}
public class MethodHandleTest {
static {
System.out.println("methodhandle init");
}
public static void println(String s) {
System.out.println(s);
}
}
下面运行如下代码
public class Main {
static {
System.out.println("main init");
}
public static void main(String[] args) {
// 0
StaticGrandfather sg = new StaticGrandfather();
// try {
// StaticGrandfather sg = (StaticGrandfather) Class
// .forName("com.togo.jvm.load.StaticGrandfather").newInstance();
// } catch (InstantiationException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (IllegalAccessException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (ClassNotFoundException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// 1
// System.out.println(StaticSon.FATHER_VALUE);
// 2
// StaticSon.SON_VALUE = 123;
// 3
// System.out.println(StaticSon.SON_FINAL_VAL);
// 4
// StaticMethod.invokeStaticMethod();
//5
// MethodType mt = MethodType.methodType(void.class, String.class);
// try {
// MethodHandle mh = MethodHandles.lookup().findStatic(MethodHandleTest.class, "println", mt);
// mh.invoke("ss");
// } catch (NoSuchMethodException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (IllegalAccessException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (Throwable e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
}
}
当我们运行0时(其他代码注释,下同),控制台打印结果为
main init
grandfather init
0代码是new了一个对象,通过打印结果可以知道①JVM会先初始化main()方法所在的类;②new对象会触发该类的初始化。
不光是new操作,通过反射创建对象也是会触发该类的初始化,这里就不演示了。
当我们运行1时,控制台打印结果为
main init
grandfather init
father init
1
1代码是通过子类调用了父类的静态域变量,通过打印结果可以看到父类和祖父类都已经初始化了,但是子类没有初始化。这里说明①子类调用父类的静态域变量并不会让子类得到加载②而是会初始化该变量所属的类,③如果初始化的类的父类还没有初始化,则会先初始化父类。
现在运行2代码,注释掉1(下同),运行结果如下
main init
grandfather init
father init
son init
2代码是给子类的一个静态域变量赋值,通过打印结果可以看到子类已经初始化了,同样在初始化子类之前肯定需要初始化父类。所以①给类的静态变量赋值也会初始化该类。
![](/qrcode.jpg)
现在运行与2代码类似的3代码,结果如下:
main init
0
从结果看,只是打印了这个变量的值,并没有初始化这几个类中的任意一个,这是因为JVM在编译阶段,已经将static final 修饰的变量的值存储在了调用类的常量池中,所以代码中访问这个值已经跟StaticSon类没有关系了,所以并不会初始化这个类,即①访问一个类的常量变量并不会触发这个类的初始化。
运行代码4,结果如下
main init
grandfather init
father init
method init
run static method
通过结果得知①访问类的静态方法会触发该类的初始化
以上是一些会触发初始化的常见操作,下面看看动态语言支持
运行代码5,结果如下
main init
methodhandle init
ss
《深入理解JAVA虚拟机》原文是:当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化则需要先触发初始化。
动态语言实现的效果有点类似反射,但是实现的原理方式不同,最终的目标也不同,后面再详细介绍。
以上就是会触发初始化的五种情况,总结一下:
①使用new创建对象;读取或者设置类的静态变量(非final字段);调用一个静态方法。
②使用反射创建一个对象;
③初始化一个类的时候,如果这个类的父类还没有初始化,则先对父类进行初始化;
④JVM会对main()方法所在的类进行初始化;
⑤当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化则需要先触发初始化。
JVM规范中规定“有且只有”这五种情况会触发类的初始化。