深刻理解JVM中的类加载机制

0、引言

现现在,各类IDE愈来愈智能,咱们程序员的平常开发基本上都是在IDE上完成的,它能够帮助咱们将更多的注意力放在实际的业务处理中,随着这种安逸的编码生活的持续,咱们慢慢也就忘记了代码运行的底层原理。若是不学习,好像也没啥问题,毕竟咱们的关注重点是代码逻辑实现上,当出现问题了,百度,谷歌一下,或者问问公司的狠人,问题好像也能愉快的解决,本身好像也理解了似的。但事实上呢,依此周而复始,仍旧不理解,学习一门技术,只有咱们真正懂得了其底层原理,才能更好的解决问题。html

一、类加载概述

咱们在前面几篇文章中分别讲解了类文件结构JVM内存管理。这两篇文章详细描述了Class文件存储格式的具体细节及JVM运行时数据区。而今天这篇文章将会讲解Class文件中的信息进入到虚拟机中会发生什么变化。java

**先来个官方叙述:**类加载是Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化、最终造成能够被虚拟机直接使用的Java类型。通俗来说,就是咱们在完成代码的编写后,编译器会将咱们的java文件编译成对应的class文件(二进制字节码文件),经过类载器将这些class的时候将其加载到JVM中,生成对应的class对象。下面,让咱们详细来分析下类加载过程。程序员

二、类加载过程

对于任意一个类,类加载过程能够分为加载验证准备解析使用卸载七个阶段,以下图所示:安全

image-20210128214026739
image-20210128214026739

图中的加载验证准备初始化卸载这五个阶段的顺序是肯定的,而解析则不必定,为了支持java语言的运行时绑定特性,解析这个阶段能够发生在初始化阶段后。接下来咱们详细分析类加载过程当中这几个模块的做用。markdown

2.1 加载

类加载阶段是将字节码文件.Class的二进制数据读入内存中的方法区中,而后在堆中建立一个Java.lang.Class对象,对于加载阶段的任意一个类都对应着一个Class类型的对象,能够经过getClass()来获取。对于肯定的类Class,不管该类生成多少个对象,其Class类型的对象只有一个,Class类是整个反射的入口。数据结构

所以,在类加载阶段,Java虚拟机主要完成如下几类任务:oop

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

2.2验证

验证是链接阶段的第一步,其目的是为了确保Class文件内的字节流包含的信息符是否符合Java虚拟机规范的要求,保证输入的字节流不会危害到虚拟机自身的安全。咱们也许会有疑问,咱们印象中的Java语言是一门相对安全的语言啊(相比较于C++),如单纯的使用Java代码是没法访问到边界之外的数据,若是咱们非要这么作,编译器就会拒绝编译。可是,回到字节码层面,一切都变得不可控起来,这是由于Class文件能够采用不少途径来产生,并不必定要求用Java源码编译出来,若是JVM虚拟机不检查输入的字节流,对其彻底信任的话,极可能就会由于载入有害的的字节流致使系统的崩溃。所以,验证阶段在类加载过程当中占有很大的比重,它验证的项目能够大体分为如下几个:文件格式的验证、元数据验证、字节码验证和符号引用验证,下面咱们一一介绍:学习

  • 文件格式验证

文件格式的验证就是检查字节流是否符合Class文件格式的规范,不熟悉Class文件格式的能够看个人上一篇文章类文件结构,文件格式一般检查一下几个要素:编码

  • 魔数,是否以0xCAFEBABE开头spa

  • 主次版本号是否在合适的范围

  • 常量池中的常量是否有不被支持的常量类型

  • 指向常量的各类索引值是否有指向不存在的常量或者不符合类型的常量

  • ...........

  • 元数据验证

元数据的验证是对字节码描述的信息进行语义分析,验证的要素主要包含如下几点:

  • 是否有父类,除了Object外,都有父类
  • 这个类的父类是否继承被final修饰的类
  • 若这个类不是抽象类,是否实现了父类中的全部方法
  • ...........
  • 字节码验证

字节码验证是整个验证过程当中的最复杂的一个阶段,它主要经过数据流控制流分析,肯定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型作完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会作出危害虚拟机安全的事件,例如:

  • 保证任意时刻操做数栈的数据类型与指令代码序列都能配合工做,例如不会出现相似这样的状况:在操做栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
  • 保证跳转指令不会跳转到方法体之外的字节码指令上。
  • 保证方法体中的类型转换是有效地,例如能够把一个子类对象赋值给父类数据类型,这是安全的,可是把父类对象复制给子类数据类型,甚至把对象赋值给与他毫无继承关系、彻底不相干的一个数据类型,则是危险和不合法的。
  • .........
  • 符号引用验证

符号引用验证能够看作是对类自身(常量池中的各类符号引用)的信息进行匹配性校验,它的目的是确保解析动做可以正常执行,若是没法经过符号的引用验证,则会抛出异常。符号引用验证阶段一般须要校验如下内容:

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

......

2.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一块儿分配在Java堆中。这里所说的初始值“一般状况”下是数据类型的零值。

public static int number=10  
复制代码

类变量number在准备阶段值是0而不是10,由于这时候还没有开始执行任何Java方法,而把number赋值为10的putstatic指令是程序被编译后,存放于类构造器()方法之中,因此把number赋值为10的动做将在初始化阶段才会执行。下表列出了全部Java基础类型的零值:

2.4 解析

解析阶段就是将Class中的常量池中的符号引用解析为直接引用。符号引是使用一组符号描述被引用的目标,引用的目标不必定加载到内存中;直接引用可使直接指向目标地址的指针,相对偏移量或者间接定位到目标的句柄,有了直接引用,引用的目标必定存在在虚拟机中。主要包括四种类型引用的解析,分别是类或接口解析、字段解析、方法解析和接口方法解析。下面以字段解析和方法解析为例:

2.5 初始化

初始化是类加载过程的最后一步,到了初始化阶段,才开始正真的执行字节码文件,根据字节码文件的内容对类的各个字段进行赋值;初始化是执行类构造器()方法的过程。实际上,在链接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员本身写的逻辑去初始化类变量和其余资源,举例以下:

    public static int number1  = 5;
    public static int number2  = 6;
    static{
        number = 68;
    }
复制代码

在准备阶段number1和number2都等于0;在初始化阶段number1和number2分别等于5和6。

总结一下初始化发生的条件:

  • 建立一个新的对象实例时(好比new、反射、序列化)
  • 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
  • 调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操做时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
  • 调用JavaAPI中的反射方法时(好比调用java.lang.Class中的方法,或者java.lang.reflect包中其余类的方法)
  • 初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提早完成初始化操做,接口例外)
  • JVM启动包含main方法的启动类时。

使用阶段是当执行完初始化后,就能够根据本身的实际须要使用具体的类;当咱们在程序中执行System.exit(),加载的类会从内存中卸载,一般状况下,当程序正常执行结束后、或者发生错误而终止都会使得已加载的类对象被卸载。

经过以上的讲解,咱们知道了类Class文件被虚拟机加载、使用直至卸载须要经历的步骤,可是咱们忽略了一个很是重要的问题,类是如何被加载器加载的,加载器须要知足什么样的规律?下面咱们一一来说解。

三、类加载器与双亲委派模型

3.1 类加载器

类的加载是使用类加载器经过查询路径的方式进行的,加载阶段既可使用虚拟机里内置的引导类加载器来完成,也能够由用户自定义类加载器来完成Java中的类加载器一般分为四类:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)、用户自定义类加载器(User ClassLoader)。不一样的类加载器负责不一样区域的类的加载。

image-20210128211514071
image-20210128211514071
  • 启动类加载器

    启动类加载器是加载存放在<JAVA_HOME>\lib目录,或者被Xbootclasspath选项指定的jar包,如rt.jar、tools.jar。Java中的不少组件都是经过启动类加载器来完成的,不只如此,扩展类加载器和应用程序类加载器也是由它来加载的。

  • 扩展类加载器

    扩展类加载器是加载<JAVA_HOME>\lib\ext*.jar或者-java.ext.dirs指定目录下的jar包,它的做用是与启动类加载器配合用于完成系统组件的加载。

  • 应用程序类加载器

    应用程序类加载器是加载Classpath或java.class.path所指定的目录下的类和jar包,一般状况下,咱们自定义的类都是经过这类加载器完成的。

  • 用户自定义类加载器

    用户自定义类加载器是经过java.lang.ClassLoader的子类自定义加载class。

3.2 双亲委派模型

上面咱们讲到不一样的类加载器都有不一样的加载范围,当某个类加载器要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操做,若是上级的类加载器没有加载,本身才会去加载这个类。所以,不一样类加载器相互配合就造成类双亲委派模型。

image-20210128213431950
image-20210128213431950

咱们先分析如下加载流程:

咱们在上图能够看到,除了启动类加载器,每个类加载器都有一个父类加载器。当一个类加载器加载一个类时,首先会把加载动做委派给他的父加载器,若是父加载器没法完成这个加载动做时才由该类加载器进行加载。因为类加载器会向上传递加载请求,因此一个类加载时,首先尝试加载它的确定是启动类加载器(逐级向上传递请求,直到启动类加载器,它没有父加载器),以后根据是否能加载的结果逐级让子类加载器尝试加载,直到加载成功。

双亲委派模型的做用:

  • 防止重复加载同一个.class。经过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全
  • 保证核心.class不能被篡改。经过委托方式,不会去篡改核心.clas,即便篡改也不会去加载,即便加载也不会是同一个.class对象了。不一样的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

参考文献

[1]周志华.深刻理解Java虚拟机(第三版)

[2]https://blog.csdn.net/en_joker/article/details/79959330

[3]https://www.cnblogs.com/aspirant/p/7200523.html

相关文章
相关标签/搜索