【JVM虚拟机】虚拟机的类加载机制

一、概述

1. 类的生命周期

类从被加载到虚拟机内存开始、到卸载处内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载 七个阶段,其中验证、准备、解析 统称为连接。

2. 什么情况进行类加载

虚拟机明确规定,有且只有以下五种:

  1. 使用new关键字实例化对象时、读取或者设置一个静态字段时以及调用一个类的静态方法
  2. 使用java.lang.reflect包的方法,对类进行反射调用时,如果没有进行初始化,则要先触发其初始化
  3. 当初始化一个类时,如果发现它的父类还没有初始化,则要先触发父类的初始化
  4. 当虚拟机启动时,用户要指定一个要执行的主类(main),虚拟机会先初始化这个主类
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果时REF_getStatic、REF_putStatic、REF_invoke Static的方法句柄,且这个方法句柄对应的类没有被初始化,则需要先触发其初始化。

这五种情况被称之为对一个类的主动引用,除此之外,所有引用类的方法都不会触发初始化,称为被动引用,比如下面三种情况,都不会触发类加载:

  1. 通过子类引用父类的静态变量,不会导致子类初始化
  2. 通过数组定义来引用类,不会触发类的初始化
  3. 常量在编译期间就会存入调用类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义常量类的初始化

二、类加载过程

类加载的全过程就是:加载、验证、准备、解析、初始化 这五个阶段,接下来我们来逐一看看。

1. 加载

“加载”是“类加载”过程的一个阶段。在“加载”过程中,虚拟机完成了以下事儿:

  • 通过一个类的全限定名来获取定义此类的二进制流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

一个非数组类型的加载阶段,是可控的,因为在加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成
对于数组类而言,因为数组本身不通过类加载器创建,它是由Java虚拟机直接创建的,但数组类和类加载器也有很大关系,因为数组中存放的元素类型最终是要靠类加载器去创建

2. 验证

验证时连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含信息符合当前虚拟机的要求,且不会危害虚拟机的自身安全。这个阶段也是很重要的,因为它的是否严谨直接决定了Java虚拟机是都能承受恶意代码的攻击,从性能上来说,验证阶段的工作量也占了整个类加载的相当大一部分。验证阶段大致会完成以下四个方面的检验动作:文件格式检验、元数据检验、字节码检验、符号引用验证

(1) 文件格式验证

这一阶段主要验证字节流是都符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段包括以下几点:

  • 是否以魔数0xCAFEBABE开头
  • 主次版本号是否在当前虚拟机的处理范围内
  • 常量池中的常量中是否由不被支持的常量类型
  • 指向常量的各种索引值中是都由只想不存在的常量或不符合类型的常量
  • Class文件中的各个部分以及文件本身是否有被删除或者附加的其它信息

实际上第一阶段的验证点远远不止这些,该验证主要目的就是保证输入的字节流能正确的解析并存储在方法区中,这个阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存中的方法区进行存储,所以后面三个验证都是基于方法区的存储结构进行的,不会再直接操作字节流。

(2) 元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段的验证点有:

  • 这个类是否有父类(除了java.lang.Object类之外,所有类都要有父类)
  • 这个类的父类是否继承了不允许继承的类(final修饰)
  • 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
  • 类中的字段方法是否与父类产生了矛盾(如,覆盖了父类的final字段,或者出现不符合规则的方法重载)
(3) 字节码验证

第三阶段是整个验证的过程中最复杂的一个阶段,主要是通过数据流和控制流的分析,确定程序语义是否合法、符合逻辑。在第二阶段对元数据进行验证后,这个阶段对类的方法体进行校验分析,以保证被校验类的方法在运行时不会做出危害虚拟机安全的事情,如:

  • 保证任何时刻操作数栈的数据类型与指令代码序列都能配合工作
  • 保证跳转指令不会跳转到方法体意外的字节码指令上
  • 保证方法体中的类型转换是有效的(例如可以把子类的对象赋给父类的引用是安全的,但把父类的对象赋值给子类的甚至是没有关系的对象是不合法的)
(4) 符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段——解析中完成。符号引用验证可以看作是对类自身意外的信息进行匹配性校验,通常有以下工作:

  • 符号引用中通过字符串描述的全限定名能否找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号中引用的类、字段、方法的访问性(private、protected、public)是否可以被当前类访问

3.准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这个阶段会在方法区分配所有静态类变量的内存,而不包括实例变量,实例变量在对象实例化时放入Java堆中。准备阶段过后,静态变量的值就是相应的数据类型的初始零值。如果类的字段中存在final修饰的不可变的字段,那么就会在准备阶段就将该字段进行初始化为其属性设置的值。

4. 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。其中包括:类或接口解析、字段解析、类方法解析、接口方法解析。这里就不展开讲了。

符号引用:符号引用是一组符号来描述所引用的对象,符号可以是任何形式的字面量,只要使用时能定位到目标即可,符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄(一种特殊的智能指针)。直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定存在内存中。

5. 初始化

初始化是类加载的最后一步。到了初始化阶段,才开始执行类中定义的Java代码。在准备阶段,变量已经被赋值为系统的初始零值,而在初始化阶段,则根据程序员通过程序主观设定。

三、双亲委派模型

绝大部分的Java程序都会用到以下3中系统提供的类加载器。

  • 启动类加载器:负责讲存在<JAVA_HOME>\lib目录或者-Xbootclasspath参数指定的路径中的,并且被虚拟机识别的类加载到虚拟机内存中。启动类加载器无法被Java程序直接使用
  • 扩展类加载器: 负责加载<JAVA_HOME>\lib\ext目录或者被java.ext.dirs系统变量指定的路径中的所有类库,可以被开发者直接使用
  • 应用程序类加载器:这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,也称为系统类加载器,负责加载用户类路径(classPath)上指定的类库,开发者可直接使用。

我们的程序就是有这三种类加载器配合来进行加载的,还可以引入自己的类加载器。这些类加载器的关系如图:
在这里插入图片描述

如图这种类加载器之间具有的层次关系就称之为双亲委派模型
双亲委派模型要求除了顶级的启动类加载器之外,其它的类加载器都应该有自己的父类加载器,这里的父类和子类关系一般不会使用继承,而使用组合来复用父类的代码。

双亲委派模型的工作过程就是:如果一个类加载器收到了加载类的请求,它首先不会自己去尝试加载这个类,而是把这个类加载的请求交给父类加载器来完成。每一层都是这样的工作,所以所有的加载请求最后都会到达顶级的启动类加载器中,只有当父类加载器反馈自己无法加载这个请求的类时,子加载器才会尝试自己去加载。

使用双亲委派模型保证了程序运作的稳定,其实现也不复杂,在java.lang.ClassLoader类中的loadClass()方法的代码如下:

 protected synchronized Class loadClass(String name, boolean resolve)  
        throws ClassNotFoundException {  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        try {  
            if (parent != null) {  
                c = parent.loadClass(name, false);  
            } else {  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
}  

可以看出其逻辑是很清晰的,首先判断是否已经被加载过,如果没有再判断父类是否为null,不为null就直接调用父类的加载器去加载,否则调用启动类加载器来加载,如果父类抛出异常表示无法加载是调用自身的findClass()方法来加载类。

以上就是关于类加载的一些基本知识,本文参考《深入理解Java虚拟机》一书,也推荐大家阅读。如果文章有任何问题欢迎提出您宝贵的意见和建议,也欢迎点赞关注一起进步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值