JVM学习笔记(三)

上回我们通过一个 Hello World 程序,分析了class文件结构,这回我们来分析下JVM是如何加载class文件中的类。

我们在运行java程序之前,肯定需要将程序用到的类加载到虚拟机中,那么虚拟机是多会加载的类呢。首先虚拟机中规定了java类的整个生命周期如下(顺时针):

JVM学习笔记(三)_第1张图片
在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代码是给子类的一个静态域变量赋值,通过打印结果可以看到子类已经初始化了,同样在初始化子类之前肯定需要初始化父类。所以①给类的静态变量赋值也会初始化该类。

现在运行与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规范中规定“有且只有”这五种情况会触发类的初始化。

你可能感兴趣的:(jvm)