Java 类加载器详解
什么是类加载器
类加载器(ClassLoader)是 Java 虚拟机(JVM)的重要组成部分,它负责将 Java 类的字节码加载到内存中,并转换为 JVM 可以识别的 Class 对象。
类加载器的作用
- 加载类:将类的字节码从磁盘、网络或其他来源加载到内存中
- 链接类:验证字节码的有效性,准备类的静态变量,解析符号引用
- 初始化类:执行静态初始化块,初始化静态变量
- 命名空间管理:每个类加载器都有自己的命名空间,相同名称的类在不同的类加载器中是不同的
类加载的过程
类加载的过程包括以下几个步骤:
- 加载(Loading):通过类的全限定名找到类的字节码,将其加载到内存中
- 链接(Linking):
- 验证(Verification):验证字节码的有效性
- 准备(Preparation):为静态变量分配内存并设置默认值
- 解析(Resolution):将符号引用转换为直接引用
- 初始化(Initialization):执行静态初始化块,初始化静态变量
Java 内置的类加载器
Java 中有三种类加载器:
引导类加载器(Bootstrap ClassLoader):
- 负责加载 Java 核心类库(如 rt.jar)
- 由 C++ 实现,不是 Java 类
- 没有父类加载器
扩展类加载器(Extension ClassLoader):
- 负责加载 Java 扩展类库(如 lib/ext 目录下的 jar 包)
- 父类加载器是引导类加载器
应用类加载器(Application ClassLoader):
- 负责加载应用程序的类(如 classpath 下的类)
- 父类加载器是扩展类加载器
类加载器的工作原理
类加载的伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Class<?> loadClass(String name) { Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { c = findClass(name); } } return c; }
Class<?> findClass(String name) { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); }
|
类加载的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class ClassLoaderDemo { public static void main(String[] args) { ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader(); System.out.println("应用类加载器:" + classLoader); ClassLoader parentClassLoader = classLoader.getParent(); System.out.println("扩展类加载器:" + parentClassLoader); ClassLoader grandParentClassLoader = parentClassLoader.getParent(); System.out.println("引导类加载器:" + grandParentClassLoader); ClassLoader stringClassLoader = String.class.getClassLoader(); System.out.println("String 类的加载器:" + stringClassLoader); } }
|
双亲委派模型
什么是双亲委派模型
双亲委派模型是 Java 类加载器的一种工作机制,它要求每个类加载器在加载类之前,首先将加载请求委托给父类加载器,只有当父类加载器无法加载时,才由自己尝试加载。
双亲委派模型的工作流程
- 当一个类加载器收到加载类的请求时,它首先不会自己尝试加载,而是将请求委托给父类加载器
- 父类加载器也会同样将请求委托给它的父类加载器,直到到达顶层的引导类加载器
- 如果父类加载器能够加载该类,则返回加载的结果
- 如果父类加载器无法加载,则由当前类加载器尝试加载
双亲委派模型的代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { }
if (c == null) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
|
双亲委派模型的作用
- 避免类的重复加载:当父类加载器已经加载了该类时,子类加载器就不需要再加载一次
- 保护 Java 核心类库:防止核心类库被篡改,例如自定义一个
java.lang.String 类是无法被加载的
- 确保类的层次结构:保证了 Java 核心类的一致性,不同的类加载器加载的类之间的关系是确定的
为什么需要双亲委派模型
- 安全性:防止恶意代码替换 Java 核心类库的类
- 一致性:确保相同的类在不同的类加载器中是相同的
- 效率:避免类的重复加载,提高加载效率
自定义类加载器
如何自定义类加载器
自定义类加载器需要继承 ClassLoader 类,并重写 findClass 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class CustomClassLoader extends ClassLoader { private String classPath;
public CustomClassLoader(String classPath) { this.classPath = classPath; }
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] classData = loadClassData(name); if (classData == null) { throw new ClassNotFoundException(); } return defineClass(name, classData, 0, classData.length); } catch (Exception e) { throw new ClassNotFoundException(e.getMessage()); } }
private byte[] loadClassData(String className) throws Exception { String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try (InputStream is = new FileInputStream(path); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { bos.write(buffer, 0, len); } return bos.toByteArray(); } } }
|
自定义类加载器的使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class CustomClassLoaderTest { public static void main(String[] args) throws Exception { CustomClassLoader classLoader = new CustomClassLoader("d:/classes"); Class<?> clazz = classLoader.loadClass("com.example.TestClass"); Object obj = clazz.newInstance(); Method method = clazz.getMethod("sayHello"); method.invoke(obj); } }
|
自定义类加载器的作用
- 加载非标准位置的类:从网络、数据库、加密文件等非标准位置加载类
- 实现类的热部署:在不重启应用的情况下替换类的实现
- 实现类的加密和解密:对类的字节码进行加密,防止反编译
- 实现类的隔离:不同的类加载器加载的类是隔离的,可以在同一个 JVM 中运行不同版本的类
自定义类加载器的使用场景
- 应用服务器:如 Tomcat,需要隔离不同的 Web 应用
- OSGi 框架:需要实现模块的热部署和隔离
- 插件系统:需要动态加载插件
- 加密应用:需要对类进行加密保护
- 远程调用:需要从网络加载类
打破双亲委派模型
在某些情况下,需要打破双亲委派模型,例如:
- Tomcat:需要加载 Web 应用的类,而不是委托给父类加载器
- OSGi:需要实现模块的热部署和隔离
打破双亲委派模型的方法是重写 loadClass 方法,不按照双亲委派的顺序加载类。
打破双亲委派模型的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class BreakClassLoader extends ClassLoader { private String classPath;
public BreakClassLoader(String classPath) { this.classPath = classPath; }
@Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if (c == null) { try { c = findClass(name); } catch (ClassNotFoundException e) { return super.loadClass(name, resolve); } } if (resolve) { resolveClass(c); } return c; }
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { } }
|
类加载器的命名空间
每个类加载器都有自己的命名空间,命名空间由该类加载器及其所有父类加载器加载的类组成。
两个类相等的条件:
- 类的全限定名相同
- 由同一个类加载器加载
类加载器的引用
类加载器会引用它加载的类,类也会引用它的类加载器:
1 2 3 4 5
| ClassLoader classLoader = TestClass.class.getClassLoader();
|
类加载器的垃圾回收
当类加载器不再被引用时,它加载的类也可能被垃圾回收(如果这些类也不再被引用)。
总结
类加载器是 Java 虚拟机的重要组成部分,它负责将类的字节码加载到内存中并转换为 Class 对象。双亲委派模型是类加载器的核心机制,它保证了 Java 核心类的安全性和一致性。自定义类加载器可以实现一些特殊的功能,如加载非标准位置的类、实现类的热部署、加密类等。
通过理解类加载器的工作原理,我们可以更好地掌握 Java 的运行机制,解决一些与类加载相关的问题,如类冲突、类加载失败等。同时,自定义类加载器也为我们提供了一种灵活的扩展机制,可以根据需要实现一些特殊的功能。