文章目录
前言
本文记录笔者关于Java中的类加载机制的相关理解
一、类加载是什么,在哪个阶段发生,及能起到什么作用
在这一部分,我们对类加载机制的前世今生作一个简单的总结,也就是对这一部分标题中的问题作相关回答
- Java代码经过编译后形成的.class文件,需要加载到虚拟机后才能运行。我们将.class文件加载到虚拟机的过程称之为类加载机制。一个类经过类加载机制加载到内存,然后从内存中卸载是这个类的生命周期。整个类的生命周期包含了七个阶段。
- 根据上一条所提到的,类加载机制需要加载的是Java编译后形成的.class文件,所以类加载机制发生在编译后
- 类加载机制能够降二进制字节流的.class文件加载到内存中,供JVM虚拟机进行调用,如生成Class文件供反射机制进行调用
简而言之,类加载就是
JVM类加载机制 接收的是: .class文件的二进制字节流,产出的是: java.lang.Class对象。
二、类加载机制
1.类加载实现-类加载器
类加载机制主要依赖类加载器和双亲委派机制进行实现,概括如下
- Java 中将类的加载工具抽象为「类加载器(classloader)」。
- 「双亲委派」机制是 Java 中通过加载工具(classloader)加载类文件的一种具体方式。
- JVM 中类加载器默认使用双亲委派原则,但双亲委派模型并不是一个强制性约束。如 Java 的 「SPI 机制」、Tomcat、日志门面等场景中,均打破了双亲委派模型。
关于双亲委派,本文中的后续部分会进行讲解
类加载器
通过一个类全限定名称来获取其二进制文件(.class)流的工具,被称为类加载器(classloader)。
Java支持的四种类加载器
具体如图所示
如图,Java的类加载器可被分为四类
1.启动类加载器Bootstrap ClassLoader
- 用于加载Java的核心类
- 它不是一个 Java 类,是由底层的 C++ 实现。因此,启动类加载器不属于 Java 类库,无法被 Java 程序直接引用。Bootstrap ClassLoader 的 parent 属性为 null
2.标准扩展类加载器 Extension ClassLoader
- 由 sun.misc.Launcher$ExtClassLoader 实现
- 负责加载 JAVA_HOME 下 libext 目录下的或者被 java.ext.dirs 系统变量所指定的路径中的所有类库
3.应用类加载器 Application ClassLoader
- 由 sun.misc.Launcher$AppClassLoader 实现
- 负责在 JVM 启动时加载用户类路径上的指定类库
4.用户自定义类加载器 User ClassLoader
- 当上述 3 种类加载器不能满足开发需求时,用户可以自定义加载器
- 自定义类加载器时,需要继承 java.lang.ClassLoader 类。如果不想打破双亲委派模型,那么只需要重写 findClass 方法即可;如果想打破双亲委派模型,则需要重写 loadClass 方法
2.生命周期和加载过程
类的生命周期可以划分为 7 个阶段
1.加载
2.验证
3.准备
4.解析
5.初始化
6.使用
7.卸载
其中1-5的阶段,统一被成为类加载
1.加载
加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。
该过程可以总结为**「JVM 加载 Class 字节码文件到内存中,并在方法区创建对应的 Class 对象」**。
2.验证
当 JVM 加载完 Class 字节码文件,并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。
这个校验过程,大致可以分为下面几个类型
-
JVM 规范校验
JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。
例如,校验文件是否是以 0x cafe babe 开头,主次版本号是否在当前虚拟机处理范围之内等。 -
代码逻辑校验
JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。
例如,一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。
3.准备
准备阶段中,JVM 将为类变量分配内存并初始化。
准备阶段,有两个关键点需要注意
1.内存分配的对象
2.初始化的类型
这里需要注意的点在于两点
1.内存分配的对象是类变量而不是类成员变量
2.这里的变量初始化只针对类变量,且初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
例:
public static int factor = 3;
public String website = "www.google.com";
如上代码,在准备阶段,只会为 factor 变量分配内存,而不会为 website 变量分配内存,同时为factor变量所赋的值为Java变量类型的默认初始值0,而不是程序所希望为他赋的值3
4.解析
解析过程中,JVM 针对「类或接口」、「字段」、「类方法」、「接口方法」、「方法类型」、「方法句柄」、「调用点限定符」这 7 类引用进行解析。解析过程的主要任务是将其在常量池中的符号引用,替换成其在内存中的直接引用。
5.初始化
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化。
-
一般来说,当 JVM 遇到下面 5 种情况的时候会触发初始化
遇到 new、getstatic、putstatic、invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
-
使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
-
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
-
当使用 JDK 1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果是 REF_getstatic、REF_putstatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化时,则需要先出触发其初始化。
6.使用
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
7.卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。
三、Java类加载机制的特点
类加载机制的3个特点
双亲委派
1.JVM 中,类加载器默认使用双亲委派原则
负责依赖
2.如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。
缓存加载
3.为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。
双亲委派
JVM 中,类加载器默认使用双亲委派原则。
双亲委派机制是一种任务委派模式,是 Java 中通过加载工具(classloader)加载类文件的一种具体方式。 具体表现为
- 如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行。
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器 BootstrapClassLoader。
- 如果父类加载器可以完成类加载任务,就成功返回;倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载。
- 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类;如果将加载任务分配至系统类加载器(AppClassLoader)也无法加载此类,则抛出异常。
为什么JVM其实是将类加载机制向上委托,却被翻译成双亲委派机制
- 文档翻译错误,Oracle官方解释文档中parent属性,会翻译成了双亲
- classLoader类中,存在parent指针,设置双亲属性,具体见下
ExtClassLoader.parent=null;
AppClassLoader.parent=ExtClassLoader
//自定义
XxxClassLoader.parent=AppClassLoader
需要注意的是,启动类加载器(BootstrapClassLoader)不是一个 Java 类,它是由底层的 C++ 实现,因此启动类加载器不属于 Java 类库,无法被 Java 程序直接引用,所以 ExtClassLoader.parent=null;。
委派
设置双亲之后,便可以进行委派。委派过程也就是类加载的过程
ClassLoader中里面有三个重要方法,具体如下:
1.loadClass()
2.findClass()
3.defineClass()
实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法中。
public abstract class ClassLoader {
// 委派的父类加载器
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 保证该类只加载一次
synchronized (getClassLoadingLock(name)) {
// 首先,检查该类是否被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//父类加载器不为空,则用该父类加载器
c = parent.loadClass(name, false);
} else {
//若父类加载器为空,则使用启动类加载器作为父类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//若父类加载器抛出ClassNotFoundException ,
//则说明父类加载器无法完成加载请求
}
if (c == null) {
//父类加载器无法完成加载请求时
//调用自身的findClass()方法进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
上述代码的相关检测流程如下:
- 先检查类是否已经被加载过
- 若没有加载,则调用父加载器的 loadClass() 方法进行加载
- 若父加载器为空,则默认使用启动类加载器作为父加载器
- 如果父类加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载
loadClass、findClass、defineClass 方法的区别
ClassLoader 中和类加载有关的方法有很多,前面提到了 loadClass(),除此之外,还有 findClass() 和 defineClass() 等。这3个方法的区别如下
- loadClass():默认的双亲委派机制在此方法中实现
- findClass():根据名称或位置加载 .class 字节码
- definclass():把 .class 字节码转化为 Class 对象
双亲委派的优点
- 避免类的重复加载:通过委派的方式,可以避免类的重复加载。当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
- 保证安全性:通过双亲委派的方式,可以保证安全性 。因为 BootstrapClassLoader 在加载的时候,只会加载 JAVA_HOME 中的 jar 包里面的类,如 java.lang.String,那么这个类是不会被随意替换的,除非有人跑到你的机器上,破坏你的 JDK。
双亲委派的缺点
- 在双亲委派中,子类加载器可以使用父类加载器已经加载过的类,但是父类加载器无法使用子类加载器加载过的类(类似继承的关系)。
Java 提供了很多服务提供者接口(SPI,Service Provider Interface),它可以允许第三方为这些接口提供实现,比如数据库中的 SPI 服务 - JDBC。这些 SPI 的接口由 Java 核心类提供,实现者确是第三方。如果继续沿用双亲委派,就会存在问题,提供者由 Bootstrap ClassLoader 加载,而实现者是由第三方自定义类加载器加载。这个时候,顶层类加载就无法使用子类加载器加载过的类。
要解决上述问题,就需要打破双亲委派原则。
四、打破双亲委派模型
双亲委派模型并不是一个强制性约束,而是 Java 设计者推荐给开发者的类加载器的实现方式。在一定条件下,为了完成某些操作,可以 “打破” 模型。
打破双亲委派模型的具体方法包括
- 重写 loadClass() 方法
- 利用线程上下文加载器
重写 loadClass 方法
- 在双亲委派的过程,都是在 loadClass() 方法中实现的,因此要想要破坏这种机制,可以自定义一个类加载器,继承 ClassLoader 并重写 loadClass() 方法即可,使其不进行双亲委派。
利用线程上下文加载器
- 利用线程上下文加载器(Thread Context ClassLoader)也可以打破双亲委派。
- Java 应用上下文加载器默认是使用 AppClassLoader。若想要在父类加载器使用到子类加载器加载的类,可以使用 Thread.currentThread().getContextClassLoader()。
// 使用线程上下文类加载器加载资源
public static void main(String[] args) throws Exception{
String name = "java/sql/Array.class";
Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
System.out.println(url.toString());
}
}
六、类加载机制和spi的联系
- spi打破了双亲委托的类加载机制,直接在load方法中调用反射获取到了相关类的实例
七、类加载机制和反射的联系
- 类加载机制是JVM通过文件系统将一系列.class文件转化为二进制流加载到JVM内存并生成一个该类的Class对象,为后面的程序运行提供资源的动作。
- 反射是对于任意一个类,在 运行 状态中,对于任意一个类,都能够知道这个类的所有属性和方法 ;对于任
意一个对象,都能够调用它的任意方法和属性,既然能拿到,我们就可以修改部分类型信息;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。Class对象将编译产生的.class文件通过加载器加载进内存的堆中的,这里就体现出类加载机制。