一般来说,Java 类的虚拟机使用 Java 方式如下:
- Java 源程序(.java 文件)在经过 J ava 编译器编译 之后就被转换成 Java 字节代码 (.class 文件)。
- 类加载器负责读取 Java 字节代码,并转换成
java.lang.Class
类的一个实例。 - 每个这样的实例用来表示一个 Java 类。
- 通过此实例的
newInstance()
方法就可以创建出该类的一个对象。
类的生命周期#
我们先来看下类的生命周期,包括:
- 加载
- 连接
- 初始化
- 使用
- 卸载
其中 加载 、 连接 、 初始化 属于 类加载过程 。
使用 是指我们new对象进行使用。
卸载 指对象被GC垃圾回收掉。
[](https://links.jianshu.com/go?to=https://aduner-blog-img.oss-cn-
beijing.aliyuncs.com/img/image-20210517215428824.png)
image-20210517215428824(https://links.jianshu.com/go?to=https://aduner-
blog-img.oss-cn-beijing.aliyuncs.com/img/image-20210517215428824.png)
类加载过程#
JVM的类加载的过程是通过引导类加载器(bootstrap class loader)创建一个初始类(initial
class)来完成的,这个类是由JVM的具体实现指定的。
Class 文件需要加载到虚拟机中之后才能运行和使用,系统加载 Class 类型的文件份如下几步:
- 加载
- 连接
- 验证
- 准备
- 解析
- 初始
顺序是这样一个顺序,但是 加载阶段和连接阶段 的 部分内容是交叉进行 的,加载阶段尚未结束,连接阶段可能就已经开始了。
下面我们来逐步解析
加载#
这里的 加载 是 微观 上的,是类加载过程中的一小步,也是第一步, 类加载过程中的加载 是 宏观 上的。
加载的流程如下:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
简单来说就是: 加载二进制数据到内存 —> 映射成JVM能识别的结构 —> 在内存中生成class文件 。
在 虚拟机规范 上,对这部分的 规定并不具体 ,所以实现方式是很 灵活 的。
加载阶段我们可以用自定义类加载器去控制字节流的获取方式,是 非数组类的可控性最强的阶段 ,而数组类型不通过类加载器创建,它由 Java
虚拟机直接创建。
关于类加载器是什么,后文再聊。
连接#
连接分为三步,验证、准备、解析,目的是将上面创建好的Class类合并至JVM中,使之能够执行的过程。
验证#
确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。
准备#
为类中的静态字段分配内存,并设置默认的初始值,比如int类型初始值是0。
被final修饰的static字段不会设置,因为final在编译的时候就分配了。
解析#
解析阶段的目的,是将常量池内的 符号引用 转换为 直接引用 的过程。
解析动作主要针对类、接口、字段、类方法、接口方法、方法类型等。
如果 符号引用指向 一个 未被加载的类 ,或者 未被加载类的字段或方法
,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
符号引用 就是一组符号来描述目标,可以是 任何字面量 ,符号引用的字面量形式明确定在《Java 虚拟机规范》的Class文件格式中。
直接引用 就是 直接指向目标的指针 、相对偏移量或一个间接定位到目标的句柄。
举个例子:
在程序执行方法时,系统需要 明确知道 这个方法所在的 位置 。
Java 虚拟机为 每个类 都准备了一张 方法表来存放类中所有的方法 。
当需要调用一个类的方法的时候,只要知道这个方法在 方法表中的偏移量 就可以直接调用该方法了。
通过 解析操作符号引用 就可以直接转变为 目标方法在类中方法表的位置 ,从而使得方法可以被调用。
所以,解析阶段是虚拟机将常量池内的 符号引用替换为直接引用的过程 ,也就是得到 类或者字段、方法在内存中的指针或者偏移量 。
初始化#
初始化就是执行类的构造器方法,是类加载的最后一步,这一步 JVM才开始真正执行类中定义的 Java 程序代码
这个方法不需要定义,是javac编译器自动收集类中所有类变量的 赋值动作 和 静态代码块中的语句 合并来的。
若该类具有父类,jvm
会保证父类的init()
先执行,然后在执行子类的init()
。
对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化,只有主动去使用类才会初始化类:
- 当遇到
new
、getstatic
、putstatic
或invokestatic
这 4 条直接码指令时
* 当遇到一个类, **读取一个静态字段** (未被 final 修饰)、或 **调用一个类的静态方法** 时。
* 当 JVM执行 **`new` 指令**时会初始化类。即当程序 **创建一个类的实例对象** 。
* 当 JVM执行 **`getstatic` 指令**时会初始化类。即程序 **访问类的静态变量** (不是静态常量,常量会被加载到运行时常量池)。
* 当 JVM执行 **`putstatic` 指令**时会初始化类。即程序 **给类的静态变量赋值** 。
* 当 JVM执行 **`invokestatic` 指令**时会初始化类。即程序 **调用类的静态方法** 。对类进行
- 初始化一个类,如果其 父类还未初始化 ,则先 触发该父类的初始化 。
- 当虚拟机启动时,用户需要 定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
MethodHandle
和VarHandle
可以看作是 轻量级的反射调用机制 ,而要想使用这 2 个调用, 就必须先使用findStaticVarHandle
来初始化要调用的类。- 「补充,来自issue745」 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
类加载器#
三大类加载器#
了解了类加载过程后,我们来看看 类加载器 。
类加载器(ClassLoader)用来加载 Java 类到 Java 虚拟机中。
JVM 中内置了三个重要的 ClassLoader ,同时按如下顺序进行加载:
- BootstrapClassLoader 启动类加载器 :最顶层的加载类,由C 实现,负责加载
%JAVA_HOME%/lib
目录下的 核心jar包和类 或者或被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader 扩展类加载器 :主要负责加载目录
%JRE_HOME%/lib/ext
目录下的jar包和类,或被java.ext.dirs
系统变量所指定的路径下的jar包。 - AppClassLoader 应用程序类加载器 :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器。
需要注意的是,Java虚拟机对Class文件采用的是 按需加载 的方式,也就是说当需要使用该类时才会将它的Class文件加载到内存生成Class对象。
双亲委派模型#
概念#
每一个类都有一个对应它的类加载器。在加载类的时候,是采用的 双亲委派模型 ,即把请 优求先交给父类处理 的一种任务委派模式。
系统中的类加载器在协同工作的时候会默认使用 双亲委派模型 。
双亲委派模型 的理论很简单,分为如下几步:
- 即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
- 加载的时候, 首先 会把该请求委派给该 父类加载器的
loadClass()
处理,因此所有的请求最终都应该传送到 顶层 的启动类加载器BootstrapClassLoader
中。 - 当父类加载器无法处理时,才由自己来处理 。
AppClassLoader 的父类加载器为 ExtensionClassLoader , ExtensionClassLoader 的父类加载器为null,当父类加载器 为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。
为什么要使用双亲委派模型#
试想一种情况,我们在项目目录下,手动创建了一个java.lang
包,并在该包下创建了一个Object
,这时候我们再去启动Java程序,原生Object
会被篡改吗? 当然是不会的!
因为Object
类是Java的核心库类,由 BootstrapClassLoader
加载,而自定义的java.lang.Object
类应该是由 AppClassLoader 来加载。
BootstrapClassLoader 先于 AppClassLoader
进行加载,根据上面的双亲委派模型的概念,我们可以知道,java.lang.Object
类已经被加载,并且 AppClassLoader
要加载类之前都要先给其父类过目,所以自己写的野类是无法撼动核心库类的。
结论
双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改。
源码分析#
双亲委派模型的都集中在 java.lang.ClassLoader
的 loadClass()
中,相关代码如下所示:
private final ClassLoader parent;
代码语言:txt复制protected Class<?> loadClass(String name, boolean resolve)
代码语言:txt复制 throws ClassNotFoundException
代码语言:txt复制 {
代码语言:txt复制 synchronized (getClassLoadingLock(name)) {
代码语言:txt复制 // 首先,检查请求的类是否已经被加载过
代码语言:txt复制 Class<?> c = findLoadedClass(name);
代码语言:txt复制 if (c == null) {
代码语言:txt复制 long t0 = System.nanoTime();
代码语言:txt复制 try {
代码语言:txt复制 //父加载器不为空,调用父加载器loadClass()方法处理
代码语言:txt复制 if (parent != null) {
代码语言:txt复制 c = parent.loadClass(name, false);
代码语言:txt复制 } else {
代码语言:txt复制 //父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
代码语言:txt复制 c = findBootstrapClassOrNull(name);
代码语言:txt复制 }
代码语言:txt复制 } catch (ClassNotFoundException e) {
代码语言:txt复制 //抛出异常说明父类加载器无法完成加载请求
代码语言:txt复制 }
代码语言:txt复制 if (c == null) {
代码语言:txt复制 long t1 = System.nanoTime();
代码语言:txt复制 //自己尝试加载
代码语言:txt复制 c = findClass(name);
代码语言:txt复制 // this is the defining class loader; record the stats
代码语言:txt复制 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
代码语言:txt复制 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
代码语言:txt复制 sun.misc.PerfCounter.getFindClasses().increment();
代码语言:txt复制 }
代码语言:txt复制 }
代码语言:txt复制 if (resolve) {
代码语言:txt复制 resolveClass(c);
代码语言:txt复制 }
代码语言:txt复制 return c;
代码语言:txt复制 }
代码语言:txt复制 }
反双亲委派模型#
双亲委派模型是Java默认的,假如我们不想用双亲委派,我们要怎么办呢?
我们可以 自定义一个类加载器 ,除了 BootstrapClassLoader
其他类加载器均由 Java
实现且全部继承自java.lang.ClassLoader
。如果我们要自定义自己的类加载器,很明显需要 继承ClassLoader
。
从上面的源码我们知道,双亲委派模型的都集中在 java.lang.ClassLoader
的 loadClass()
中,如果想打破双亲委派模型则需要 重写 loadClass()
方法。
如果我们不想打破双亲委派模型,就重写
ClassLoader
类中的findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。