JVM--虚拟机加载机制

1、 概述

​   虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制.

2、 类加载的时机

​   一个类的生命周期:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading)七个阶段.验证,准备,解析这三个阶段统称为连接.

类的生命周期

​   加载,验证,准备,初始化和卸载这五个阶段的先后顺序是确定的,而解析阶段则不一定,某些情况下也可能在初始化阶段之后进行.

​   加载阶段的执行交由虚拟机的具体实现来自由把握,而初始化阶段则有严格的规定了有且只有5种情况必须立即对类进行初始化:

  • 遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,就需要先触发其初始化.
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则先进行初始化.
  • 对一个类进行初始化的时候,发现它的父类没有进行初始化,就对父类进行初始化.
  • 虚拟机启动时,用户制定一个要执行的主类,虚拟机就会初始化这个主类.
  • 使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄没有初始化,那么先进行初始化.

​   对于接口的初始化,与类有不同的只有第3点:当真正使用到父接口的时候(如引用接口中定义的常量),才会初始化父接口.

3、 类加载的过程

​   类加载的全过程就是加载,验证,准备,解析和初始化五个阶段所执行的动作.

 3.1 加载

​   在加载阶段,虚拟机要完成以下三件事:

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

​   这三点规范名要求的并不算具体,如第一条就没有限制从哪里获取二进制字节流,可能来自本地的Class文件,也可能给来自压缩包(jar,war),还有可能来自网络(Applet).

​   对于数组类,情况有些不同.数组类本身不通过类加载器创建,而是由Java虚拟机直接创建的.但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的.数组类的创建过程遵循以下规则:

  • 如果数组的组件类型是引用类型,那么递归使用之前定义的方式去加载这个组件类型.数组C将在加载该组件类型的类加载器的类名空间上被标识.
  • 如果数组的组件类型不是引用类型,Java虚拟机将会把数组C标记为与引导类加载器关联.
  • 数组类的可见性与其组件类型的可见性一致,如果组件类型不是引用类型,那么可见性默认设置为public

​   加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义.然后在内存中实例化一个java.lang.Class对象.

 3.2 验证

​   验证是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求.

​   Java代码是相对安全的语言,许多错误在编译阶段就会被发现,并拒绝编译,但Class文件中已经生成的字节码指令不一定经过了编译(Class文件不一定经过了编译,可以由任何方式产生,如直接使用16进制编写).如果不对Class文件进行检查,可能会载入有害的字节流导致系统崩溃.

​   验证阶段大致上会完成下面4个阶段的验证动作:

 1). 文件格式验证

​   要验证字节流是否符合Class文件格式规范.包含以下验证点:

  • 是否以魔数0xCAFEBABE开头
  • 主次版本号是否在当前虚拟机的处理范围之内.
  • 常量池的常量中是否有不被支持的常量类型.
  • 常量池中的各种索引是否有效

 2). 元数据验证

​   对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求.验证点如下:

  • 这个类是否有父类(除了java.lang.Object类之外)
  • 是否继承了不允许被继承的类(final修饰)
  • 如果这个类不是抽象类,是否实现了父类或者接口中的所有方法.
  • 类中的字段,方法是否与父类产生矛盾.

 3). 字节码验证

​   验证过程中最复杂的一个阶段.通过堆数据流和控制流的分析,确定程序语义是合法的,符合逻辑的.

​   但如果一个类通过了字节码验证,也不一定是安全的

 4). 符号引用验证

​   发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段–解析中发生.

​  符号引用验证可以看做是对类自身以外的信息进行匹配性校验,内容包含以下:

  • 符号引用中通过字符串描述的全限定名能否找到对应的类
  • 在指引类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段(目标类中是否能找到这里调用的方法或者字段).
  • 符号引用中的类,字段,方法的访问性是否可以被访问

 3.3 准备

​   准备阶段正式对类变量(static变量,不包括实例变量)分配内存并设置类变量初始值(一般就是0值,final属性会初始化为设定的值).这些变量使用的内存都将在方法区中进行分配.

 3.4 解析

​   虚拟机将常量池内的符号引用替换为直接引用(直接指向目标的指针或者间接定位到目标的句柄)的过程.

​   虚拟机规范中并没有规定解析阶段发生的具体时间,只要在执行了用于操作符号引用的16个字节码指令之前,对它们所使用的符号引用进行解析就可以.

​   对一个符号引用进行多次的解析请求是很常见的事情,虚拟机实现可以对第一次解析的结果进行缓存,从而避免动作重复进行.

​   解析动作只要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类符号引用进行.

 3.5 初始化

​   类初始化阶段是类加载过程的最后一步,这一步才开始执行类中定义的Java代码(字节码).

​   在准备阶段,变量已经付过依次系统要求的初始值,在初始化阶段则根据程序中定制的去初始化变量和其他资源(可以说初始化是执行类构造器<clinit>()方法的过程).

  • <clinit>()方法是有编译器自动收集类中的所有变量的赋值动作和静态代码块中的语句合并产生的.
  • <clinit>()方法与类的构造函数不同,不需要显式地调用父类构造器,虚拟机保证在子类的<clinit>()方法执行之前父类的<clinit>()方法已经执行完毕,所以第一个执行的<clinit>()肯定是java.lang.Object的<clinit>().
  • 由于<clinit>()方法先于构造方法执行,就决定了静态代码块中的代码要先于构造函数执行.
  • 父类的<clinit>()方法先执行,意味着子类初始化时,父类中的静态代码块肯定是执行过的.
  • <clinit>()方法不是必须的,如果没有静态代码块也没有赋值操作,就不会生成<clinit>()方法.
  • 执行接口的<clinit>()方法不需要执行父接口的<clinit>()方法,当使用到父借口时才会执行.接口的实现类也一样.
  • <clinit>()方法是线程同步的.

4、 类加载器

​ 类加载器实现的动作就是"通过一个类的全限定类名来获取描述此类的二进制字节流".

 4.1 类与类加载器

​   每一个类加载器都有一个独立的命名空间,也就是说,就算两个类完全一样,但是是由不同的类加载器加载的,也会被认为是不同的两个类.

​   这样,对于任意一个类,确定这个类在Java虚拟机中的唯一性就必需要用到类加载器.

 4.2 双亲委派模型

​   从Java虚拟机的角度来看,只存在两种不同的类加载器,一种是启动类加载器,由c++实现,是虚拟机的一部分.另一种是所有其他的类加载器,由Java语言实现,独立于虚拟机外部.

​   但从Java开发人员的角度看,其他所有类加载器还可以被分为扩展类加载器应用程序加载器.

  • 启动类加载器:复杂将存放在<JAVA_HOME>\lib目录中或者参数设定的目录下中的类库加载带虚拟机中.启动类加载器无法被Java程序直接引用.
  • 扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中,或者被系统变量所指定路径下的所有类库,开发者可以直接使用扩展类加载器.
  • 应用程序类加载器:负责加载用户类路径上所指定的类库开发者可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,那么默认使用这个类加载器,

 类加载器的继承关系:

类加载器的继承关系

​   双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都有自己的父类加载器,这里类加载器之间的父子关系一般不会以继承实现,而是使用组合的关系实现.

Java类加载器双亲委派模型

​   双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一个层次的来加载器都是如此,因此所有的家在请求最终都应该传送到父类加载器去完成,只有当父加载器反馈无法完成这个加载类的请求(在自己的搜索范围中没有找到这个类)时,子加载器才会尝试自己去加载.

​   这样做的好处就是Java类随着他的类加载器一起具备了一种带着优先级的层次关系.防止一个类经过不同的类加载器加载而被识别成不同的类.

发布了121 篇原创文章 · 获赞 45 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_41596568/article/details/102906035
今日推荐