7.1 概述
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
7.2 类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段(运行时绑定特性比如动态绑定或晚期绑定时),它在某些情况下可以在初始化阶段之后再开始。按部就班地“开始”,而不是按部就班地“进行”,强调这点是因为这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。
关于在什么情况下需要开始类加载过程的第一个阶段“加载”,具体要求,但是对于初始化阶段,却严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之
前开始):
- 遇到new(new关键字实例化对象时)、getstatic(读或写静态字段)、putstatic(写一个静态值)或invokestatic(调用一个静态方法)这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。
- 使用java.lang.reflect包(反射包)的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
- 当初始化类的时候,如果其父类还没初始化,则需要先初始化父类。(例子未初始化父类)
- 当虚拟机启动时,用户需要指定一个要执行的Main类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 当一个接口中定义了默认方法时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
7.3 类加载的过程
7.3.1 加载
把文件加载到内存(虚拟机)里面
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
这三点要求其实并不是特别具体,留给虚拟机实现与Java应用的灵活度都是
相当大的。例如“通过一个类的全限定名来获取定义此类的二进制字节流”这条规则,它并没有指明二进制字节流必须得从某个Class文件中获取,那么我们可以从ZIP压缩包中读取,从网络中获取,由其他文件生成,从数据库中读取,从加密文件中获取等获取方式,对于数组类而言,有基本类型、引用类型这些都会有所变化
7.3.2 验证
唯一要求就是是否符合我们的全部约束要求
1.文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。
2.元数据验证
是对字节码描述的信息进行语义分析
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾
3.字节码验证
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
4.符号引用验证
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问。
7.3.3 准备
是类中定义的变量(静态变量不加final的被static修饰的变量)分配内存设初始值的一个阶段,分配内存给初始值(0值)
这里是只有static的不加final。
public static int value = 123;
那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。
public static final int value = 123;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置
将value赋值为123,就不是纯的类变量了。
7.3.4 解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用(具体的内存地址)的过程,
7.3.5 初始化
类加载的最后阶段,这时一开始为0的值全都开始赋初值了(如value开始赋值123了)
7.4 类加载器
7.4.2 双亲委派模型
这里是对于1.8及以前的设计的版本介绍三层类加载器:
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系,能有效的保证我们Java的核心类不被篡改,维护我们Java环境运行的稳定性。例如类java.lang.Object,始终由启动类加载器进行加载。如果是自己写的与rt.jar同名的Java类,会发现他只会正常编译而不加载。
7.4.3 破坏双亲委派模型
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前。
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,有些父类加载器加载需要访问子类加载器加载的类。(在JDK 6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。)
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。(代码热替换的问题,想达到新写的代码直接就可以使用,不需要重启)
这时IBM提出了一个OSGI的模型,这个模型把原来优先级的加载结构改为了网状结构
- 将以java.*开头的类,委派给父类加载器加载。
- 否则,将委派列表名单内的类,委派给父类加载器加载。
- 否则,将Import列表中的类(导入的类),委派给Export(导出列表的类)这个类的Bundle的类加载器载。
- 否则,查找当前Bundle的ClassPath(查找当前的类路径),使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
- 否则,查找Dynamic Import(动态导入的类)列表的Bundle,委派给对应Bundle的类加载器载。
- 否则,类查找失败
相互之间的引用就构成了一个网状的结构,不再有优先级了,那么我们想换这个类的时候,也就有了方式,我们连带模块一起替换掉,类的各类信息全部替换掉
参考书籍:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明