虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的时机:类从被加载到虚拟机的内存中,到卸载内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。
本篇只叙述类加载过程中的加载、初始化阶段以及类加载器和双亲委派模型。
什么时候加载?
Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
什么时候初始化?
1.遇到new、getstatic、putstatic或invokestatic这4条指令时,如果类没有进行过初始化,则需要先触发其初始化。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄,并且这个方· 法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
代码:
package com.shamgod.classload;/** * 将被初始化的类 * @author ShammGod * */public class ClassLoad1 { static int i = 1; static { System.out.print(ClassLoad1.class + " 被初始化了!"); } static void info() { System.out.println("I'm a class"); }}
情况1:
package com.shamgod.classload;/** * 类初始化测试类 * @author ShammGod * */public class TestClassLoad { public static void main(String[] args) { //new指令 ClassLoad1 classLoad1 = new ClassLoad1(); //输出:class com.shamgod.classload.ClassLoad1 被初始化了! }}
1 package com.shamgod.classload; 2 /** 3 * 类初始化测试类 4 * @author ShammGod 5 * 6 */ 7 public class TestClassLoad { 8 9 public static void main(String[] args) {10 11 //getstatic指令12 System.out.println(ClassLoad1.i);13 //输出:class com.shamgod.classload.ClassLoad1 被初始化了!114 }15 }
1 package com.shamgod.classload; 2 /** 3 * 类初始化测试类 4 * @author ShammGod 5 * 6 */ 7 public class TestClassLoad { 8 9 public static void main(String[] args) {10 11 //putstatic指令12 ClassLoad1.i = 20;13 //输出:class com.shamgod.classload.ClassLoad1 被初始化了!14 }15 }
1 package com.shamgod.classload; 2 /** 3 * 类初始化测试类 4 * @author ShammGod 5 * 6 */ 7 public class TestClassLoad { 8 9 public static void main(String[] args) {10 11 //invokestatic指令12 ClassLoad1.info();13 //输出:class com.shamgod.classload.ClassLoad1 被初始化了!I'm a class14 }15 }
情况2:
package com.shamgod.classload;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;/** * 类初始化测试类 * @author ShammGod * */public class TestClassLoad { public static void main(String[] args) throws Throwable { //反射获取该ClassLoad1类的类对象 Class class1 = ClassLoad1.class; //得到该类对象的所有字段 Field[] fields = class1.getDeclaredFields(); for (Field field : fields) { //使用java.lang.reflect包的Field类的get()方法进行反射调用,触发初始化 System.out.print(field.get(class1)); //输出:class com.shamgod.classload.ClassLoad1 被初始化了!1 } }}
情况3:
1 package com.shamgod.classload; 2 /** 3 * 将被初始化的类 4 * @author ShammGod 5 * 6 */ 7 public class ClassLoad1 { 8 9 static int i = 1;10 static {11 System.out.print(ClassLoad1.class + " 被初始化了!");12 }13 14 static void info() {15 System.out.println("I'm a class");16 }17 }18 class ClassLoad2 extends ClassLoad1{19 static {20 System.out.println(ClassLoad2.class + " 被初始化了!");21 }22 }
1 package com.shamgod.classload; 2 3 /** 4 * 类初始化测试类 5 * @author ShammGod 6 * 7 */ 8 public class TestClassLoad { 9 10 public static void main(String[] args){11 ClassLoad2 classLoad2 = new ClassLoad2();12 }13 }
情况4:
1 package com.shamgod.classload; 2 3 /** 4 * 类初始化测试类 5 * @author ShammGod 6 * 7 */ 8 public class TestClassLoad { 9 //执行主类的静态代码块,类加载过程中的初始化阶段会执行该代码块10 static {11 System.out.print("我是一个要执行的主类(我有main方法!)");12 }13 14 public static void main(String[] args){15 System.out.print("main方法执行了!");16 }17 //输出:我是一个要执行的主类(我有main方法!)main方法执行了!18 }
情况5:TODO
加载:“加载”是“类加载”过程的一个阶段,在加载阶段需要完成3个步骤:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
初始化:类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
类加载器:虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为:“类加载器”。
双亲委派模型:
启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException
),子加载器才会尝试自己去加载。
双亲委派模型的好处:
Java类随着他的类加载器一起聚类了带有优先级的层次关系。无论哪个类加载器要加载这个类最终都会传递到最顶层的启动类加载器,再向下逐层尝试,这就能保证所有相同的类在各种类加载器环境中都是同一个类,若没有类似的向上传递的过程,由各个类加载器自行去加载的话,系统中会出现多个不同的一个名称的类,各种类的行为无法保证一致,体系崩坏。