1.为什么要有类加载器的设计?
在不使用类加载器的场景中(假想场景),所有的类都是在程序启动时一次性加载到JVM中的。
(1)程序启动慢。需要在启动时一次性加载程序中的所有类,即使在程序运行期间根本用不到。
(2)资源浪费。未被使用的类的加载占用了JVM的内存和其他资源。
(3)类冲突。在复杂的应用程序中,可能会存在多个版本的同一个类库。如果不使用类加载器进行隔离,这些不同版本的类库可能会相互冲突,导致程序运行错误。(所有的类都将处于同一个命名空间中,这会导致版本冲突和命名冲突)
(4)安全性问题。一次性加载所有类的方式缺乏灵活性,无法根据类的来源、用途等信息进行有针对性的安全检查。如果采用一次性加载所有类的方式,当所有类都被加载到内存中时,恶意代码也可能已经混入其中。
2.什么是类加载器?
Java类加载器(Class Loader)是Java运行时环境(JRE)的一部分,负责动态地将Java类加载到Java虚拟机(JVM)的运行时环境中。当一个Java程序需要使用某个类时,JVM并不会立即去加载这个类,而是等到程序真正运行时,第一次引用到这个类的时候,才会通过类加载器来加载这个类。
类加载器必须实现的核心功能是通过全类名获取类的二进制字节流,并将类的二进制字节流解析并转换成JVM能够识别的数据结构(如Java类和接口的内部表示,以及类的元数据信息等),然后存储在JVM的方法区中,最后在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
下面的例子展示了Java类加载的懒加载(或称为延迟加载)特性,即类只在需要时才被加载,这种机制有助于减少程序启动时的内存占用和加载时间。
// MyClass.java
public class MyClass {
public void sayHello() {
System.out.println("Hello, MyClass!");
}
}
// MainClass.java
public class MainClass {
public static void main(String[] args) {
// 在这里,我们还没有创建MyClass的实例或调用其方法
// 因此,根据懒加载的原则,MyClass类还没有被加载到JVM中
// 假设这里有一些其他的逻辑...
// 当我们第一次引用MyClass时(例如,通过创建其实例或调用其静态方法),
// JVM会通过类加载器来加载MyClass类
MyClass myObject = new MyClass();
myObject.sayHello(); // 在这里,MyClass类被加载到JVM中,并执行sayHello方法
// 从现在起,MyClass类已经在JVM中,可以被多次使用而无需重新加载
}
}
3.类加载器的实现
3.1三种类加载器:
(1)启动类加载器(Bootstrap ClassLoader)
由C/C++语言实现,是JVM自带的类加载器,负责加载Java的核心库(如java.lang、java.util等),
(2) 扩展类加载器(Extension ClassLoader)
由Java语言编写,负责加载Java的扩展库,是ClassLoader类的子类
(3) 系统类加载器(System ClassLoader)
别名:应用程序类加载器(Application ClassLoader),是ClassLoader类的子类,负责加载用户类路径(CLASSPATH)上的类库(包括开发者自己编写的类以及第三方库)
3.2三种类加载器之间的组织关系:
(1)逻辑关系
启动类加载器是JAVA中所有类加载器的最顶层父加载器,但出于安全考虑,它并不继承自java.lang.ClassLoader,因此没有父加载器的概念;扩展类加载器的父加载器是启动类加载器;
系统类加载器的父加载器是扩展类加载器。
(2)什么是父加载器
在Java的类加载机制中,"父加载器"是一个相对的概念,它指的是在类加载器的双亲委派模型(Parent Delegation Model)中,当前类加载器用于委托加载类请求的上一级类加载器。
值得注意的是:类加载器的父加载器是一种包含或委托关系(这种关系通过双亲委派模型来实现),而不是Java类之间的继承关系(子加载器并不是父加载器的子类,它们之间的关系是通过委托和组合来实现的,而不是通过继承)。
(3)什么是双亲委派模型(重要面试题)
当Java程序需要使用某个类时,负责加载这个类的类加载器会首先把加载请求委托给它的父加载器,而不是自己直接加载,这种机制被称为双亲委派模型。
向上委派:实际上是查找缓存,是否加载了该类,有则直接返回,没有继续向上。
向下查找:查找加载路径,有则加载返回,没有则继续向下查找。
代码示例:
代码首先获取了ClassLoaderDemo类的类加载器,并打印出来,这通常是Application ClassLoader(系统类加载器),它负责加载用户路径(如CLASSPATH)上的类。
public class ClassLoaderDemo {
public static void main(String[] args) {
// 打印当前类的加载器
ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
System.out.println("ClassLoaderDemo的类加载器是:" + classLoader);
// 获取类加载器的父加载器,并继续向上获取直到为null(Bootstrap ClassLoader)
while (classLoader != null) {
classLoader = classLoader.getParent();
System.out.println("父类加载器是:" + classLoader);
}
// 注意:上面的循环最终会打印null,因为Bootstrap ClassLoader没有父加载器
// Bootstrap ClassLoader是JVM自带的类加载器,负责加载Java的核心类库
// 尝试加载java.lang.String类,观察其加载器
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println("java.lang.String的类加载器是:" + stringClassLoader);
// 通常,核心类(如java.lang.String)是由Bootstrap ClassLoader加载的
// Bootstrap ClassLoader不是java.lang.ClassLoader的子类,因此返回null
}
}
通过循环向上获取类加载器的父加载器,直到达到Bootstrap ClassLoader(启动类加载器),Bootstrap ClassLoader不是java.lang.ClassLoader的子类,因此调用getParent()会返回null。由于java.lang.String是Java的核心类,尝试获取其类加载器,并打印出来,会发现是由Bootstrap ClassLoader加载的,而Bootstrap ClassLoader不是java.lang.ClassLoader的子类,所以调用getClassLoader()会返回null。
(4)为什么需要双亲委派模型(委托加载的好处)
(1)确保Java平台的核心类库的安全性(例如,无论哪个类加载器尝试加载java.lang.Object类,最终都会由启动类加载器来加载,从而避免了被恶意代码替换的风险)
(2)有助于实现类的唯一性,无论通过哪个类加载器加载,同一个类在JVM中只会有一个Class对象,这有助于维护Java的类型安全。
4.怎么用类加载器
4.1.获取类加载器
(1)获取系统类加载器(Application ClassLoader):这是默认的类加载器,通过ClassLoader.getSystemClassLoader()
获取
(2)获取当前类的类加载器
任何类都可以通过调用getClass().getClassLoader()
来获取加载该类的类加载器
(3)获取父加载器
通过调用类加载器的getParent()
方法可以获取其父类加载器(如果有的话),注意,Bootstrap ClassLoader(启动类加载器)没有父类加载器,因此会返回null。
4.2.自定义类加载器
如果需要自定义类加载行为(如从非标准路径加载类、加密的类文件等),可以继承java.lang.ClassLoader类并重写其findClass(String name)
方法(或loadClass(String name, boolean resolve)方法,但通常只重写findClass)。
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 自定义加载逻辑,例如从文件系统、网络等位置加载类
byte[] classData = loadClassData(name); // 假设这个方法实现了从某处加载类数据的逻辑
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String name) {
// 实现加载类数据的逻辑
// ...
return null; // 示例,实际应返回类数据的字节数组
}
}
4.2.使用类加载器来加载类
一旦有了类加载器(无论是系统类加载器、当前类的类加载器还是自定义类加载器),使用其来加载类
ClassLoader classLoader = MyClassLoader.class.getClassLoader(); // 获取MyClassLoader的类加载器,或直接使用自定义的MyClassLoader实例
Class<?> clazz = classLoader.loadClass("com.example.MyClass"); // 加载指定全限定名的类
// 或者,如果是自定义类加载器
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> clazz = myClassLoader.loadClass("com.example.MyClass");
参考视频资料:https://www.bilibili.com/video/BV1HP411t7Lq?p=16&vd_source=ea0f09fc475a05984bf43c87396aa0f8