「JVM 执行子系统」类加载器与双亲委派模型

类加载器(Class Loader)的作用是通过一个类的全限定名来获取描述该类的二进制字节流;自定义类加载器是 JVM 设计团队有意开放给 JVM 外部的能力(用于 Java Applet 技术、类层次划分、OSGi、程序热部署、代码加密等领域);

1. 类与类加载器

任意一个类都必须由加载它的类和这个类本身共同确立骑在 JVM 中的唯一性,每个类加载器都拥有一个独立的类名称空间;

要判定两个类是否相等,要检查类的 Class 对象的 equals() 方法,isAssignableFrom() 方法,isInstance() 方法的返回结果,还有 instanceof 判定对象所属关系的结果;

不同类加载器对 instanceof 运算的结果影响

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".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);
                }
            }
        };

        Object obj = myLoader.loadClass("edu.aurelius.jvm.clazz.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof edu.aurelius.jvm.clazz.ClassLoaderTest);
    }
}

运行结果

class edu.aurelius.jvm.clazz.ClassLoaderTest
false

JVM 中同时存在两个 ClassLoaderTest 类,一个有 JVM 的应用程序类加载器加载的,另一个是有自定义的类加载器加载的,它们来自同一个 Class 文件,但 JVM 将它们当做相互独立的两个类;

2. 双亲委派模型

JVM 的角度,有两种类加载器:启动类加载器Bootstrap Class Loader,HotSpot VM 的 JDK 8 是 C++ 实现,是 JVM 的一部分,JDK 9 是 JVM 与 Java 类相互配合实现的)、其他所有类加载器(Java 语言实现,独立与 JVM 外部,全部继承自 java.lang.ClassLoader);

Java 开发者角度,类加载器有三层:启动类加载器Bootstrap Class Loader)、扩展类加载器Extension Class Loader)、应用程序类加载器Application Class Loader);

请添加图片描述

  • 启动类加载器Bootstrap Class Loader),负载加载 <JAVA_HOME>/bin 目录,或被 -Xbootclasspath 参数指定的路径,而且是 JVM 能够识别的(按文件名识别,只能是 rt.jar、tools.jar 等)类库;若应用程序要将加载委派给启动类加载器,直接使用 null 代替即可;
/**
  * Returns the class loader for the class. Some implementations may use null to represent the bootstrap class loa
  */
public ClassLoader getClassLoader() {
    ClassLoader cl = getClassLoader0();
    if (cl == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader ccl = ClassLoader.getCallerClassLoader();
        if (ccl != null && ccl != cl && !cl.isAncestor(ccl)) {
            sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
        }
    }
    return cl;
}
  • 扩展类加载器Extension Class Loader),在类 sun.misc.Launcher$ExtClassLoader 中实现,负责加载 <JAVA_HOME>/lib/ext 目录,或者 java.ext.dirs 系统变量指定的路径中所有的类库;是 Java 系统类库的扩展机制,在 JDK 9 的模块化取代;开发者也可以直接使用扩展类加载器加载 Class 文件;
  • 应用程序类加载器Application Class Loader),在类 sun.misc.Launcher$AppClassLoader 中实现,负责加载用户类路径(ClassPath)的所有类库;应用程序类是 ClassLoader 类中 getSystemClassLoader() 方法的返回值,因此也叫系统类加载器;若应用程序中没有自定义过类加载器,应用这个类加载器一般就是程序的默认类加载器;

各种类加载器之间的层次关系被称为类加载器的双亲委派模型Parents Delegation Model);双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器;类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码;

双亲委派机制:当类加载器收到了类加载的请求,它会先尝试将这个请求委派给父类加载器去完成,所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载;

双亲委派模型 使得 Java 中的类随它的类加载器一起具备了优先级层次关系;它保障了类在各种类加载器环境中都是同一个类,也保障了 Java 类型体系的稳定性;

即使自定义了自己的类加载器,强行用 defineClass() 方法去加载一个以 java.lang 开头的类也不会成功,JVM 内部将抛出 java.lang.SecurityException: Prohibited package name:java.lang

双亲委派模型的实现

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 首先,检查请求的类是否已经被加载过了
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父类加载器抛出ClassNotFoundException
            // 说明父类加载器无法完成加载请求
        }
        if (c == null) {
            // 在父类加载器无法加载时
            // 再调用本身的findClass方法来进行类加载 c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

3. 破坏双亲委派模型

  • JDK 1.2 之前,尚未使用双亲委派模型,已经存在用户自定义类加载器;为了兼容已有代码,特意新增了 protected 的 findClass(),并引导用户使用复写这个方法来实现自定义类加载器;按照 loadClass() 的逻辑,父类加载失败会自动调用自己的 findClass() 完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的;
  • 当引导类加载器加载的基础类也要调用用户代码时(JNDI 服务,对资源进行查找,集中管理,它需要调用 ClassPath 下的 SPI 代码),引导类加载器是不可能认识、加载这些代码的;为了解决这个问题,引入了不太优雅的线程上下文类加载器Thread Context Class Loader),使用这个线程上下文类加载器去加载所需的 SPI 服务代码;这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器;为了消除这种极不优雅的实现方式,在 JDK 6 时 JDK 提供了 java.util.ServiceLoader 类,以 META-INF/services 中的配置信息辅以责任链模式实现 SPI 的加载;
  • 代码热替换(Hot Swap)、模块热部署(Hot Deployment)等,实现的关键是自定义的类加载器,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换;在 OSGi 环境下,类加载器不再是双亲委派模型的树状结构,而是加复杂的网状结构;

OSGi 类搜索步骤

  • java.* 开头的类委派给父类加载器加载;
  • 否则,将委派列表名单内的类委派给父类加载器加载;
  • 否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载;
  • 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载;
  • 否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle的类加载器加载;
  • 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载;
  • 否则,类查找失败;

上一篇:「JVM 执行子系统」类加载的 5 个阶段
下一篇:「JVM 执行子系统」Java 模块化系统

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 我不认为JVM可以“打破”双亲委派模型JVM使用双亲委派模型是为了保证类的和安全性,这是JVM的基本设计原则之一。 在双亲委派模型中,当一个类被时,首先会委托其父类尝试该类。如果父类无法该类,则会将该请求传递给其父类的父类,直到最终传递到启动类。如果启动类也无法该类,则会抛出ClassNotFoundException异常。 这种模型的好处是可以防止类的重复,并且可以保证类的安全性,因为只有在受信任的类的类才能访问受信任的资源。如果JVM打破了这种模型,可能会导致类的重复和不安全的类访问,从而破坏JVM的基本设计原则。 虽然JVM不能打破双亲委派模型,但是它提供了一些机制来绕过该模型。例如,可以使用Java的反射机制或者自定义类类,但是这些机制都需要开发者自己负责类的和安全性,因此需要谨慎使用。 ### 回答2: JVM双亲委派模型是一种类机制,用于保证Java程序的安全性和稳定性。在双亲委派模型中,JVM首先会查找并自定义类的类,如果找不到,才会向上一级的父类请求。这种模型的好处是可以避免类重复和类冲突,确保程序的运行稳定性。 但有时候,我们可能需要打破双亲委派模型,以满足特定的需求。下面是一些打破双亲委派模型的方式: 1. 自定义类:可以自己编写一个类,并重写其中的类的逻辑。这样就可以绕过父类,直接自定义的类。 2. 本地库:JVM的类无法本地库,因此可以通过JNI(Java Native Interface)来打破双亲委派模型,直接本地库。 3. 使用反射技术:通过反射,可以调用私有的类方法,从而绕过双亲委派模型特定的类。 4. 使用SPI机制:在Java标准库中,有一些接口通过SPI(Service Provider Interface)机制提供了灵活的类方式,可以通过实现自己的SPI来打破双亲委派模型。 需要注意的是,打破双亲委派模型可能会导致类的重复和类冲突,引发程序的运行问题。因此,在使用这些方法时,需要谨慎考虑,确保安全性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aurelius-Shu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值