你的位置:首页 > Java教程

[Java教程]Java类加载机制

什么是类加载机制

java类加载机制,指的是jvm把class文件转换成二进制数据读取到内存,将其放在方法区内,然后再堆区创建相应的Java对象(java.lang.class)。再简单点来说,就是jvm解析class文件并最终生成运行时Java对象的过程。

类加载过程

类从被加载到虚拟机内存开始直到卸载出内存,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。其中验证、准备、解析三个部分统称为连接(Linking)。加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,解析阶段则不一定,在某些情况下可以再初始化之后再开始,这是为了支持Java动态绑定。

类加载时机

主动引用:类被主动引用之后会触发初始化过程(加载、验证、准备需在此之前开始)

1.遇到new、getstatic、putstatic和invokestatic这4条字节码指令是,如果类没有进行过初始化,则触发初始化。生成这4条指令最常见的代码场景分别对应:使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用类的静态方法。

2.使用反射方法对类进行调用时,如果类没有初始化,则触发初始化。

3.当初始化一个类的时候,如果发现其父类还没有初始化,则触发父类初始化。

4.当jvm启动时,用户需要指定一个执行的主类(包含main()方法的类),jvm会先初始化这个类。

5.当使用jdk7+的动态语言支持时,如果java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化,则触发初始化。

被动引用:一个类如果是被动引用的话,不会触发初始化

1.通过子类引用父类的静态字段,不会导致子类初始化,对于静态字段,只有直接定义该字段的类才会被初始化,因此对于子类引用父类静态字段,只会初始化父类,而不会初始化子类。

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

3.常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量类的初始化。

加载

查找并加载类的二进制数据,在加载阶段,jvm需要完成以下3件事情(参考java.lang.ClassLoader.loadClass()方法):

1.通过一个类的全限定名来获取定义此类的二进制字节流,一般从.class文件获取,也可能从网络、数据库或者动态生成;

2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;

3.在内存中(Java堆)中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口。

加载阶段和连接阶段的部分内容(如一部分class文件格式的校验)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些穿插在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

另外,相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以通过自定义类加载器来完成加载。

验证

验证是链接阶段的第一步,目的是为了确保class文件的字节流中包含的信息符合jvm规范,并且不会危害jvm自身安全。大致分为4个方面的校验:

1.文件格式验证,验证class字节流是否符合class文件格式规范,例如:是否以魔数(magic)0xCAFEBABE开头、主次版本号是否在当前jvm处理范围之内、常量池中的常量是否有不被支持的类型;

2.元数据验证,对字节码描述的信息进行语义分析(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息;

3.字节码验证,进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为;

4.符号引用验证,这是最后一个验证,它发生在虚拟机将符号引用转化为实际引用的时候,主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性校验。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。这时候进行内存分配的仅包括类变量(static修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一起分配在堆中。另外,这里所述初始值“通常情况”下是当前数据类型的“零值”,假设一个类变量定义为:

public static int i=10;

那么初始值是0而不是10。因为此时尚未执行任何java方法,赋值操作的putstatic指令是程序被编译后,存放于类构造器方法中,所以赋值操作会在初始化阶段执行。

之所以上述表述为“通常情况”下,是因为对于final修饰的类变量,会在准备阶段初始化为指定的值,即i=10,而不是0。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。 

符号引用:以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用于虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。

直接引用:可以是直接指向目标的指针、相对偏移量或是一个能够间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用那么引用的目标必定已存在内存中。

1.类或接口的解析:判断所要转化成的直接引用是对数组类型还是普通的对象类型的引用,从而进行不同的解析。

2.字段解析:在本类中查找是否包含有简单名称和字段描述符都与咪表相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和他们的父接口;如果还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。

3.类方法解析:对类方法的解析与对字段类似,只是多了判断该方法所处位置是类还是接口接口,另外对类方法的匹配搜索是先搜索父类再搜索接口。

4.接口方法解析:类似类方法解析,接口不会有父类,因此只递归向上搜索父接口即可。

初始化

初始化是类加载过程的最后一步,前面的类加载过程,除了加载阶段开发者可以通过自定义类加载器参与之外,其余操作全都有虚拟机完成。到了初始化阶段,才真正开始执行类中定义的java程序代码。

初始化阶段是执行类构造器<clinit>()方法的过程。

1.<clinit>()方法是有编译器自动收集类中的所有变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。

2.<clinit>()方法与类的构造函数不同,它不需要显示的调用父类构造器,虚拟机会保证子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中第一个执行的<clinit>()方法的类一定是java.lang.Object。

3.由于父类的<clinit>()优先执行,也就意味着父类的静态语句块要优先于子类的变量赋值操作。

4.<clinit>()方法对于类或接口并不是必须的,如果一个类中没有静态语句块也没有对变量的赋值操作,编译器可以不生成它。

5.接口中可能会有变量赋值操作,因此接口也可能会生成<clinit>()方法。但是接口与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也不会执行接口的<clinit>()方法。

6.虚拟机会保证一个类的<clinit>()方法在多线程下被正确的加锁和同步。因此如果一个类的<clinit>()方法耗时较长,那么可能会造成线程阻塞。

双亲委派模型

JVM预定义的三种类型类加载器: 
     1.启动(Bootstrap)类加载器:是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。 
     2.标准扩展(Extension)类加载器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。 
     3.系统(System)类加载器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。 


双亲委派机制描述: 
     某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

相关代码示例

 

public class ClassLevelFirst { static {  System.out.println("first static"); } public static int firstValue = 1; public ClassLevelFirst() {  System.out.println("first init"); }}public class ClassLevelSecond extends ClassLevelFirst { static {  System.out.println("second static"); } public static int secondValue = 2; public ClassLevelSecond() {  System.out.println("second init"); }}public class ClassLevelThird extends ClassLevelSecond { static {  System.out.println("third static"); } public static int thirdValue = 3; public static final String thirdStrValue = "not init"; public ClassLevelThird() {  System.out.println("third init"); }}
定义三个类
问题1:  public static void main(String[] args) {    new ClassLevelThird(); }问题2:  public static void main(String[] args) {    System.out.println(ClassLevelThird.firstValue); }问题3:public static void main(String[] args) {  ClassLevelThird[] classLevelThirds = new ClassLevelThird[10]; }问题4:public static void main(String[] args) {    System.out.println(ClassLevelThird.thirdStrValue); }
问题
参考答案:1.first staticsecond staticthird staticfirst initsecond initthird init2.first static13.无输出4.not init
答案

最后再贴一个有意思的问题(引用自网络,侵删):

public class TestClassLoaderExample { public static void main(String[] args) {  staticFunction(); } static TestClassLoaderExample st = new TestClassLoaderExample(); static {  System.out.println("1"); } {  System.out.println("2"); } TestClassLoaderExample() {  System.out.println("3");  System.out.println("a=" + a + ",b=" + b); } public static void staticFunction() {  System.out.println("4"); } int a = 110; static int b = 112;}
有意思的程序

思考下,打印的输出是什么样子呢?

 

答案见:

      云南丽江游组团云南丽江游组团云南丽江游组团云南丽江自由行费用云南丽江自由行费用云南丽江自由行费用MAXIM MAX3661EAG Datasheet MAXIM MAX3662GAG Datasheet  MAX6310UK40D2+T Datasheet  MAX6310UK40D2-T Datasheet 温州海外旅行 温州海外旅行 温州海外旅行 温州海外旅行公司 温州海外旅行公司 温州海外旅行公司