前言
在上一篇深入理解Java虚拟机之内存模型分析中,我简单的介绍了Java虚拟机的内存模型,在开篇的时候也有贴一张类加载的流程图。在对象还没进内存区域的之前,Java类又是如何变成成class文件,进而在Java内存里的呢?
因此在本篇,主要讲Java虚拟机是如何加载Java类的。
类加载器
在说类加载过程前,先说说类加载器,毕竟这也是类加载过程首要的一步。类加载器主要负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。
而JVM预定义有三种类加载器,如下三种类加载器:
根类加载器(bootstrap class loader)
根类加载器加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。
由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
扩展类加载器(extensions class loader)
扩展类加载器负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。
扩展类加载器是由Java语言实现,父类加载器为null。
系统类加载器(system class loader)
系统类加载器也被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。
程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。
系统类加载器也是由Java语言实现,父类加载器为扩展类加载器。
类加载过程
一般来说,我们把Java的类加载过程分为三个主要步骤:加载、链接、初始化。因此我们要从这三个主要步骤开始说起,这里我会用搭建楼房的方式来比喻类加载过程,也方便理解类加载过程。
加载阶段
加载这里我们可以理解为是Java查找字节流,将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(class文件),如 jar 文件、 class文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError 。
在加载过程中,除了启动类加载器之外,其他的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象。这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java 虚拟机中,方能执行类加载。
加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
链接阶段
链接阶段是类加载过程中最重要的阶段,主要是把原始的类定义信息转化到JVM运行的过程中,这里也分了三个步骤:验证、准备、初始化。
验证
在JVM接收到类加载过来的字节信息后,也需要需要核验是符合Java虚拟机规范的,否则就被认为是VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
因此,验证阶段也是JVM安全的一道保障。
准备
通过验证完后,接收到的字节信息就可以在JVM具体初始化,这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,
不会去执行更进一步的 JVM 指令。
就比如,搭房子的时候,我们先把这一块的材料都准备好,等建筑工人过来,就可以直接搭房子了。
除了分配内存外,部分 Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。
解析
解析阶段主要是将常量池中的符号引用(symbolic reference)替换为直接引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。
初始化阶段
初始化阶段是类加载的最后一步,这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这
部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
只有当初始化完成之后,类才正式成为可执行的状态。
那么,类的初始化何时会被触发呢?
在JVM 规范枚举了下述多种触发情况:
当虚拟机启动时,初始化用户指定的主类;
当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
当遇到调用静态方法的指令时,初始化该静态方法所在的类;
当遇到访问静态字段的指令时,初始化该静态字段所在的类;
子类的初始化会触发父类的初始化;
如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
使用反射 API 对某个类进行反射调用时,初始化这个类;
当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
双亲委派
说完了类加载过程,也顺便说说双亲委派问题吧。在之前的一次面试中,面试官也刚好问过这个问题,那时候只知道一个模糊的概念,因此回答得也是挺模糊的,所以在此也好好梳理一下双亲委派模型。
什么是双委派模型
双亲委派模型,简单来说就是每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
Java 9 变化
在 Java 9 之前,启动类加载器负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。除了启动类加载器之外,另外两个重要的类加载器是扩展类加载器(extension class loader)和应用类加载器(application class loader),均由 Java 核心类库提供。
扩展类加载器的父类加载器是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。
应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
Java 9 引入了模块系统,并且略微更改了上述的类加载器1。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。
最后
Java类加载这一篇也算总结完了,JVM一直以来都是比较复杂的一块,最近也在看一本《深入Java虚拟机》的书,书中还是有很多自己不能理解的,包括类加载过程中,还是有一些小细节需要好好打磨的。在以后,我也会把自己理解的写出来,类加载过程的总结就先到这里结束了!
Coding for dream!
The first step is as good as half over!!