我们先来看下类的生命周期,包括:
- 加载
- 连接
- 初始化
- 使用
- 卸载
其中 加载 、 连接 、 初始化 属于 类加载过程 。
使用 是指我们new对象进行使用。
卸载 指对象被GC垃圾回收掉。
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复制 }