JVM系列-类加载器机制

内容整理自:

  1. JVM系列(五) - JVM类加载机制详解
  2. 深入理解JVM类加载机制

一、简述

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

二、类的加载过程以及生命周期

image.png

类加载的过程分为三个步骤(五个阶段) :加载 -> 连接(验证、准备、解析)-> 初始化

加载

加载的过程描述:

  • 通过类的全限定名定位.class文件,并获取其二进制字节流。
  • 把字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  • 在Java堆中生成一个此类的java.lang.Class对象,作为方法区中这些数据的访问入口

连接

连接:包括验证、准备、解析三步

验证

验证是连接阶段的第一步,用于确保Class字节流中的信息是否符合虚拟机的要求

具体验证形式

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行

准备

为类的静态变量分配内存,并将其初始化为默认值。准备过程通常分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口信息等

具体行为:

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是在Java代码中被显式赋值的(被显式赋值的常量例外)

解析

解析:把类中对常量池内的符号引用转换为直接引用

符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以唯一定位到目标即可。符号引用于内存布局无关,所以所引用的对象不一定需要已经加载到内存中。各种虚拟机实现的内存布局可以不同,但是接受的符号引用必须是一致的,因为符号引用的字面量形式已经明确定义在Class文件格式中

直接引用(Direct References): 直接引用时直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那么它一定已经存在于内存中了

常量池中常量类型:

  • 常量池中常量数量不固定,因此常量池开头放置一个 u2 类型的无符号数,用来存储当前常量池的容量。
  • 常量池的每一项常量都是一个表,表开始的第一位是一个 u1 类型的标志位(tag),代表当前这个常量属于哪种常量类型
类型 tag 描述
CONSTANT_utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 标识方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

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

初始化

初始化:对类静态变量赋予正确的初始值

初始化的目标

  • 实现对声明类静态变量时指定的初始值的初始化;
  • 实现对使用静态代码块设置的初始值的初始化。

初始化的步骤

  • 如果此类没被加载、连接,则先加载、连接此类;
  • 如果此类的直接父类还未被初始化,则先初始化其直接父类;
  • 如果类中有初始化语句,则按照顺序依次执行初始化语句。

初始化的时机

image.png

其中情况1中的4条字节码指令在Java里最常见的场景是:

  • new一个对象时
  • set或者get一个类的静态字段(除去那种被final修饰放入常量池的静态字段)
  • 调用一个类的静态方法

Java中父类和子类初始化顺序

  1. 父类中静态成员变量和静态代码块
  2. 子类中静态成员变量和静态代码块
  3. 父类中普通成员变量和代码块,父类的构造函数
  4. 子类中普通成员变量和代码块,子类的构造函数

类的主动引用与被动引用

在java虚拟机规范中,严格规定了,只有对类进行主动引用,才会触发其初始化方法。而除此之外的引用方式称之为被动引用,不会触发类的初始化方法

主动引用
主动引用:在类加载阶段,只执行加载、连接操作,不执行初始化操作

被动引用
主动引用之外的引用情况都称之为被动引用,这些引用不会进行初始化
被动引用的几种形式:

  1. 通过子类引用父类的的静态字段,不会导致子类初始化;
  2. 定义类的数组引用而不赋值,不会触发此类的初始化;
  3. 访问类定义的常量,不会触发此类的初始化

三、三种类加载器

image.png

  • Bootstrap Classloader 是在Java虚拟机启动后初始化的。
  • Bootstrap Classloader 负责加载 ExtClassLoader,并且将 ExtClassLoader的父加载器设置为 Bootstrap Classloader
  • Bootstrap Classloader 加载完 ExtClassLoader 后,就会加载 AppClassLoader,并且将 AppClassLoader 的父加载器指定为 ExtClassLoader

Bootstrap ClassLoader

启动类加载器:负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

Extension ClassLoader

扩展类加载器:该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。

Application ClassLoader

应用程序类加载器:该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(程序自己classpath下的类)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

类加载器的隔离问题

每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name) 进行搜索来检测这个类是否已经被加载了。

JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个类不是由一个 ClassLoader 加载,是无法将一个类的实例强转为另外一个类的,这就是 ClassLoader 隔离性。

为了解决类加载器的隔离问题,JVM引入了双亲委托机制

四、双亲委托模型

核心思想:其一,自底向上检查类是否已加载;其二,自顶向下尝试加载类

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类

具体加载过程

  • 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 如果BootStrapClassLoader加载失败(例如在%JAVA_HOME%/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 如果ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

双亲委派模型意义

  • 系统类防止内存中出现多份同样的字节码,使得类有了层次的划分
  • 保证Java程序安全稳定运行

就拿java.lang.Object来说,你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的,也就是最终都是由Bootstrap ClassLoader去找\lib中rt.jar里面的java.lang.Object加载到JVM中, 这样如果有不法分子自己造了个java.lang.Object,里面嵌了不好的代码,但是如果按照双亲委派模型来实现类加载的话,最终加载到JVM中的只会是我们rt.jar里面的东西,也就是这些核心的基础类代码得到了保护

拓展
为什么说java spi破坏双亲委派模型?

五、类的加载方式

  • 命令行启动应用时候由JVM初始化加载
  • 通过Class.forName()方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载

Class.forName()和ClassLoader.loadClass()

  • Class.forName():把类的.class文件加载到JVM中,对类进行解释的同时执行类中的static静态代码块;
  • ClassLoader.loadClass():只是把.class文件加载到JVM中,不会执行static代码块中的内容,只有在newInstance才会去执行

六、自定义加载器

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

  • 在执行非置信代码之前,自动验证数字签名。
  • 动态地创建符合用户特定需要的定制化构建类。
  • 从特定的场所取得java class,例如数据库中和网络中