虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析,和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载时机
类加载时机分为七个阶段,分别为加载->验证->准备->解析->初始化->使用->卸载。
加载,验证,准备,初始化,卸载这5个阶段的顺序是确定的,也就是说一个类的初始化完成,那么这个类必须经历准备阶段。
加载
在加载阶段,虚拟机要完成以下三个阶段:
-
通过一个类的全限定名来获取定义此类的二进制字节流。(这里我们可以在硬盘,网络,运行时动态生成都行,只要我们能得到并转换成jvm的字节码规定的格式就行)。
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。(也说明了方法区中存放的时类信息)
-
生成一个代表这个类的Class对象。
验证
这个阶段的目标就是为了确保Class文件流中包含的信息符合当前虚拟机的要求,对安全性进行检测。
准备
这个阶段是给类变量(static修饰的变量存放于方法区中)设置初始值。注意是设置初始值,比如int类型的初始值是0。假设一个类变量的定义为:
public static int vaule = 110 ;
那在准备阶段也先会赋值value等于0,而不是110 ;赋值110是在初始化阶段执行。下面列出Java中所有的基本类型的初始值。有一个例外,当value为final修饰为常量,即变量value定义为:
public static final int value = 110;
这时value的值在准备阶段为110。
boolean | false |
---|---|
char | [] |
byte | 0 |
int | 0 |
long | 0 |
short | 0 |
float | 0.0 |
double | 0.0 |
reference(引用) | null |
解析
解析就是把把在常量池中的符号引用替换为直接引用的过程。通俗点就是能够在常量池中的符号引用直接指向目标的指针,这样就能够映射到符号引用代表的对象的内存地址。
初始化
初始化阶段是根据程序员通过程序制定的主观计划去初始化类变量和其他资源。通俗点讲就是给类变量赋我们给的值,和执行static语句块里面的内容。
那什么时候一个类会进行初始化呢?总结得出以下几点。
-
new一个对象
-
读取或者设置一个类的静态字段(被final修饰,已在编译期把结果放在常量池除外)
-
调用一个类的静态方法
-
通过反射调用
-
当初使化一个类的时候,如果发现其父类还没有进行初始化,那么需先初始化这个父类
-
执行main()方法
上面说的这几点属于对类的主动引用,有且只有这几种方式属于主动引用,其他称为被动引用,被动引用不会引发类的初始化。
有了这些理论知识的铺垫,下面我们来通过代码加深类加载的理解。同时也可以对面试中经常问的类的初始化顺序的面试题有一个系统学习。可以说只要是理解了下面几个代码的执行过程,那么此类面试题以后都不会有问题了。我会结合上面的理论知识详尽的解释代码执行过程,让你做到心中有数。
1.第一个代码,从最基础的代码来入手,从输出结果来详细分析它的类加载过程。代码中的注释为输出的结果。
public class MyTest1 {
static {
System.out.println("MyTest1");
}
public static void main(String[] args) {
System.out.println(MyParent1.str);
}
}
class MyParent1{
public static String str = "str" ;
static {
System.out.println("MyParent1");
}
}
//MyTest1
//MyParent1
//str
代码分析:首先运行main方法,对应我们上面初始化阶段的第6点,此为对类的主动引用,会引发类的初始化。因为在类的初始化阶段会执行static语句块里面的内容,所以第一行打印MyTest1。接着调用MyParent1类的类变量str,对应上面的第2点读取一个类的静态字段,引发对MyParent1的类的初始化,打印出static语句块里面的内容MyParent1,最后输出str。
下面我们改造下把MyParent1类的str变量改为final修饰,但是用不同的方式赋值,一个是直接赋值,另一个是通过方法来赋值。输出结果我也给出来了,请看下面的注释
1. public static final String str = "str" ;
//MyTest1
//str
2. public static final String str = UUID.randomUUID().toString();
//MyTest1
//MyParent1
//f0759064-4d27-450f-b392-69d689678fdf
可以看到第一句使用final来修饰,MyParent1没有输出,那么说明MyParent1这个类没用初始化。而第二句同样是使用final修饰,但是给str变量赋值确实通过调用UUID类的方法来生成,输出的结果表示MyParent1类初始化了。被final修饰的常量,并且在编译器就能确定的话,对应public static final String str = "str" ;在MyTest1调用str常量时,会在MyTest1的常量池中生成str常量,所以不会初始化MyParent1这个类。而使用public static final String str = UUID.randomUUID().toString();在编译器是不知道str的值,所以必须初始化MyParent1类。对应上面初始化阶段的第2点:读取或者设置一个类的静态字段(被final修饰,已在编译期把结果放在常量池除外)。被final修饰,并且在编译器就知道的常量不会进行初始化。
2.第二个代码分析
public class MyTest4 {
public static void main(String[] args) {
MyParent4 myParent4 = new MyParent4() ;
MyParent4 myParent41 = new MyParent4() ;
}
}
class MyParent4{
static {
System.out.println("MyParent4 static code");
}
}
//MyParent4 static code
通过new来实例化对象,对类的主动使用,会输出MyParent4 static code。但是只输出一次,因为类加载了一次之后,是不会在加载的,所以初始化只进行了一次。相当于两个对象,但是对象的Class类只有一份,存在于方法区中。
3.第三个代码分析,分析了上面初始化的第5点:当初使化一个类的时候,如果发现其父类还没有进行初始化,那么需先初始化这个父类。
public class MyTest8 {
static {
System.out.println("MyTest8 static code");
}
public static void main(String[] args) {
System.out.println(Child.b);
}
}
class Parent{
static int a = 3 ;
static {
System.out.println("Parent static block");
}
}
class Child extends Parent{
static int b = 4 ;
static {
System.out.println("Child static block");
}
}
/**
* MyTest8 static code
* Parent static block
* Child static block
* 4
*/
同样的分析,main的使用,对MyTest8的主动使用,输出了MyTest8 static code。调用Child类的类变量b,引发对Child类的主动使用,这时发现它的父类还没有进行初始化,所以对Parent类进行初始化,此时又发现它的父类Object(所有类如果没用继承别的类它的父类都是Object)没用初始化,这时应该先初始化类Object,然后初始化Parent,输出Parent static block,接着初始化Child类,输出Child static block,最后输出4.为了让你更好的知道这几个类的初始化顺序,我们可以在idea运行这个类前,设置vm options为-XX:+TraceClassLoading。这个参数会在类输出的时候打印类加载信息。
[Loaded java.lang.Object from D:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
..........
MyTest8 static code
[Loaded java.net.Inet6Address$Inet6AddressHolder from D:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
[Loaded MyClassLoader.Parent from file:/D:/Users/Maodun/IdeaProjects/myWork/MyJvm/target/classes/]
[Loaded MyClassLoader.Child from file:/D:/Users/Maodun/IdeaProjects/myWork/MyJvm/target/classes/]
[Loaded java.net.Socket$2 from D:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
Parent static block
Child static block
4[Loaded java.net.SocketInputStream from D:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar]
在其中截取了一部分,可以看出来与我们上面的分析是吻合的。先加载Object类,然后是Parent,Child.
4.第四个代码分析
public class MyTest10 {
public static void main(String[] args) {
System.out.println(Child3.a);
Child3.doSomething();
}
}
class Parent3{
static int a = 3 ;
static {
System.out.println("Parent3 static code");
}
static void doSomething(){
System.out.println("do something");
}
}
class Child3 extends Parent3{
static {
System.out.println("Child3 static code");
}
}
/**
* Parent3 static code
* 3
* do something
*
*/
这个代码没有输出Child3 static code。说明在调用Child3.a时,初始化了Parent3类,而没有初始化Child3类。即使是通过Chile3类来调用的。让你对类的加载更加清晰。
此外在new一个对象数组的时候,也不会对类进行初始化。
public class MyTest5 {
public static void main(String[] args) {
MyParent5[] myParent5 = new MyParent5[1] ;
}
}
class MyParent5{
static {
System.out.println("MyParent5 static code");
}
}
执行这个代码,没有输出语句,说明MyParent5没有进行初始化。 对于数组实例来说,其类型是由JVM在运行期间动态生成的,表示为[LMyClassLoader.MyParent5; 这种形式,而不是 MyParent5类,所以MyParent5类是不会初始化的。