本篇内容包括:Java 类的加载机制(Jvm 结构组成、Java 类的加载)、类的生命周期(加载-验证-准备-解析-初始化-使用-卸载)、类加载器 以及 双亲委派模型。
一、Java 类的加载机制
1、 Jvm 结构组成
Jvm 整体组成可分为四个部分:类加载器、运行时数据区(Runtime Data Area)、执行引擎(Execution Engine)、本地库接口(Native Interface)
- 类加载器:负责从字节码(Class)文件中,加载 class 信息到运行时数据区的方法区;
- 运行时数据区:存放 Jvm 在执行 Java 程序时相关数据的区域;
- 执行引擎:将字节码翻译成底层系统指令再交由 CPU 去执行;
- 本地库接口:执行过程中可能需要调用到其他语言(比如 C 语言)的本地接口。
PS:Javac 是收录于 Jdk 中的 Java 语言编译器。该工具可以将后缀名为 .java 的源文件编译为后缀名为 .class 的可以运行于 Java 虚拟机的字节码。
程序在被执行之前, Java 代码会被先转换成字节码(.class 文件), Jvm 首先通过一定的方式类加载器①(ClassLoader)把字节码文件加载到内存中运行时数据区②(Runtime Data Area),而字节码文件是 Jvm 提供的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎③(Execution Engine)将字节码翻译成底层系统指令再交由 CPU 去执行,而这个过程中需要调用其他语言的接口本地库接口④(Native Interface)来实现整个程序的功能,这就是这 4 个主要组成部分的职责与功能。
而我们通常所说的 Jvm 组成指的是运行时数据区,因为通常需要程序员调试分析的区域就是运行时数据区,或者更具体的来说就是运行时数据区里面的堆(Heap)模块!
2、Java 类的加载
类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象,Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被首次主动使用时再加载它, Jvm 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误( LinkageError 错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
加载.class 文件的方式:
- 从本地系统中直接加载
- 通过网络下载 .class 文件
- 从 zip、jar 等归档文件中加载 .class 文件
- 从专有数据库中提取 .class 文件
- 将 Java 源文件动态编译为 .class 文件
- 由其他文件生成
二、Java 类的生命周期
类从被加载到 Jvm 内存中开始到卸载出内存为止,生命周期分为7个阶段:加载-验证-准备-解析-初始化-使用-卸载。(或分为5个阶段,把 验证-准备-解析 分为连接阶段)
1、加载
加载过程就是把 class 字节码文件载入到虚拟机中,至于从哪儿加载,虚拟机设计者并没有限定,你可以从文件、压缩包、网络、数据库等等地方加载 class 字节码。
- 通过一个类的全限定名来获取其定义的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在Java堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口
2、验证(连接阶段的第一步):确保被加载的类的正确性
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3、准备(连接阶段的第二步):为类的静态变量分配内存,并将其初始化为默认值
准备阶段的工作就是为类的静态变量分配内存并设为 Jvm 默认的初值,对于非静态的变量,则不会为它们分配内存。静态变量的初值为 Jvm 默认的初值,而不是我们在程序中设定的初值。(仅包含类变,不包含实例变量)
4、解析(连接阶段的第三步):把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
5、初始化:为类的静态变量赋予正确的初始值
主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- 声明类变量是指定初始值;
- 使用静态代码块为类变量指定初始值
Jvm初始化步骤:
-
假如这个类还没有被加载和连接,则程序先加载并连接该类
-
假如该类的直接父类还没有被初始化,则先初始化其直接父类
-
假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如 Class.forName(“com.shengsiyuan.Test”) )
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类( JavaTest ),直接使用 java.exe 命令来运行某个主类
三、类加载器
类加载器是负责将可能是网络上、也可能是磁盘上的 .class 文件加载到内存中。并为其生成对应的 java.lang.Class 对象。一旦一个类被载入 Jvm 了,同一个类就不会被再次加载。那么怎样才算是同一个类?在 Java 中一个类用其全限定类名(包名和类名)作为其唯一标识,但是在 Jvm 中,一个类用其全限定类名和其类加载器作为其唯一标识。也就是说,在 Java 中的同一个类,如果用不同的类加载器加载,则生成的 .class 对象认为是不同的。
当 Jvm启动时,会形成由三个类加载器组成的初始类加载器层次结构:
- 启动类加载器(Bootstrap ClassLoader):是嵌在 Jvm 内核中的加载器,该加载器是用 C++ 语言写的,主要负载加载 JAVA_HOME/lib 下的类库,启动类加载器无法被应用程序直接使用;
- 扩展类加载器(Extension ClassLoader):该加载器器是用JAVA编写,且它的父类加载器是 Bootstrap,是由
sun.misc.Launcher$ExtClassLoader
实现的,主要加载 JAVA_HOME/lib/ext 目录中的类库。开发者可以这几使用扩展类加载器; - 系统类加载器(App ClassLoader):也称为应用程序类加载器,负责加载应用程序 classpath 目录下的所有 .jar 和 .class 文件。它的父加载器为 Extension ClassLoader。
上述三种类加载器的层次关系如下:
Ps:类加载器的体系并不是“继承”体系,而是委派体系,大多数类加载器首先会到自己的 parent 中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。
五、双亲委派模型
1、双亲委派模型
如果以上三种类加载器不能满足要求的话,程序员还可以自定义类加载器(继承 java.lang.ClassLoader 类)
它们的层级关系即 自定义类加载器 -> 应用程序加载器 -> 扩展加载器 -> 启动类加载器,这种层次关系被称作为双亲委派模型:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。
2、双亲委派模式优势
- 采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次;
- 其次是考虑到安全因素,Java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心 API 库被随意篡改;
- 可能你会想,如果我们在 classpath 路径下自定义一个名为 java.lang.SingleInterge 类呢?该类并不存在 java.lang 中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为 java.lang 是核心 API 包,需要访问权限,强制加载将会报出异常
java.lang.SecurityException: Prohibited package name: java.lang
所以无论如何都无法加载成功的。