本文主要是介绍重新对Java的类加载器的学习方式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《重新对Java的类加载器的学习方式》:本文主要介绍重新对Java的类加载器的学习方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教...
Java 类加载器和JVM 类加载器是同一体系中的不同概念。
Java 类加载器是一个更高抽象层面的概念,可以实现自定义加载器;而 JVM 类加载器是 JVM 的实现部分,负责实际的字节码加载。
类加载器:负责将.class文件加载到内存中,并为之生成对应的Class对象(是JVM执行类加载机制的前提)
1、介绍
1.1、简介
在Java中,类加载器(ClassLoader)是Java虚拟机(JVM)用来加载类的核心组件。
它负责将Java字节码文件(.class文件)动态加载到内存中,并将其转化为JVM可以执行的类对象。类加载器是Java运行时系统的一部分,它支持Java的动态特性,使得Java程序可以在运行时加载类和接口。
如下图所示:
1.2、符号引用和直接引用
关于符号引用和直接引用的介绍这里先进行一个理解,下面在类加载器执行过程的连接阶段,有个解析的过程需要联系到这里的知识。
- 符号引用:指的是一个字符串,该字符串表示一个类、字段或方法的名称,这个名称由 JVM 在运行时解析。
- 直接引用:指的是内存中对象的具体地址或偏移量,它用于直接访问该字段或调用该方法。
1、符号引用
符号引用通常在 Java 字节码(class
文件)中以字符串的形式出现。它是类与类之间一种相对的、灵活的引用方式。这种引用方式在字节码编译时就已经确定,但实际内存地址在运行时才会分配。
示例:符号引用的特点
考虑一下这段代码:
class Example { void hello() { System.out.println("Hello, World!"); } }
在编译成字节码后,hello
方法的符号引用会包含:
- 方法名:
hello
- 方法描述符:
()V
(意味着无参数且没有返回值)
在字节码中的常量池部分,可以看到以下条目(这里是简化的表示):
1. Class: 'Example' 2. Method: 'hello' with descriptor '()V'
这个符号引用不会包含任何内存地址或具体实现细节。
2、直接引用
当 JVM 运行时解析符号引用时,它会将符号引用转换为直接引用。直接引用是指向内存中对象或方法入口的确切地址,这样 JVM 就可以直接访问它们。
示例:直接引用的获取过程
下面的例子展示了符号引用到直接引用的过程。
public class Main { public static void main(Sjavascripttring[] args) { Example eandroidxample = new Example(); // 创建 Example 对象 example.hello(); // 调用 hello 方法 } } class Example { void hello() { System.out.println("Hello, World!"); } }
在这个代码中:
example.hello()
是调用hello
方法。在编译时,hello
方法的引用是符号引用。- 当 JVM 到达这行代码时,它首先会解析
hello
的符号引用。
3、符号转直接的过程
1.指向类定义:
在符号引用查找过程中,JVM 会查找并确认 Example
类的符号引用,即它的名称 Example
。
2.处理方法:
一旦 Example
类被确认可用,JVM 将查找 hello
方法的符号引用。当它找到这个符号引用时,它会定位到方法在内存中的地址(也就是直接引用)。
这个直接引用通常是方法在内存中相对于类对象的偏移。
3.执行调用:
最后,JVM 使用这个直接引用来调用 hello
方法,从而直接在内存中找到并执行方法的字节码。
2、加载流程
Java是一个动态语言,这意味着类在程序运行时被加载,而不是在编译时完成加载。类加载器的主要任务就是将类的字节码文件从文件系统或网络等资源加载到内存中。
具体而言,类加载器的职责包括:
- 加载类:将Java字节码文件读取到内存,并转换为
Class
对象。 - 链接类:将类的二进制数据合并到JVM运行时环境中。这一步包括验证、准备和解析。
- 初始化类:执行类的静态初始化块和静态变量的初始化。
关于上述各个阶段的主要流程下面进行了详细的介绍。
由上图可知:
ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。
整个执行过程可以分为三大步:加载、连接、初始化。
1.加载:将字节码文件通过IO流读取到JVM的方法区,并同时在堆中生成Class对像。
2.链接:
- 验证:校验字节码文件的正确性。
- 准备:为类的静态变量分配内存,并初始化为默认值;对于final static修饰的变量,在编译时就已经分配好内存了。
- 解析:将类中的符号引用转换为直接引用。
注意:如果类加载后,未通过验证,则不能被使用。
3.初始化:对类的静态变量和静态代码块初始化为指定的值,执行静态代码。
- ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的。
- ClassLoader是否可以运行,则由Execution Engine决定。
- ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例,然后交给Java虚拟机进行链接、初始化等操作。
3、类加载的分类
分为显式加载 vs 隐式加载(即JVM加载class文件到内存的方式)
3.1、显示加载:
在代码中显示调用ClassLoader加载class对象。
实现方式
- 1.Class.forName(name)
- 2.this.getClass().
- 3.getClassLoader().loadClass() 。
加载class对象。
3.2、隐式加载:
通过虚拟机自动加载到内存中,是不直接在代码中调用ClassLoader的方法加载class对象,类在被引用(如调用静态方法或访问静态字段)时自动加载。
如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
代码示例:
// 隐式加载 User user = new User(); // 显式加载,并初始化 Class clazz = Class.forName("com.test.java.User"); // 显式加载,但不初始化 ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent");
心得:
- 隐式加载:为开发者简化了加载过程,不需要显式调用,通常在程序中不易察觉。
- 显式加载:可用于动态加载类,灵活控制加载时机。
4、命名空间
命名空间指的是在一定范围内,标识符(如类名、变量名等)被唯一绑定到一个特定的实体。对于类加载器而言,命名空间是China编程指每个类加载器有其各自的发现和加载类的范围,它负责自己加载的类及其依赖关系。
4.1、类加载器和命名空间的关系
1.隔离性
每个类加载器都有一个独立的命名空间。相同类名的类可以在不同的类加载器中存在,而彼此不会干扰。
例如,一个 JAR 中的 com.example.MyClass
可以通过不同的类加载器各自加载,而不会发生冲突。
2.父类优先原则
当一个类加载器加载类时,它会首先将加载请求委托给它的父类加载器。这种机制确保了核心类库得以优先加载,从而避免了相同名称的类在不同上下文中出现。
4.2、示例
以下是一个简单的示例,展示不同类加载器之间的命名空间如何影响类的加载。
MyClass.java:
package com.example; public class MyClass { static { System.out.println("MyClass loaded!"); } }
CustomClassLoader.java:
import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String filePath = name.replace('.', File.separatorChar) + ".class"; try (FileInputStream fis = new FileInputStream(new File(filePath))) { byte[] b = new byte[fis.China编程available()]; fis.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException("Class not found: " + name, e); } } }
Main.java:
public class Main { public static void main(String[] args) { try { CustomClassLoader loader1 = new CustomClassLoader(); CustomClassLoader loader2 = new CustomClassLoader(); // 使用两个自定义类加载器加载同一类 Class<?> class1 = loader1.loadClass("com.example.MyClass"); Class<?> class2 = loader2.loadClass("com.example.MyClass"); // 检查两个类加载器加载的类是否相同 System.out.println("Are class1 and class2 the same? " + (class1 == class2)); // 创建实例 Object instance1 = class1.getDeclaredConstructor().newInstance(); Object instance2 = class2.getDeclaredConstructor().newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
当运行 Main.java
时,输出可能会是:由于加载器不同。
MyClass loaded! MyClass loaded! Are class1 and class2 the same? false
解释
- 双重加载:由于我们使用了两个不同的自定义类加载器 (
loader1
和loader2
) 来加载同一个类com.example.MyClass
,因此它们在内存中生成了两个不同的Class
实例。 - 命名空间隔离:
class1
和class2
虽然指向同一个类的符号引用,但由于在不同的类加载器中加载,它们拥有独立的命名空间,因此class1 == class2
结果为false
。
总结
- 命名空间:为每个类加载器创建了一个独立的命名空间,使得相同名称的类可以共存于不同的上下文中。
- 父类优先原则:用于提升类加载的安全性,优先通过父类加载器查找类。
- 自定义类加载器:可以实现轻松管理类加载过程,提供更多灵活性和控制权。
5、类加载器的分类
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(C z ClassLoader)。
- Bootstrap ClassLoader(启动类加载器):
- Extension ClassLoader(扩展类加载器):
- Application ClassLoader(系统类加载器):
自定义类加载器:扩展 java.lang.ClassLoader
代码示例:
public class ClassTestLoader extends ClassLoader{ public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = new ClassTestLoader(); Class<?> clazz = classLoader.loadClass("com.ali.sls.test.Counter"); System.out.println("class loader:=="+ clazz.getClassLoader()); System.out.println("class loader:=="+ clazz.getClassLoader().getParent()); System.out.println("class loader:=="+ clazz.getClassLoader().getParent().getParent()); } } class loader:==sun.misc.Launcher$AppClassLoader@18b4aac2 class loader:==sun.misc.Launcher$ExtClassLoader@6433a2 class loader:==null
6、双亲委派
Java的类加载机制采用了双亲委派模型(Parent Delegation Model)。该模型的核心思想是:当一个类加载器试图加载某个类时,它会先将这个请求委托给父类加载器,而不是自己直接加载。只有当父类加载器无法找到该类时,才由当前类加载器尝试加载。
6.1、缓存机制
每个类加载器(包括父类加载器)都会维护一个缓存,用于存储已经加载过的类。当类加载器收到加载请求时,会首先检查缓存中是否已经加载过该类。如果已经加载过,则直接返回缓存的类,而不会重新加载。
- 子类加载器加载的类:
子类加载器加载的类会存储在子类加载器的缓存中,父类加载器无法访问子类加载器的缓存。
- 父类加载器加载的类:
父类加载器加载的类会存储在父类加载器的缓存中,子类加载器可以通过双亲委派机制访问父类加载器的缓存。
因此,如果子类加载器已经加载了某个类,父类加载器不会再次加载该类,因为父类加载器无法感知子类加载器的缓存。
6.2、类的唯一性
在JVM中,类的唯一性是由 类的全限定名 + 类加载器 共同决定的。即使两个类加载器加载了同一个类的字节码,JVM也会将它们视为不同的类。
如果子类加载器加载了一个类,父类加载器再次尝试加载同一个类,JVM会认为这是两个不同的类(因为类加载器不同)。这可能导致 类冲突 或 类型转换异常,因为JVM认为这两个类是独立的。
6.3、工作流程:
类加载器接收到加载请求时,首先将请求委派给父类加载器。
如果父类加载器能找到该类,则加载成功;如果父类加载器无法加载该类,则由当前类加载器加载。这种机制确保了Java核心类库不会被用户自定义的类加载器替代或覆盖。
6.4、如何打破
1:自定义类加载器
我们将创建一个自定义的类加载器,它直接加载某个特定包中的类,而不经过父加载器。
import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> loadClass(String name) throws ClassNotFoundException { // 检查是否需要执行自定义加载 if (name.startsWith("com.example")) { // 这里实际上不调用父类加载器 return findClass(name); } // 否则,使用默认的父类加载器 return super.loadClass(name); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String filePath = classPath + File.separator + name.replace('.', File.separatorChar) + ".class"; try (FileInputStream fis = new FileInputStream(filePath)) { byte[] b = new byte[fis.available()]; fis.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException("Class not found: " + name, e); } } }
解释:
- 在
loadClass
方法中,我们直接处理以com.example
开头的类,调用findClass
来加载。而对于其他类,则调用super.loadClass(name)
,这表明默认的父类加载器将处理它。
注意:确保 com/example/MyClass.class
文件在 "path/to/classes"
目录中。
2.使用 Thread
的上下文类加载器
在某些情况下,也可以通过设置当前线程的上下文类加载器(context class loader)来打破双亲委派。
public class ContextClassLoaderExample { public static void main(String[] args) { // 设置自定义类加载器为当前线程的上下文类加载器 Thread.currentThread().setContextClassLoader(new MyClassLoader("path/to/classes")); // 然后通过上下文类加载器加载类 ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); try { Class<?> myClass = contextClassLoader.loadClass("com.example.MyClass"); // 直接调用上下文类加载器 System.out.println("Loaded class: " + myClass.getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
7、类的卸载
Java 的 GC(垃圾回收)会在类引用不存在时进行类的卸载。只有使用 ClassLoader
加载的类,如果其 ClassLoader 被卸载,该类才会被卸载。
类的卸载并不是一个强制性的操作,只有在特定条件下才会发生。
7.1、卸载条件
类的卸载发生在以下情况下:
1.类加载器被垃圾回收
当没有任何引用指向某个类加载器时,该类加载器及其所加载的类有可能被卸载。JVM 可以回收类加载器,并同时卸载它加载的所有类。
2.类及其类加载器均无法到达
类和类加载器不仅没有引用,而且它们在常量池、栈帧等处也不再被引用时,可以进行卸载。
7.2、触发条件
虽然部分类可以在 Java 程序运行时被卸载,但是的确没有显式的方法去卸载类,整个卸载过程是由 JVM 的垃圾回收器自动处理。
以下是一些触发类卸载的条件。
1.动态类加载:
当使用自定义类加载器动态加载类时,如果不再有引用指向这个类和类加载器,它们会被视为垃圾对象,从而可能被回收。
2.Classpath 变化:
如果应用程序在运行时改变了类路径(比如加载新版本的同名类),旧的类及其加载器可能被卸载。
以下是一个类卸载的简单示例:
public class ClassUnloadingExample {
public static void main(String[] args) {
CustomClassLoader classLoader = new CustomClassLoader();
try {
Class<?> clazz1 = classLoader.loadClass("cojavascriptm.example.MyClass");
// 使用 clazz1
Object instance = clazz1.getDeclaredConstructor().newInstance();
System.out.println("Class Loaded: " + clazz1.getName());
// 设置 classLoader 为 null,解除引用
classLoader = null;
// 触发垃圾回收
System.gc();
Thread.sleep(1000); // 确保 GC 有足够时间运行
// 这里将不会再有引用指向这个类,可能被卸载
System.out.println("Unloading classes...");
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 自定义类加载器
class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 加载类的逻辑
// 假设类文件框架完整
return super.findClass(name);
}
}
8、类加载异常处理
在类加载过程中可能会遇到的异常主要有:
ClassNotFoundException
: 当请求的类不存在时抛出。NoClassDefFoundError
: 类存在但不再可用,通常是因为 class 文件被删除或 JVM 启动时未找到。UnsupportedClassVersionError
: 由于 Java 版本不兼容导致的错误。
总结
通过上述文章的介绍,希望可以帮助开发者在项目日常中更加清晰了解java类的加载机制原理。
这篇关于重新对Java的类加载器的学习方式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!