JVM 系列 - 类加载器机制#
内容整理自:
一、简述#
虚拟机把描述类的数据从 class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制
二、类的加载过程以及生命周期#
类加载的过程分为三个步骤 (五个阶段) :加载
-> 连接(验证、准备、解析)
-> 初始化
加载#
加载的过程描述:
- 通过类的全限定名定位.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 类符号引用进行
初始化#
初始化:对类静态变量赋予正确的初始值
初始化的目标#
- 实现对声明类静态变量时指定的初始值的初始化;
- 实现对使用静态代码块设置的初始值的初始化。
初始化的步骤#
- 如果此类没被加载、连接,则先加载、连接此类;
- 如果此类的直接父类还未被初始化,则先初始化其直接父类;
- 如果类中有初始化语句,则按照顺序依次执行初始化语句。
初始化的时机#
其中情况 1 中的 4 条字节码指令在 Java 里最常见的场景是:
- new 一个对象时
- set 或者 get 一个类的静态字段(除去那种被 final 修饰放入常量池的静态字段)
- 调用一个类的静态方法
Java 中父类和子类初始化顺序#
- 父类中静态成员变量和静态代码块
- 子类中静态成员变量和静态代码块
- 父类中普通成员变量和代码块,父类的构造函数
- 子类中普通成员变量和代码块,子类的构造函数
类的主动引用与被动引用#
在 java 虚拟机规范中,严格规定了,只有对类进行主动引用,才会触发其初始化方法。而除此之外的引用方式称之为被动引用,不会触发类的初始化方法
主动引用
主动引用:在类加载阶段,只执行加载、连接操作,不执行初始化操作
被动引用
主动引用之外的引用情况都称之为被动引用,这些引用不会进行初始化
被动引用的几种形式:
- 通过子类引用父类的的静态字段,不会导致子类初始化;
- 定义类的数组引用而不赋值,不会触发此类的初始化;
- 访问类定义的常量,不会触发此类的初始化
三、三种类加载器#
- 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 去找 <JAVA_HOME>\lib 中rt.jar
里面的 java.lang.Object 加载到 JVM 中,这样如果有不法分子自己造了个 java.lang.Object,里面嵌了不好的代码,但是如果按照双亲委派模型来实现类加载的话,最终加载到 JVM 中的只会是我们 rt.jar 里面的东西,也就是这些核心的基础类代码得到了保护
五、类的加载方式#
- 命令行启动应用时候由 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,例如数据库中和网络中