第七章:虚拟机类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析并初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的时机
类从加载到虚拟机内存中开始,到卸载处内存,生命周期包括:
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
解析阶段在某些情况下可以在初始化阶段之后在开始,这是为了支持Java语言的运行时绑定(动态绑定)
Java虚拟机规范严格规定了有且只有5中情况必须立即对类进行“初始化”:
- 遇到new,getstatic,putstatic,invokestatic四条字节码指令时,需要先初始化。4条指令的常见的场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰除外)及调用一个类的静态方法的时候。
- 使用Java.lang.reflect包的方法对类进行反射调用时。
- 当初始化一个类时,发现其父类还没有进行初始化,则需要先出发对其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类。
- 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_get_static,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化,则需要先触发其初始化。
这五种场景称为对一个类进行主动引用。
不会触发初始化的引用称之为被动引用。
- 通过子类引用父类的静态字段,不会导致子类的初始化。
- 通过数组定义类引用类,不会触发此类的初始化。
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
类加载的过程
加载
加载阶段,虚拟机主要完成以下三年事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
关于通过类的全限定名获取定义此类的二进制字节流,有很多方式
- 从zip包中读取,jar包,war包等
- 从网络中读取,比如Applet
- 运行时计算生成使用最多的就是动态代理
- 等等
验证
验证阶段大致会完成以下4各阶段的检验动作
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
有两个容易混肴的概念:
- 这时候进行内存分配的仅包括类变量(被static修饰的变量)不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆内存中。
- 这里所说的初始值“通常情况”是数据类型的零值。
解析
解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。
符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。
直接引用
直接引用可以直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关。有了直接引用,则引用的目标一定在内存中存在。
解析动作主要针对:
- 类或接口
- 字段
- 类方法
- 接口方法
- 方法类型
- 方法句柄
- 调用点限定符
7类符号引用进行。后三种是与JDK1.7动态语言支持息息相关。
初始化
初始化阶段,在真正开始执行类中定义的Java程序代码
类加载器
类加载过程“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块成为“类加载器”。
类与类加载器
判断两个类是否相等,只有这两个类是由同一个类加载器加载的前提下才有意义。
双亲委派模型
对于Java虚拟机来说,只存在两种不同的类加载器,一种是启动类加载器(Bootstrap ClassLoader),这个加载器是由C++实现的,第二个是其他的类加载器,由Java实现,独立于虚拟机外部,并且继承自java.lang.ClassLoader。
对于Java开发人员来说,分为三种:
- 启动类加载器:Bootstrap ClassLoader。负责将存放在
\lib目录中的类库加载到虚拟机内存中。这个加载器无法直接被Java程序引用。 - 扩展类加载器:Extension ClassLoader:这个加载器负责加载
\lib\ext 目录,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。 - 应用程序类加载器:Application ClassLoader。这个加载器由sun.misc.Launcher$App-ClassLoader实现。也叫做系统类加载器,负责加载用户类路径上指定的类库。开发者可以直接使用这个类加载器。
三者的关系是:类加载器的双亲委派模型。
工作过程是:如果一个类加载器收到类加载的请求,他首先不会自己去尝试加载这个类,而是将请求委派给父类加载器去完成,所有的加载请求最终都应该传递到顶层的启动类加载器中,只有当父类无法加载时,子加载器才会尝试加载。
破坏双亲委派模型
- 模型自身的缺陷,基础类被用户代码调用,基础类又要调用用户代码。例如JNDI服务。解决方案就是线程上下文类加载器。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法设置。
- 用户对程序动态性的追求导致。例如hotswap。