双亲委派模型与自定义类加载器

简介: 双亲委派模型与自定义类加载器

双亲委派模型与自定义类加载器


转载请注明出处:【huachao1001的专栏:http://blog.csdn.net/huachao1001】

其实,双亲委派模型并不复杂。自定义类加载器也不难!随便从网上搜一下就能搜出一大把结果,然后copy一下就能用。但是,如果每次想自定义类加载器就必须搜一遍别人的文章,然后复制,这样显然不行。可是自定义类加载器又不经常用,时间久了容易忘记。相信你经常会记不太清loadClass、findClass、defineClass这些函数我到底应该重写哪一个?它们主要是做什么的?本文大致分析了各个函数的流程,目的就是让你看完之后,难以忘记!或者说,延长你对自定义类加载器的记忆时间!随时随地想自定义就自定义!

1. 双亲委派模型

关于双亲委派模型,网上的资料有很多。我这里只简单的描述一下,就当是复习。

1.1 什么是双亲委派模型?

首先,先要知道什么是类加载器。简单说,类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。如果站在JVM的角度来看,只存在两种类加载器:

启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。

其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:

扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。

应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

微信图片_20221214182741.png

1.2 为什么需要双亲委派模型?

为什么需要双亲委派模型呢?假设没有双亲委派模型,试想一个场景:

黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。

或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

举个简单例子:

ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。

1.3 如何实现双亲委派模型?

双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实ClassLoader类默认的loadClass方法已经帮我们写好了,我们无需去写。

2. 自定义类加载器

2. 1几个重要函数

2.1.1 loadClass

loadClass默认实现如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

再看看loadClass(String name, boolean resolve)函数:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        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) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

从上面代码可以明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大致过程如下:

首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。

如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。

如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

话句话说,如果自定义类加载器,就必须重写findClass方法!

2.1.1 find Class

findClass的默认实现如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

可以看出,抽象类ClassLoader的findClass函数默认是抛出异常的。而前面我们知道,loadClass在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass函数,因此我们必须要在loadClass这个函数里面实现将一个指定类名称转换为Class对象.

如果是是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为Class对象呢?很简单,Java提供了defineClass方法,通过这个方法,就可以把一个字节数组转为Class对象啦~

2.1.1 defineClass

defineClass主要的功能是:

将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。如,假设class文件是加密过的,则需要解密后作为形参传入defineClass函数。

defineClass默认实现如下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError  {
        return defineClass(name, b, off, len, null);
}

2.2 函数调用过程

上一节所提的函数调用过程如下:

微信图片_20221214182748.png

2.3 简单示例

首先,我们定义一个待加载的普通Java类:Test.java。放在com.huachao.cl包下:

package com.huachao.cl;
public class Test {
    public void hello() {
        System.out.println("恩,是的,我是由 " + getClass().getClassLoader().getClass()
                + " 加载进来的");
    }
}

注意:

如果你是直接在当前项目里面创建,待Test.java编译后,请把Test.class文件拷贝走,再将Test.java删除。因为如果Test.class存放在当前项目中,根据双亲委派模型可知,会通过sun.misc.Launcher$AppClassLoader 类加载器加载。为了让我们自定义的类加载器加载,我们把Test.class文件放入到其他目录。

在本例中,我们Test.class文件存放的目录如下:

微信图片_20221214182754.png

接下来就是自定义我们的类加载器:

import java.io.FileInputStream;
import java.lang.reflect.Method;
public class Main {
    static class MyClassLoader extends ClassLoader {
        private String classPath;
        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
    };
    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.huachao.cl.Test");
        Object obj = clazz.newInstance();
        Method helloMethod = clazz.getDeclaredMethod("hello", null);
        helloMethod.invoke(obj, null);
    }
}

最后运行结果如下:

恩,是的,我是由 class Main$MyClassLoader 加载进来的


相关文章
|
存储 Go
Go语言接口声明规范和最佳实践
Go语言接口声明规范和最佳实践
512 0
|
5月前
|
人工智能 架构师 安全
2026 研发效能蓝皮书:深度测评年度最佳 AI 代码生成平台 Top 榜单
在 2026 年,AI 代码助手已完成从“代码补全”到“研发智能体”的跨越。本文立足于阿里云开发者生态,深度横评全球顶尖 AI 编程平台,旨在为企业及开发者提供一份基于工程化落地、逻辑准确性及安全性视角的选型指南。
965 2
|
安全 Java 编译器
使用枚举实现“状态”转换限制
我是小假 期待与你的下一次相遇 ~
553 4
|
10月前
|
数据采集 机器学习/深度学习 算法
智慧交通大数据分析
智慧交通大数据分析通过多源数据采集、实时计算与智能算法,实现交通态势感知、拥堵预测、信号优化及应急响应,提升通行效率与出行体验。结合实战案例,展现其在缓解拥堵、提高管理效能方面的显著成效,推动城市交通智能化发展。(238字)
661 0
|
机器学习/深度学习 人工智能 小程序
RL 和 Memory 驱动的 Personal Agent,实测 Macaron AI
人工智能不仅提升生产力,也重塑人际关系。Macaron AI 探索“哆啦A梦关系”,融合实用与情感,通过长期记忆和强化学习技术,实现深度个性化陪伴,开创人机互动新方式。
714 1
|
Web App开发 数据安全/隐私保护
快手批量发布作品工具,短视频一键发布到多个平台, 自媒体批量发布工具
这个批量发布工具支持快手、抖音、B站和YouTube平台,使用Selenium实现自动化上传
|
缓存 监控 JavaScript
HarmonyOSNext应用无响应全解析:从机制到实战的卡死问题排查
本文深入解析HarmonyOS Next中应用无响应的卡死问题,涵盖THREAD_BLOCK_6S、APP_INPUT_BLOCK与LIFECYCLE_TIMEOUT三种常见类型,结合日志分析与实战案例,手把手教你快速定位并解决主线程阻塞、输入事件卡顿及生命周期超时问题,适用于Stage模型开发者学习使用。
1091 0
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
存储 缓存 关系型数据库
滴滴面试:单表可以存200亿数据吗?单表真的只能存2000W,为什么?
40岁老架构师尼恩在其读者交流群中分享了一系列关于InnoDB B+树索引的面试题及解答。这些问题包括B+树的高度、存储容量、千万级大表的优化、单表数据量限制等。尼恩详细解释了InnoDB的存储结构、B+树的磁盘文件格式、索引数据结构、磁盘I/O次数和耗时,以及Buffer Pool缓存机制对性能的影响。他还提供了实际操作步骤,帮助读者通过元数据找到B+树的高度。尼恩强调,通过系统化的学习和准备,可以大幅提升面试表现,实现“offer直提”。相关资料和PDF可在其公众号【技术自由圈】获取。

热门文章

最新文章