本文记录笔者关于Java中的类加载机制的相关理解
在这一部分,我们对类加载机制的前世今生作一个简单的总结,也就是对这一部分标题中的问题作相关回答
简而言之,类加载就是
JVM类加载机制 接收的是: .class文件的二进制字节流,产出的是: java.lang.Class对象。
类加载机制主要依赖类加载器和双亲委派机制进行实现,概括如下
关于双亲委派,本文中的后续部分会进行讲解
通过一个类全限定名称来获取其二进制文件(.class)流的工具,被称为类加载器(classloader)。
Java支持的四种类加载器
具体如图所示

如图,Java的类加载器可被分为四类
1.启动类加载器Bootstrap ClassLoader
2.标准扩展类加载器 Extension ClassLoader
3.应用类加载器 Application ClassLoader
4.用户自定义类加载器 User ClassLoader
类的生命周期可以划分为 7 个阶段
1.加载
2.验证
3.准备
4.解析
5.初始化
6.使用
7.卸载

其中1-5的阶段,统一被成为类加载
加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。
该过程可以总结为**「JVM 加载 Class 字节码文件到内存中,并在方法区创建对应的 Class 对象」**。
当 JVM 加载完 Class 字节码文件,并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。
这个校验过程,大致可以分为下面几个类型
JVM 规范校验
JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。
例如,校验文件是否是以 0x cafe babe 开头,主次版本号是否在当前虚拟机处理范围之内等。
代码逻辑校验
JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。
例如,一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。
准备阶段中,JVM 将为类变量分配内存并初始化。
准备阶段,有两个关键点需要注意
1.内存分配的对象
2.初始化的类型
这里需要注意的点在于两点
1.内存分配的对象是类变量而不是类成员变量
2.这里的变量初始化只针对类变量,且初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
例:
public static int factor = 3;
public String website = "www.google.com";
如上代码,在准备阶段,只会为 factor 变量分配内存,而不会为 website 变量分配内存,同时为factor变量所赋的值为Java变量类型的默认初始值0,而不是程序所希望为他赋的值3
解析过程中,JVM 针对「类或接口」、「字段」、「类方法」、「接口方法」、「方法类型」、「方法句柄」、「调用点限定符」这 7 类引用进行解析。解析过程的主要任务是将其在常量池中的符号引用,替换成其在内存中的直接引用。
到了初始化阶段,用户定义的 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 的方法句柄,并且这个方法句柄所对应的类没有进行初始化时,则需要先出触发其初始化。
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。
1.JVM 中,类加载器默认使用双亲委派原则
2.如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。
3.为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。
JVM 中,类加载器默认使用双亲委派原则。
双亲委派机制是一种任务委派模式,是 Java 中通过加载工具(classloader)加载类文件的一种具体方式。 具体表现为
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);
}
}
上述代码的相关检测流程如下:
ClassLoader 中和类加载有关的方法有很多,前面提到了 loadClass(),除此之外,还有 findClass() 和 defineClass() 等。这3个方法的区别如下
双亲委派模型并不是一个强制性约束,而是 Java 设计者推荐给开发者的类加载器的实现方式。在一定条件下,为了完成某些操作,可以 “打破” 模型。
打破双亲委派模型的具体方法包括
// 使用线程上下文类加载器加载资源
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());
}
}