网站首页 > java教程 正文
- 类加载器(ClassLoader)概述
- 定义和作用:类加载器是 Java 虚拟机(JVM)的一个组成部分,它负责加载类的字节码文件(通常是.class文件)到内存中,使得这些类可以被 JVM 执行。类加载器是 Java 语言动态性的关键支撑,它使得 Java 程序可以在运行时加载新的类,这对于插件式架构、动态代理等功能非常重要。
- 类加载器的层次结构:Java 中有 3 种主要的类加载器,它们构成了一个层次结构。
- 启动类加载器(Bootstrap ClassLoader):它是最顶层的类加载器,主要负责加载 Java 的核心类库,如java.lang包下的类(像String、Object等)。启动类加载器是用原生代码(C/C++)实现的,它没有对应的 Java 类,在 Java 程序中无法直接获取对它的引用。
- 扩展类加载器(Extension ClassLoader):它的父加载器是启动类加载器。扩展类加载器负责加载 Java 的扩展类库,这些类库通常位于jre/lib/ext目录下。它是java.net.URLClassLoader的子类,可以通过ClassLoader.getSystemClassLoader().getParent()来获取它的引用。
- 应用程序类加载器(Application ClassLoader):也称为系统类加载器,它的父加载器是扩展类加载器。应用程序类加载器负责加载用户自定义的类路径(classpath)下的类,这是我们在日常开发中最常接触到的类加载器。可以通过ClassLoader.getSystemClassLoader()来获取它的引用。
JVM 类加载机制
- 加载(Loading)阶段
- 字节码文件的获取:类加载的第一步是将类的字节码文件加载到内存中。类加载器会根据类的全限定名(如com.example.MyClass)来寻找字节码文件。字节码文件可以从本地文件系统、网络(如通过 HTTP 从远程服务器下载)或者其他数据源获取。例如,应用程序类加载器会在用户指定的classpath下查找类对应的.class文件。
- 验证(Verification):在加载字节码的过程中,JVM 会对字节码进行验证。验证的目的是确保字节码的格式正确,并且遵循 Java 虚拟机规范。这包括检查字节码的结构是否合法,例如方法的字节码指令是否符合语法规则,变量的访问是否在合法的范围内等。如果字节码验证不通过,将会抛出VerifyError异常。
- 准备(Preparation):准备阶段是为类的静态变量分配内存并设置默认初始值。需要注意的是,这个阶段只会对静态变量进行内存分配和初始化默认值,而不会执行任何自定义的初始化代码。例如,对于一个 static int 变量,会被初始化为 0;对于一个静态的Object引用变量,会被初始化为null。
- 连接(Linking)阶段
- 验证(Verification):连接阶段的验证和加载阶段的验证有所不同。这里主要是对符号引用进行验证,确保类之间的引用是合法的。例如,检查一个类中引用的其他类是否确实存在,方法调用的签名是否匹配等。如果符号引用验证失败,会抛出NoClassDefFoundError或IncompatibleClassChangeError等异常。
- 准备(Preparation):此阶段和加载阶段的准备也有区别。这里主要是为类的静态变量分配内存并设置初始值,这个初始值是根据变量的类型和在字节码中定义的初始值来设置的。例如,如果一个静态变量在代码中被初始化为一个常量表达式(如private static final int value = 10;),那么在这个阶段就会将value设置为 10。
- 解析(Resolution):解析阶段是将类、接口、字段和方法的符号引用转换为直接引用。符号引用是一种在字节码中使用的、以字符串形式表示的对其他类、方法等的引用。直接引用则是指向目标的实际内存地址或者偏移量。例如,当一个类中调用了另一个类的方法时,在解析阶段会确定这个方法在内存中的实际位置,以便在运行时能够正确地调用。
- 初始化(Initialization)阶段
- 静态变量初始化和静态代码块执行:初始化阶段是类加载过程中的一个关键阶段。这个阶段会执行类的初始化代码,主要是对静态变量进行初始化(如果有初始值设定)和执行静态代码块。静态变量的初始化按照代码中出现的顺序进行。例如,如果有一个类MyClass,其中有两个静态变量static int a = 10;和static int b = a + 5;,那么首先a会被初始化为 10,然后b会被初始化为 15。同时,静态代码块也会按照在类中出现的顺序执行。静态代码块可以用于在类加载时进行一些一次性的初始化操作,如初始化数据库连接池、加载配置文件等。
- 类构造器<clinit>方法:在 Java 中,编译器会为每个类生成一个<clinit>方法(类构造器),这个方法包含了所有静态变量的初始化语句和静态代码块的内容。在初始化阶段,这个方法会被执行。需要注意的是,<clinit>方法是线程安全的,因为在多线程环境下,只有一个线程能够执行类的初始化操作,其他线程会被阻塞,直到初始化完成。
双亲委派机制(Parents Delegation Model)
工作原理详细步骤:
- 第一步:接收加载请求:当一个类加载器(假设是应用程序类加载器)收到类加载的请求时,这个请求包含了要加载的类的全限定名,比如com.example.DemoClass。
- 第二步:委派给父加载器:它首先会将这个加载请求委派给它的父加载器(扩展类加载器)。应用程序类加载器不会先尝试自己去查找和加载这个类。
- 第三步:父加载器继续委派(如果有):扩展类加载器收到请求后,因为它的父加载器是启动类加载器,所以它会把请求继续委派给启动类加载器。
- 第四步:启动类加载器尝试加载:启动类加载器会在它负责的区域(主要是 Java 核心类库)查找是否有对应的类。如果启动类加载器能够找到并加载这个类(例如这个类是java.lang.String这种核心类库中的类),它就会将加载后的类返回,整个加载过程结束。
- 第五步:父加载器尝试加载(如果启动类加载器未找到):如果启动类加载器找不到要加载的类,它会将请求返回给扩展类加载器。此时扩展类加载器会在它负责的区域(Java 扩展类库)尝试查找和加载这个类。
- 第六步:应用程序类加载器尝试加载(如果扩展类加载器未找到):如果扩展类加载器也找不到要加载的类,它会将请求返回给应用程序类加载器。最后,应用程序类加载器会在用户自定义的classpath下查找并尝试加载这个类。如果应用程序类加载器也找不到,就会抛出ClassNotFoundException异常。
优点:
- 保证核心类库安全与一致性:这种机制最重要的优点是保证了 Java 核心类库的安全性和一致性。通过让启动类加载器优先加载核心类库,避免了用户自定义的类覆盖 Java 核心类库中的类。例如,java.lang.String类是由启动类加载器加载的,用户无法通过自定义一个同名的类来替换它,因为应用程序类加载器会先委托给启动类加载器去加载java.lang.String类,这确保了系统的稳定性和核心类库的不可篡改性。
- 避免类的重复加载:双亲委派机制能够有效地避免类的重复加载。因为类加载请求会先由父加载器处理,当父加载器已经加载过某个类时,子加载器就无需再次加载,这样可以节省内存和系统资源,提高系统的运行效率。
缺点:
- 一定程度上限制了灵活性:在某些特殊情况下,这种严格的层次结构可能会限制类加载的灵活性。例如,在一些需要热部署或者动态加载自定义类来替换原有类的场景中,双亲委派机制可能会成为阻碍。因为它总是优先使用父加载器加载的类,想要打破这种规则需要额外的复杂操作。
- 可能导致类加载问题排查困难:由于双亲委派机制涉及多层类加载器的交互,当出现类加载相关的问题(如类找不到、版本冲突等)时,排查问题的难度会增加。需要仔细分析每个类加载器的加载路径和加载过程,才能确定问题所在。
打破双亲委派机制的方法:
- 自定义类加载器并重写loadClass方法:可以通过自定义类加载器并覆盖loadClass方法来打破双亲委派机制。在loadClass方法中,改变原有的委派顺序或者跳过委派过程,直接尝试加载类。例如:
import java.io.IOException;
import java.io.InputStream;
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 先尝试自己加载类,而不是先委派给父类加载器
try {
String fileName = name.replace('.', '/') + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
}
- 使用线程上下文类加载器(Thread Context ClassLoader):线程上下文类加载器可以在运行时动态地设置和获取。它提供了一种在双亲委派机制之外加载类的方式,常用于在框架中加载用户自定义的类或者插件。例如,在一些 Java EE 应用服务器中,当需要加载应用程序中的类时,可能会使用线程上下文类加载器来绕过双亲委派机制。在代码中,可以通过Thread.currentThread().getContextClassLoader()来获取线程上下文类加载器,然后使用它来加载类。这种方式使得框架代码可以在不违反双亲委派机制的情况下,让用户自定义的类加载器有机会加载特定的类。
- 打破双亲委派机制带来的风险:
- 类版本冲突风险:打破双亲委派机制后,可能会导致类版本冲突。因为没有了双亲委派机制对类加载顺序的严格控制,不同的类加载器可能会加载不同版本的同一个类。例如,一个自定义类加载器加载了一个与核心类库中同名但版本不同的类,这可能会导致程序在运行时出现不兼容的情况,如方法签名不匹配、变量类型不一致等问题,甚至可能引发程序崩溃。
- 安全风险:双亲委派机制的存在在一定程度上保证了类加载的安全性。打破这一机制可能会使恶意代码更容易被加载。如果自定义的类加载逻辑没有进行严格的安全检查,例如在从非信任的数据源(如网络、用户输入等)加载类时,可能会导致恶意代码被注入到系统中,从而对系统安全造成威胁。
- 类加载混乱导致的运行时异常:随意打破双亲委派机制可能会导致类加载过程变得混乱。不同的类加载器可能会以不符合预期的方式加载类,使得类之间的关系(如继承、接口实现等)在运行时出现问题。例如,一个类可能无法正确地访问它所依赖的其他类,因为这些类可能是由不同的类加载器加载的,这会导致ClassCastException、NoSuchMethodError等各种运行时异常。
自定义类加载器
- 需求场景:在一些特定的场景下,我们需要自定义类加载器。例如,当需要从非标准的数据源(如加密的字节码文件、数据库等)加载类时,或者需要实现类的动态加载和更新(如热部署)功能时,就需要自定义类加载器。
- 实现步骤:要自定义一个类加载器,通常需要继承java.lang.ClassLoader类,并实现findClass方法。在findClass方法中,可以编写从自定义数据源获取字节码文件的逻辑,然后调用defineClass方法将字节码文件转换为Class对象。例如,以下是一个简单的自定义类加载器的骨架代码:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
try {
byte[] classData = loadClassData(className);
return defineClass(className, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException("Class not found: " + className, e);
}
}
private byte[] loadClassData(String className) throws IOException {
String fileName = classPath + File.separator + className.replace('.', File.separatorChar) + ".class";
FileInputStream fis = new FileInputStream(fileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length;
while ((length = fis.read(buffer))!= -1) {
bos.write(buffer, 0, length);
}
fis.close();
return bos.toByteArray();
}
}
- 使用注意事项:在自定义类加载器时,需要注意遵循双亲委派机制,除非有特殊的需求。同时,要确保字节码文件的来源是可靠的,避免加载恶意代码。另外,自定义类加载器加载的类可能会和其他类加载器加载的类产生隔离问题,需要谨慎处理类之间的交互。
猜你喜欢
- 2025-02-04 SpringBoot开发 - 什么是热部署和热加载?devtool的原理是什么?
- 2025-02-04 JVM详解之:类的加载链接和初始化(类加载java)
- 2025-02-04 面试官:什么是java类加载当中的双亲委派?
- 2025-02-04 jvm超详细探索自定义类加载器(值得收藏)
- 2025-02-04 详解java反射的原理(java 的反射)
- 2025-02-04 JVM性能调优(1)——JVM内存模型和类加载运行机制
- 2025-02-04 Java反射:作用与原理解析(java反射到底有什么用)
- 2025-02-04 java类加载与初始化(java类的加载机制及加载过程)
- 2025-02-04 类加载的验证阶段你不知道的东西(类加载检查)
- 2025-02-04 类是如何加载的?(类的加载过程是什么?简单描述一下每个步骤)
你 发表评论:
欢迎- 最近发表
-
- Java常量定义防暴指南:从"杀马特"到"高富帅"的华丽转身
- Java接口设计原则与实践:优雅编程的艺术
- java 包管理、访问修饰符、static/final关键字
- Java工程师的代码规范与最佳实践:优雅代码的艺术
- 编写一个java程序(编写一个Java程序计算并输出1到n的阶乘)
- Mycat的搭建以及配置与启动(mycat部署)
- Weblogic 安装 -“不是有效的 JDK Java 主目录”解决办法
- SpringBoot打包部署解析:jar包的生成和结构
- 《Servlet》第05节:创建第一个Servlet程序(HelloSevlet)
- 你认为最简单的单例模式,东西还挺多
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)