7.1 概述
虚拟机把描述类的数据从Class文件加载到内存,对数据进行校验,转换解析,初始化,最终形成可以被java虚拟机直接使用的java类型,这就是类加载机制。
7.2 类的加载时机
类加载时机
类从被加载到内存中到卸载出内存,所经过的生命周期为:加载-验证-准备-解析-初始化-使用-卸载,其中验证-准备-解析三个部分统称为连接。
解析阶段和其他的阶段不同,解析阶段可以在初始化之后执行,这时为了支持java语言的运行时绑定(动态绑定)。
什么时候需要开始初始化?有且只有5种情况如下:
遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其类的初始化。
当虚拟机启动时,用户需要指定一个需要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,那么需要先触发初始化。
需要注意的是:通过子类引用父类的静态字段,不会导致子类初始化通过数组定义来引用来,不会触发此类初始化常量在变异阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
7.3 类加载的过程
7.3.1 加载
1)通过一个类的全限定名来获得此类的二进制字节流。
2)将这个直接流的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的class对象,作为方法区这个类的数据访问入口。
7.3.2 验证
验证代码是否正确 目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求并且不会危害虚拟机自身安全。
7.3.3 准备
准备阶段就是为类的变量正式分配内存并设置初始值。这个初始值与初始化不是同一个概念。
比如
public static int value = 12;
这个阶段value的值为0 而不是12。value赋值为12的阶段
是在初始化的过程中出现的。
java所有的基本类型都赋值为零值。(简单来说就是0 or null,0.0f,false等)
这里可以明确,类的属性是会默认初始值的。而局部变量没有初始值。所以是未定义的。
7.3.4 解析
将常量池内的符号引用替换为直接引用。
7.3.5 初始化
1)遇到new,getstatic,putstatic,invokestatic 这4条指令的时候。对应场景:
实例化一个类,读取或者设置一个类的静态字段,调用一个类的静态方法时候。
2)使用反射方法调用的时候,需要先初始化。
3)当初始化一个类时,需要先初始化父类。
4)当虚拟机启动时,需要指定一个启动类(main类),虚拟机会首先初始化这个类。
5)当使用jdk1.7动态语言时候,具体情况本文不做分析。
7.4 类加载器
7.4.1 类与类加载器
相同的二进制代码,由不用的加载器加载,对应的是不同的类型对象。
所以判断相同的类对象,必须是相同的二进制代码+相同的类加载器。
7.4.2 双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,
只有当父类加载器反馈自己无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载
好处:
Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类Object,它放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类