类加载
类加载过程
类加载过程:Java类加载过程大致分为加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)几个阶段。
- 加载(Loading):
- 这是类加载的第一阶段,类加载器负责从文件系统、网络或其他来源读取类的字节码数据,并根据这些数据在JVM内部创建一个
Class
对象。
- 链接(Linking): 链接阶段又分为三个子阶段:
- 验证(Verification):确保被加载类的正确性,验证字节码确保它遵循Java语言的规范,没有安全问题。
- 准备(Preparation):为类变量(static字段)分配内存,并设置类变量的默认初始值。
- 解析(Resolution):将类、接口、字段和方法的符号引用转换为直接引用。
- 初始化(Initialization):
- 初始化阶段是执行类构造器
<clinit>()
方法的过程。这个方法由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。当初始化一个类时,如果其父类还没有被初始化,则先触发其父类的初始化。
解析阶段
解析阶段:在解析阶段,虚拟机会对类加载阶段得到的二进制数据中的符号引用进行处理。符号引用是一组符号来描述所引用的目标,可以看作是一种抽象的概念,并不直接指向目标的内存地址。例如,一个符号引用可能是一个类的全限定名(Fully Qualified Name)。
类常量池(Class Constant Pool)
类常量池是Class文件结构的一部分,主要存储两种类型的常量:
- 字面量:这包括了文本字符串、声明为final的基本类型值(如int、long等)和引用类型值。
- 符号引用:这包括了类和接口的全限定名、字段的名称和描述符、方法的名称和描述符以及方法接口的名称和类型描述符等。
转换为直接引用:解析阶段的主要任务是将这些符号引用转换为直接引用。这意味着符号引用会被转换成具体的内存地址或是指向内存中的指针,这些引用直接指向目标的存储位置。例如,类的符号引用将被转换成指向方法区中类数据的指针。
不同类型的符号引用:在Java中,符号引用包括类和接口的名称、字段的名称和描述符、方法的名称和描述符等。这些引用在解析阶段被转换为可以直接定位到目标的引用。
动态链接:Java支持动态链接,即一些符号引用在类加载时不会立即解析。例如,对于方法的引用,可能会在第一次使用时才进行解析。这种机制是Java动态绑定和晚期绑定特性的基础。
这个过程是由JVM在运行时自动管理的,确保Java程序在不同环境下具有良好的可移植性和灵活性。解析阶段的处理使得Java能够在运行时动态加载和链接类,这是实现多态和动态绑定的关键
[!tip]
静态对象引用存在方法区中,但是对象实例还是存在堆中
类加载器
java平台提供了三种内建的类加载器,它们按照父子关系层次结构工作:
- 引导类加载器(Bootstrap ClassLoader):
- 它是虚拟机的一部分,用C++编写。负责加载
$JAVA_HOME/jre/lib
目录下的,或者由-Xbootclasspath
参数指定路径中的核心Java库(如java.lang.*
)。
- 扩展类加载器(Extension ClassLoader):
- 它负责加载
$JAVA_HOME/jre/lib/ext
目录下的,或者由系统属性java.ext.dirs
指定路径中的Java扩展库。它是Java编写,由引导类加载器加载。
- 系统(应用)类加载器(System/Application ClassLoader):
- 它负责加载环境变量
CLASSPATH
或者系统属性java.class.path
指定路径中的类库。它是程序中默认的类加载器,一般来说,我们自定义的类都是由它来加载的。
双亲委派模型
Java类加载器采用的是一种称为“双亲委派模型”的策略。当一个类加载器收到类加载请求时,它首先不会尝试自己去加载这个类,而是把这个请求委托给父类加载器去完成。只有当父类加载器反馈自己无法完成这个加载请求(它在搜索范围内没有找到对应的类)时,子类加载器才会尝试自己去加载这个类。这个模型的优点包括:
- 防止类的重复加载。
- 保护程序安全,防止核心API被随意篡改。
自定义类加载器通常是通过继承java.lang.ClassLoader
类并重写findClass
方法来实现的,以支持从非标准来源加载类,如从加密文件、网络、动态生成的字节码等。
为什么打破双亲委派机制
1. 热部署
在一些企业应用中,需要对应用或者模块进行热替换,如果遵循双亲委派机制,如果被父类加载器加载的话就不能呗替换和更新,因为副贼加载器运行期间不会卸载已经加载的类,通过打破双亲委派机制,使用自定义类的加载策略,可以运行时替换或者更新类
2. 不同版本的类共存
某些情况下不同的场景加载类的不同版本,
3. 自定义类加载逻辑
某些高度定义的场景比如从特定的网络位置加载类,或者从加密文件中加载类
4. 插件化架构
在插件化或模块化架构的应用中,每个插件或模块可能需要使用完全独立的类加载器,以确保模块间的完全隔离。这样,每个模块就可以加载和卸载而不影响其他模块,同时也允许模块使用不同版
怎么自定义类加载器
继承java.lang.ClassLoader
类,并重写其findClass
方法。
- 继承
ClassLoader
类:创建一个新类,继承自java.lang.ClassLoader
。 - 重写
findClass
方法:在这个方法中,你需要定义如何查找类。如果类在你的搜索范围内(比如,你的文件系统中的一个特定目录),你需要读取类的字节码,然后调用defineClass
方法来定义这个类。 - (可选)重写
loadClass
方法:在某些复杂的场景下,如果你想完全控制类的加载过程,可以重写loadClass
方法。但是这样做时需要非常小心,以避免破坏双亲委派模型。
从指定的文件路径加载类:
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class CustomClassLoader extends ClassLoader { private String pathToBin; public CustomClassLoader(String pathToBin) { this.pathToBin = pathToBin; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classBytes; try { classBytes = loadClassFromFile(name); } catch (IOException e) { throw new ClassNotFoundException("Could not load class " + name, e); } return defineClass(name, classBytes, 0, classBytes.length); } private byte[] loadClassFromFile(String fileName) throws IOException { File file = new File(pathToBin + fileName.replace('.', File.separatorChar) + ".class"); FileInputStream fis = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len; while ((len = fis.read()) != -1) { bos.write(len); } fis.close(); return bos.toByteArray(); } }
使用自定义类加载器:
public class Main { public static void main(String[] args) throws Exception { String classPath = "/path/to/your/classes"; CustomClassLoader classLoader = new CustomClassLoader(classPath); Class<?> clazz = classLoader.loadClass("com.example.YourClass"); Object obj = clazz.newInstance(); System.out.println("Class loaded and instance created: " + obj.getClass().getName()); } }
CustomClassLoader
会从给定的文件路径加载类。你需要将/path/to/your/classes
替换为实际存放.class
文件的路径。注意,类的完整名称(包括包名)需要正确地传递给loadClass
方法。