要点提炼| 理解JVM之类加载机制

本篇将了解类加载机制和双亲委派模型这两大知识考点:

  • 概述
  • 类加载全过程
  • 类加载器&双亲委派模型

1.概述java

a.JVM类加载机制:是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验转换解析初始化,最终造成可被虚拟机直接使用的Java类型的过程。数据库

b.特性:运行期类加载。即在Java语言里面,类型的加载、链接和初始化过程都是在程序运行期完成的,从而经过牺牲一些性能开销来换取Java程序的高度灵活性。数组

JVM运行期动态加载+动态链接->Java语言的动态扩展特性安全


2.类加载全过程网络

类从被加载到虚拟机内存中开始、到卸载出内存为止,整个生命周期包括7阶段:数据结构

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

其中,验证、准备、解析这3个部分统称为链接(Linking),流程以下图:多线程

注意:布局

  • 『加载』->『验证』->『准备』->『初始化』->『卸载』这5个阶段的顺序是肯定的,而『解析』可能为了支持Java语言的运行时绑定会在『初始化』后才开始。
  • 上述阶段一般都是互相交叉地混合式进行的,好比会在一个阶段执行的过程当中调用、激活另一个阶段。

接下来将分别介绍上述几个阶段。性能


a.加载spa

任务:

  • 经过类的全限定名来获取定义此类的二进制字节流。如从ZIP包读取、从网络中获取、经过运行时计算生成、由其余文件生成、从数据库中读取等等途径......
  • 将该二进制字节流所表明的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义。
  • 在内存中生成一个表明这个类的java.lang.Class对象,它将做为程序访问方法区中的这些类型数据的外部接口。

b.验证

  • 是链接阶段的第一步,且工做量在JVM类加载子系统中占了至关大的一部分。
  • 目的:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。

因而可知,它能直接决定JVM可否承受恶意代码的攻击,所以验证阶段颇有必要,但因为它对程序运行期没有影响,并不必定必要,能够考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

  • 检验过程包括如下四个阶段
    • 文件格式验证
      • 内容:验证字节流是否符合Class文件格式的规范、以及是否能被当前版本的虚拟机处理。
      • 目的:保证输入的字节流能正确地解析并存储于方法区以内,且格式上符合描述一个Java类型信息的要求。只有保证二进制字节流经过了该验证后,它才会进入内存的方法区中进行存储,因此后续3个验证阶段所有是基于方法区而不是字节流了。
    • 元数据验证
      • 内容:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
      • 目的:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
    • 字节码验证:是验证过程当中最复杂的一个阶段。
      • 内容:对类的方法体进行校验分析,保证被校验类的方法在运行时不会作出危害虚拟机安全的事件。
      • 目的:经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的。
    • 符号引用验证
      • 内容:对类自身之外(如常量池中的各类符号引用)的信息进行匹配性校验。
      • 目的是确保解析动做能正常执行,若是没法经过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类。
      • 注意:该验证发生在虚拟机将符号引用转化为直接引用的时候,即『解析』阶段。

c.准备

任务

  • 为类变量分配内存:由于这里的变量是由方法区分配内存的,因此仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一块儿分配在Java堆中。
  • 设置类变量初始值:一般状况下零值。

d.解析

  • 以前提过,解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的过程。
    • 符号引用(Symbolic References):以一组符号来描述所引用的目标。
      • 能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。
      • 与虚拟机实现的内存布局无关,由于符号引用的字面量形式明肯定义在Java虚拟机规范的Class文件格式中,因此即便各类虚拟机实现的内存布局不一样,可是能接受符号引用都是一致的。
    • 直接引用(Direct References):
      • 能够是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
      • 与虚拟机实现的内存布局相关,同一个符号引用在不一样虚拟机实例上翻译出来的直接引用通常不一样。
  • 发生时间:JVM会根据须要来判断,是在类被加载器加载时就对常量池中的符号引用进行解析,仍是等到一个符号引用将要被使用前才去解析。
  • 解析动做:有7类符号及其对应在常量池的7种常量类型
    • 类或接口(CONSTANT_Class_info)
    • 字段(CONSTANT_Fieldref_info)
    • 类方法(CONSTANT_Methodref_info)
    • 接口方法(CONSTANT_InterfaceMethodref_info)
    • 方法类型(CONSTANT_MethodType_info)
    • 方法句柄(CONSTANT_MethodHandle_info)
    • 调用点限定符(CONSTANT_InvokeDynamic_info)

举个例子,设当前代码所处的为类D,把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,解析过程分三步:

  • 若C不是数组类型:JVM将会把表明N的全限定名传递给D类加载器去加载这个类C。在加载过程当中,因为元数据验证字节码验证的须要,又可能触发其余相关类的加载动做。一旦这个加载过程出现了任何异常,解析过程就宣告失败。
  • 若C是数组类型且数组元素类型为对象:JVM也会按照上述规则加载数组元素类型。
  • 若上述步骤无任何异常:此时C在JVM中已成为一个有效的类或接口,但在解析完成前还需进行符号引用验证,来确认D是否具有对C的访问权限。若是发现不具有访问权限,将抛出java.lang.IllegalAccessError异常。

e.初始化

  • 是类加载过程的最后一步,会开始真正执行类中定义的Java字节码。而以前的类加载过程当中,除了在『加载』阶段用户应用程序可经过自定义类加载器参与以外,其他阶段均由虚拟机主导和控制。
  • 与『准备』阶段的区分
    • 准备阶段:变量赋初始零值。
    • 初始化阶段:根据Java程序的设定去初始化类变量和其余资源,或者说是执行类构造器<clinit>()的过程。

<clinit>():由编译器自动收集类中的全部类变量的赋值动做和静态语句块static{}中的语句合并产生。

  • 是线程安全的,在多线程环境中被正确地加锁、同步。
  • 对于类或接口来讲是必需的,若是一个类中没有静态语句块,也没有对变量的赋值操做,那么编译器能够不为这个类生成 <clinit>()
  • 接口与类不一样的是,执行接口的 <clinit>()不须要先执行父接口的 <clinit>(),只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()
  • 在虚拟机规范中,规定了有且只有五种状况必须当即对类进行『初始化』:
    • 遇到newgetstaticputstaticinvokestatic这4条字节码指令时;
    • 使用java.lang.reflect包的方法对类进行反射调用的时候;
    • 当初始化一个类的时候,若发现其父类还未进行初始化,需先触发其父类的初始化;
    • 在虚拟机启动时,需指定一个要执行的主类,虚拟机会先初始化它;
    • 当使用JDK1.7的动态语言支持时,若一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,且这个方法句柄所对应的类未进行初始化,需先触发其初始化。

3.类加载器&双亲委派模型

每一个类加载器都拥有一个独立的类名称空间,它不只用于加载类,还和这个类自己一块儿做为在JVM中的惟一标识。因此比较两个类是否相等,只要看它们是否由同一个类加载器加载,即便它们来源于同一个Class文件且被同一个JVM加载,只要加载它们的类加载器不一样,这两个类就一定不相等。

a.类加载器

从JVM的角度,可将类加载器分为两种:

  • 启动类加载器(Bootstrap ClassLoader)
    • C++语言实现,是虚拟机自身的一部分。
    • 负责加载存放在<JAVA_HOME>\lib目录中、或被-Xbootclasspath参数所指定路径中的、且可被虚拟机识别的类库。
    • 没法被Java程序直接引用,若是自定义类加载器想要把加载请求委派给引导类加载器的话,可直接用null代替。
  • 其余类加载器:由Java语言实现,独立于虚拟机外部,而且全都继承自抽象类java.lang.ClassLoader,可被Java程序直接引用。常见几种:
    • 扩展类加载器(Extension ClassLoader)
      • sun.misc.Launcher$ExtClassLoader实现。
      • 负责加载<JAVA_HOME>\lib\ext目录中的、或者被java.ext.dirs系统变量所指定的路径中的全部类库。
    • 应用程序类加载器(Application ClassLoader)
      • 默认的类加载器,是ClassLoader#getSystemClassLoader()的返回值,故又称为系统类加载器
      • sun.misc.Launcher$App-ClassLoader实现。
      • 负责加载用户类路径上所指定的类库。
    • 自定义类加载器(User ClassLoader):若是以上类加载起不能知足需求,可自定义。

上述几种类加载器的关系如图:

须要注意的是,虽然数组类不经过类加载器建立而是由JVM直接建立的,但仍与类加载器有密切关系,由于数组类的元素类型最终还要靠类加载器去建立。

b.双亲委派模型(Parents Delegation Model)

  • 表示类加载器之间的层次关系。
  • 前提:除了顶层启动类加载器外,其他类加载器都应当有本身的父类加载器,且它们之间关系通常不会以继承(Inheritance)关系来实现,而是经过组合(Composition)关系来复用父加载器的代码。
  • 工做过程:若一个类加载器收到了类加载的请求,它先会把这个请求委派给父类加载器,并向上传递,最终请求都传送到顶层的启动类加载器中。只有当父加载器反馈本身没法完成这个加载请求时,子加载器才会尝试本身去加载。
  • 注意:不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。
  • 优势:类会随着它的类加载器一块儿具有带有优先级的层次关系,可保证Java程序的稳定运做;实现简单,全部实现代码都集中在java.lang.ClassLoader的loadClass()中。

好比,某些类加载器要加载java.lang.Object类,最终都会委派给最顶端的启动类加载器去加载,这样Object类在程序的各类类加载器环境中都是同一个类。相反,系统中将会出现多个不一样的Object类,Java类型体系中最基础的行为也就没法保证,应用程序也将会变得一片混乱。

相关文章
相关标签/搜索