详解Java类加载机制
Java 虚拟机的启动是通过引导类加载器 ( Bootstrap Class Loader ) 创建一个初始类 (Initial Class ) 来完成,这个类是由虚拟机的具体实现指定。紧接着,Java虚拟机链接这个初始类,初始化并调用它的 public void main(String[])方法。之后的整个执行过程都是由对此方法的调用开始。执行 main 方法中的 Java 虚拟机指令可能会导致 Java 虚拟机链接另外的一些类或接口,也可能会调用另外的方法。
可能在某种 Java 虚拟机的实现上,初始类会作为命令行参数被提供给虚拟机。当然,虚拟机实现也可以利用一个初始类让类加载器依次加载整个应用。初始类当然也可以选择组合上述的方式来工作。
类的加载流程
当 Java 虚拟机将 Java 源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析、运行等整个过程,这个过程我们叫:Java 虚拟机的类加载机制。JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。

加载
什么情况下需要开始类加载的第一个阶段:加载。 JAVA虚拟机规范并没有进行强制约束,交给虚拟机的具体实现自由把握。
加载阶段是“类加载”过程中的一个阶段,这个阶段通常也被称作“装载”,在加载阶段,虚拟机主要完成以下3件事情:
- 通过 “类全名” 来获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在 java 堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区这些数据的访问入口(所以我们能够通过低调用类.getClass() )
注意这里字节流不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。加载的信息存储在 JVM 的方法区。
对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。对于其它的类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。
如果上面那么多记不住: 请一定记住这句: 加载阶段也就是查找获取类的二进制数据(磁盘或者网络)动作,将类的数据(Class 的信息:类的定义或者结构)放入方法区 (内存)。
一图说明:

验证
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的 .class 文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:
- 文件格式的验证:验证 .class 文件字节流是否符合 class 文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是 .class 文件里面包含的数据信息、在这里可以不用理解)。
- 元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合 java 语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
- 字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。
- 符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用 -Xverfity:none 来关闭大部分的验证。
准备
当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。
- 内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。
1 | public static int factor = 3; |
- 初始化的类型。在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。
1 | public static int sector = 3; |
但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。
1 | public static final int number = 3; |
之所以 static final 会直接被复制,而 static 变量会被赋予零值。其实我们稍微思考一下就能想明白了。
两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 number 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。
解析
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。Java 虚拟机明确在 Class 文件格式中定义的符号引用的字面量形式。
直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
在解析的阶段,解析动作主要针对7类符号引用进行,它们的名称以及对于常量池中的常量类型和解析报错信息如下:
| 解析动作 | 符号引用 | 解析可能的报错 | | ———- | ——————————- | ———————————————————–
| | 类或接口 | CONSTANTClassInfo | java.land.IllegalAccessError
| | 字段 | CONSTANTFieldrefInfo | java.land.IllegalAccessError 或 java.land.NoSuchFieldError
| | 类方法 | CONSTANTMethodefInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError
| | 接口方法 | CONSTANTInterfaceMethoderInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError
| | 方法类型 | CONSTANTMethodTypeInfo |
| | 方法句柄 | CONSTANTMethodhandlerInfo |
| | 调用限定符 | CONSTANTInvokeDynamicInfo |
解析的整个阶段在虚拟机中还是比较复杂的,远比上面介绍的复杂的多,但是很多特别细节的东西我们可以暂时先忽略,先有个大概的认识和了解之后有时间在慢慢深入了。
初始化
类初始阶段是类加载过程的最后一步,在上面提到的类加载过程中,除了加载阶段用户应用程序可以通过自定义类加载器参与之外,其余的动作全部由虚拟机主导和控制。初始化阶段,是真正开始执行类中定义的 Java 程序代码(或者说是字节码)。
在准备阶段,变量已经赋值过一次系统要求的初始值(零值),而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。(或者从另一个角度表达:初始化阶段是执行类构造器 <clinit>() 方法的过程。)
在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的 Java 代码场景是:使用new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
看到上面几个条件你可能会晕了,但是不要紧,不需要背,知道一下就好,后面用到的时候回到找一下就可以了。
注意这里的初始化,并不是说创造的类的实例,而是执行了类构造器,简单来说就是只对静态变量,静态代码块进行初始化。对于构造函数只有在创建实例的时候才会执行。
使用
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。
卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

