Java虚拟机:Java类加载机制

中国人最喜欢访问的网站
只要注册ofo就送你10块钱,还等什么,快来注册吧

源代码(.java)会被编译为class文件(.class文件),.class文件描述了类的各种信息,.class文件需要加载到虚拟机之后才能运行和使用。

类加载使用的7个阶段

类从加载到虚拟机到到卸载出内存,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸载(Unloading)这7个阶段。
类加载使用的7个阶段
其中验证、准备、解析这个三个阶段统称为连接(Linking)
加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的,而解析阶段不一定:它在某些情况下可以初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。
加载、验证、准备、解析、初始化五个阶段组成了一个完整的类加载过程。
卸载属于GC的工作。

加载(Loading)

有两种时机会触发类加载:

1、预加载。虚拟机启动时加载,加载的是JAVA_HOME/lib/下的rt.jar中的.class文件,由于rt.jar包中的类经常被使用,因此随着虚拟机启动一起被加载。

验证rt.java包中的类是否在虚拟器启动的时候被加载
1
2
3
public class Test {
public static void main(String[] args) {}
}

编译运行该类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>javac Test.java
>java -XX:+TraceClassLoading Test
[Opened C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.io.Serializable from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.Comparable from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.String from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.reflect.Type from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.Class from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.Cloneable from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
[Loaded java.lang.ClassLoader from C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar]
...

2、运行时加载。虚拟机在用到一个.class文件的时会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。

加载阶段做的事情

1、获取.class文件的二进制流
2、将.class文件中包含的类信息、静态变量、字节码、常量等内容放入方法区中
3、在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机是放在方法区中的。

虚拟机规范对这三点并没有做详细的规定,因此虚拟机实现的灵活度是相当大的。
加载阶段对于开发者来说是可控性最强的一个阶段。

验证

连接阶段的第一步,主要是为了确保.class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

.class文件不一定要从Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生.class文件。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

验证阶段主要包含以下几个工作:

1、文件格式验证
.class文件的第5~第8个字节表示的是该.class文件的主次版本号,验证的时候会对这4个字节做一个验证,高版本的JDK能向下兼容以前版本的.class文件,但不能运行以后的class文件,即使文件格式未发生任何变化,虚拟机也必须拒绝执行超过其版本号的.class文件。例如有一段java代码是JDK1.6编译的,那么JDK1.6、JDK1.7能运行编译出来.class文件,而JDK1.5、JDK1.4以及更低的JDK版本是无法运行这个.class文件的。如果运行,会抛出java.lang.UnsupportedClassVersionError

2、元数据验证

3、字节码验证

4、符号引用验证

准备

准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。
1、这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
2、这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如public static int value = 123;value在准备阶段过后是0而不是123,给value赋值为123的动作是在初始化阶段才进行的;而public static final int value = 123;,在准备阶段,虚拟机就会给value赋值为123

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

初始化阶段是类加载过程的最后一步,初始化阶段是真正执行类中定义的Java字节码的过程。初始化过程是一个执行类构造器<clinit>()方法的过程,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。初始化阶段做的事就是给static变量赋予用户指定的值以及执行静态代码块。

注意一下,虚拟机会保证类的初始化在多线程环境中被正确地加锁、同步,即如果多个线程同时去初始化一个类,那么只会有一个类去执行这个类的<clinit>()方法,其他线程都要阻塞等待,直至活动线程执行<clinit>()方法完毕。因此如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。不过其他线程虽然会阻塞,但是执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程不会再次进入<clinit>()方法了,因为同一个类加载器下,一个类只会初始化一次。实际应用中这种阻塞往往是比较隐蔽的,要小心。

Java虚拟机规范严格规定了有且只有5种场景必须立即对类进行初始化,这4种场景也称为对一个类进行主动引用:

1、使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰的静态字段除外)、调用一个类的静态方法的时候
2、使用java.lang.reflect包中的方法对类进行反射调用的时候
3、初始化一个类,发现其父类还没有初始化过的时候
4、虚拟机启动的时候,虚拟机会先初始化用户指定的包含main()方法的那个类

除了上面4种场景外,所有引用类的方式都不会触发类的初始化,称为被动引用:

1、子类引用父类静态字段,不会导致子类初始化。

1
2
3
4
5
6
public class SuperClass {
public static String value = "value";
static {
System.out.println("SuperClass init");
}
}
1
2
3
4
5
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init");
}
}
1
2
3
4
5
public class TestMain {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}

运行结果为:

1
2
SuperClass init
value

2、通过数组定义引用类,不会触发此类的初始化

1
2
3
4
5
public class TestMain {
public static void main(String[] args) {
SuperClass[] scs = new SuperClass[10];
}
}

运行之后没有任何输出

3、引用静态常量时,常量在编译阶段会存入类的常量池中,本质上并没有直接引用到定义常量的类

1
2
3
4
5
6
7
public class Constant {
public static final String CONSTANT = "CONSTANT";
static {
System.out.println("Constant init");
}
}

1
2
3
4
5
public class TestMain {
public static void main(String[] args) {
System.out.println(Constant.CONSTANT);
}
}

运行结果为:

1
CONSTANT

快下载安装吧,今天头条送你钱啦!!!!
中国人都在使用的地球上最好玩的游戏
中国人都在使用的地球上最好玩的游戏
中国人都在使用的地球上最快的浏览器
中国人都在使用的地球上最厉害的安全软件
中国人都在使用的地球上最好的看图王
中国人都在使用的地球上最快速的视频软件
中国人都在使用的地球上最全的视频软件
中国人都在使用的地球上最好最全的压缩软件
中国人都在使用的地球上最好的音乐播放器
中国人都在使用的地球上最安全的杀毒软件
中国人都在使用的地球上最全的影视大全