JVM 类加载器

article/2025/9/18 7:15:53

什么是类加载器

类加载器负责在运行时将Java类动态加载到Java虚拟机,他们也是JRE(Java运行时环境)的一部分。因此,借助类加载器,JVM无需了解底层文件或文件系统即可运行Java程序。此外,这些Java类不会一次全部加载到内存中,而是在应用程序需要他们时才会进行加载,这就是类加载器发挥作用的地方,他们负责将类加载到内存中。

类加载器是有一定的层级关系,以JDK8为例:

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Excension ClassLoaderJAVA_HOME/jre/lib/ext上级为Bootstrap,显示为null
Application ClassLoaderclasspath上级为Excension
自定义的类加载器自定义上级为Application

分为了这几个层级,最顶层的叫Bootstrap ClassLoader(启动类加载器),下一级是Excension ClassLoader(扩展类加载器),再下一级是Application ClassLoader(应用程序类加载器),最后一级是自定义的类加载器

这几个不同层级的类加载器到底有什么关系呢?实际上,每个类加载器各管一块儿,比如Bootstrap ClassLoader只负责加载JAVA_HOME/jre/lib目录下的所有的类,不在这个目录下的类他就不闻不问。类似的,Excension ClassLoader只负责去加载JAVA_HOME/jre/lib/ext扩展目录里面的这些类,除了这个目录,他也不认。最常见的是Application ClassLoader,他负责加载classpath,即类路径下的所有的类。当然,自定义类加载器要加载的类自己可以去定义的。这些是他们各自管的区域。

除此以外,他还有层级关系,比如Application ClassLoader去加载类的时候,他首先会问一问,这些类是不是由它的上级(Excension ClassLoader)加载过了,即看Excension ClassLoader有没有加载过这个类,如果没有,他还会让这个Excension ClassLoader再委托他的上上级即Bootstrap ClassLoader看看有没有加载过这个类,如果他们这两个上级都没有加载,那才轮得到Application ClassLoader去加载这个类。

比如,我们想加载String类,我通过Application ClassLoader去调用他的loadClass方法去加载字符串类,结果他就会去看Excension有没有加载过这个类,若Excension说没有,Excension就会继续去问更上级的Bootstrap有没有记载过string类,结果Bootstrap加载了这个类,因为string类属于JAVA_HOME/jre/lib目录下的一个类,所以他肯定是又这个Bootstrap已经加载过了,这样的话Excension也好还是Application加载器也好,都不用操心这个string类的加载了。

那么再比如说我们自定义一个Student类,同样的也会先让Excension加载器去看有没有加载过,如果没有再去委托Bootstrap加载器有没有加载,当然,由于自定义的student类是不会出现在Java的核心工作目录中(JAVA_HOME/jre/lib),所以Bootstrap就会说没有加载,然后再让Excension加载器去看他自己在JAVA_HOME/jre/lib/ext目录里去找student类,也肯定找不到,所以student类就会交给你Application ClassLoader去加载这个类。

其实上面所介绍的类的这种委托方式,在JVM的领域把他叫做“双亲委派类加载模式”。

所以这个层级关系就是自定义类加载器的上级是应用程序类加载器应用程序类加载器的上级是扩展类加载器扩展类加载器的上级是启动类加载器,需要注意的是,扩展类加载器getParent的时候打印的是null,因为Bootstrap类加载器他不是Java代码写的,而是c++代码写的,所以他不会让我们的Java代码所直接访问。

public void test() throws ClassNotFoundException {System.out.println("Classloader of this class:" + Test.class.getClassLoader());System.out.println("Classloader of Logging:" + Logging.class.getClassLoader());System.out.println("Classloader of ArrayList:" + ArrayList.class.getClassLoader());
}// 运行结果如下:
/*
Classloader of this class:sun.misc.Launcher$AppClassLoader@18a4bbc2 // 应用类加载器(加载classpath中我们自己写的文件)
Classloader of Logging:sun.misc.Launcher$ExtClassLoader@2escaf27 // 扩展类加载器
Classloader of ArrayList:null // 启动类加载器(之所以Null是因为启动类加载器是用native code写的,而不是Java写的,因此他不会显示为Java类)
*/

启动类加载器

Bootstrap加载器通常都是去加载JAVA_HOME/jre/lib目录下的这些类,但是我们也可以通过一些特殊的虚拟机参数把我们自己编写的一些类交由Bootstrap加载器去进行加载。先定义一个F类

public class F {static {System.out.println("bootstrap F init");}
}

然后执行代码如下:

public class Load {public static void main(String[] args) throws ClassNotFoundException {// 通过Class.forName去加载类。Class.forName既可以完成类的加载,也可以顺便做类的链接、初始化操作。Class<?> aClass = Class.forName("com.cnm.F");// 如何知道F类是被哪个类加载器加载了呢?所有的Class都有getClassLoader()方法,获得这个类他对应的类加载器。所以// 打印出来就能知道是哪个类加载器加载了F类。// 如果是应用程序类加载器,就睡输出AppClassLoader,如果是扩展类加载器,就打印ExtClassLoader。而启动类加载器,由于// 是c++程序编写的,所以他不能够通过Java代码直接访问,如果打印出null,就说明他是启动类加载器。System.out.println(aClass.getClassLoader());}
}

执行类加载前,需要注意的是为了避免idea对类路径的一些干扰,可以在idea下面的Terminal命令行下执行这段代码,首先找到输出目录:cd D:\cnm\jvm

接着执行下面的命令,通过java命令去执行Load类(参数-Xbootclasspath是去指定启动类加载的路径,/a是启动类路径追加一些信息,即他不会去改变原本启动类加载起要去加载的那个JAVA_HOME/jre/lib路径,只是在原有基础上追加,后面的.是指把当前目录追加上去):java -Xbootclasspath/a:. com.cnm.Load

  • -Xbootclasspath 表示设置bootclasspath(启动类加载器的类路径),他又几种方式:
    • java -Xbootclasspath:<new bootclasspath>,即写个新路径,就相当于用新路径完全替换掉JAVA_HOME/jre/lib
    • java -Xbootclasspath/a:<追加路径>,即在原有的基础上后面追加。
    • java -Xbootclasspath/p:<追加路径>,这是前追加。(这个和上面的是开发JVM的人去考虑的,对于我们普通用户没必要去考虑,了解即可
  • 其中/a:.表示将当前目录追加至bootclasspath之后
输出如下:
bootstrap F init
null

打印null说明F类是由启动类加载器去加载的。

Java类由java.lang.ClassLoader的对象/实例加载。但是类加载器本身就是一个类,所以谁加载这个ClassLoader类呢?答案是启动类加载器。它主要负责JDK内部类,通常是rt.jar和其他位于$JAVA_HOME/jre/lib目录下的核心类库,此外,启动类加载器是所有其他ClassLoader实例的parent。启动类加载器是核心JVM的一部分,并且是用本机代码(native code)编写的,不同的JVM平台可能有这个特定类加载器的不同实现。

扩展类加载器

public class G {static {System.out.println("classpath G init");}
}public class Load {public static void main(String[] args) throws ClassNotFoundException {Class<?> aClass = Class.forName("com.cnm.G");System.out.println(aClass.getClassLoader());}
}

默认情况下,G肯定在classpath下,那么这里打印出aClass.getClassLoader()肯定是应用程序类加载器,运行如下:

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

AppClassLoader就能知道这确实是由应用程序类加载器去加载的,因为它不可能在启动类加载器的路径下以及扩展类加载器的路径下找到G类

那如果再写一个同名的G类,并放在扩展类加载器路径下,那么,此时G类是会被哪个类加载器加载呢?由于扩展类加载器路径下的类必须是以jar包形式存在,所以,可以把G类打包(为了区分先把输出语句改成"ext G init"),命令行里输入 jar -cvf my.jar com.cnm.G.class,即把G类打包成my.jar,然后把这个jar包放到C:\ProgramFiles\Java\jdk1.8.0_91\jre\lib\ext目录中,这样的话扩展类路径下有了my.jar。然后把刚才改的输出语句恢复原来的"classpath G init"后,再重新运行下Load类,输出如下:

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

可以发现,这回打印的是ext G init,即说明加载的是扩展类路径下的G类my.jar),而不是应用程序路径下的G类,从打印出ExtClassLoader能知道确实是扩展类加载器加载了他。

那如何解释这个现象呢?当我们的应用程序类加载器想去加载这个类的时候,他的先问问他的上级同不同意,上级就是扩展类加载器,结果人家扩展类加载器已经在他的类路径下找到了一个同名的G这个类,所以扩展类加载器就把G加载了,加载以后,应用程序类加载器他就没有机会再加载了。相当于优先级最高的是启动类加载器,其次是扩展类加载器,第三才会轮到应用程序类加载器
网友1:所以咱们要是写了一个和JDK同名的类,是完全用不了滴。

扩展类加载器是启动类加载器的子类(?),负责加载核心Java类的扩展,以便平台上运行的所有应用程序都可以使用他们。扩展类加载器从JDK扩展目录加载,通常是$JAVA_HOME/lib/ext目录,或java.ext.dirs系统属性中存在的任何其他目录。

应用/系统类加载器

负责将所有的程序级别的类加载到JVM中。他会加载在类路径环境变量(classpath)中找到的文件,他也是扩展类加载器的子类(?)。

双新委派模式

所谓的双亲委派,就是指调用类加载器的loadClass方法时,查找类的规则。总的来说,就是委派上级优先来做这个类的加载,上级没有的话,再由我本级的类加载器来完成加载。(需要注意翻译的方式,通常国内文献都把他翻译为双亲委派模式,但翻译为上级似乎更为合适,因为比如启动类加载器、扩展类加载器、应用程序类加载器他们其实并没有一种继承上的父子关系,他只是级别不一样

双新委派模式工作过程

比如,下面的Java代码要通过类加载器的loadClass方法去加载H类

public class Load {public static void main(String[] args) throws ClassNotFoundException {System.out.println(Load.class.getClassLoader());Class<?> aClass = Load.class.getClassLoader().loadClass("com.cnm.H");// 当然H类在classpath下System.out.println(aClass.getClassLoader());}
}

debug模式运行代码,在第二行(Class<?>…省略)暂停后,点进去loadClass方法,去观察他内部的实现。点进去后:

ClassLoader.java
...
public class<?> loadClass(String name) throws ClassNotFoundException {Ereturn loadClass(name, false);// 然后再点击loadClass进去源码
}
...

源码如下:

protected Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException {// 【断点执行到这里时(A)】当前类加载器是谁呢?此时this是AppClassLoader对象,即应用程序类加载器。synchronized (getClassLoadingLock(name)) {// 【断点执行到这里时】调用findLoadedClass,在应用程序类加载器自己的一个缓存中看看有没有已经加载过一个H这样一个类。Class<?> c = findLoadedClass(name);// 【断点执行到这里时】当然,首次肯定是没有的,即c是null。if (c == null) {long t0 = System.nanoTime();try {// 【断点执行到这里时】由于没有找到已加载的类,那他要委托他的上级parent(ExtClassLoader)去查找。if (parent != null) {// 【断点执行到这里时】如果不为空,就去调用扩展类调用器的loadClass方法。// 由于要调用loadClass方法,所以再次鼠标点击该方法进去看看,进去后,再次会跳到上面的(A)处。/*(从头部开始执行,第一次轮回)1,那么此时,当前的this就是扩展类加载器了。2,当然,扩展类加载器也有自己的类的缓存。即调用findLoadedClass函数。先到自己的缓存里去找看看有没有加载过的H。3,当然,也是没有的。所以c是null。4,接下来,扩展类加载器会委托他的上级parent,但此时parent是null,就像上面所说,上级如果是null的话那就表示parent是启动类加载器了,所以这回parent!=null条件不成立,所以会进入(B)处。5,(B)处发现启动类加载器下,还是没有找到H。所以在第一次轮回时,会走到(C)处。*/c = parent.loadClass(name,false);//(相当于递归调用了上级类加载器的loadclass来完成类加载)}else {// (B)parent为启动类加载器时,就会执行该函数,其内部就是去委托启动类加载器到JAVA_HOME\jre\lib下去找H,// 当然肯定是没有的。值得注意的是,该方法内调用的是本地方法,即c++实现的功能,所以看不到源码。c = findBootstrapClassOrNull(name);}}catch(ClassNotFoundException e) {// 如果是调用扩展类加载器的loadClass时,当执行findClass时找不到类的话,就会跑到这里,但这里没有// 做任何处理。接着执行下去的话,又会来到(C)处【D】。}// (C)如果c还是为null,就调用findClass函数。第一次轮回时,此时this是扩展类加载器,所以在这里去找扩展类// 加载器的findClass方法,其内部实际上就是到JAVA_HOME\jre\lib\ext目录下去找H类。(这里findClass会报异常,// 因为他在ext下找不到H类。由于出现异常,所以此时直接跑到catch里面,而不会执行下面的部分。因为这是在扩展// 类加载器执行loadClass时被应用程序加载器的catch给捉住了。)// 【D】由于catch后,从loadClass中跳出来了,所以这回执行时调用的是应用程序类加载器(即自己)的findClass方法。// 应用程序类加载器的findClass内部就是到类路径下去找H类。当然,这个例子中是有的[接下D2]。if (c == null) {long t1 = System.nanoTime();c = findClass(name);// 【D2】此时,c就会有值了,因为类路径下有H类。[接下D3]sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;// 【D3】return。这样的话,就跳出了自己的loadClass方法,所以回到了【E】}
}

类加载器是Java运行时环境的一部分,当JVM请求访问一个类时,类加载器会尝试定位该类并使用类的全名将类加载到运行时环境。java.lang.ClassLoader.loadClass()方法负责将类定义加载到运行时环境。他尝试根据类的全名称加载类。如果该类还没有被加载过,他会将请求委托给父类加载器,这个过程是递归发生的。最终,如果父类加载器没有找到该类,那么子类加载器将调用java.net.URLClassLoader.findClass()方法在文件系统本身中查找类。如果最有一个子类加载器也无法加载该类,他会抛出java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException异常。

线程上下文类加载器

这是一个比较特殊的类加载器。我们在使用JDBC时,都需要加载Driver驱动,不知道你注意到没有,不写Class.forName("com.mysql.jdbc.Driver");也是可以让com.mysql.jdbc.Driver正确加载的,你知道是怎么做的吗?

可以看一下DriverManager的源码:

public class DriverManager {// 注册驱动的集合private final static CopyOnWriteArrayList<DriverInfo> registerdDrivers = new CopyOnWriteArrayList<>();// 初始化驱动static {loadInitialDrivers();println("JDBC DriverManager initialized");}
}

可以看到DriverManager源码里,静态代码块儿中似乎也有一个方法(loadInitialDrivers)是用来加载和初始化驱动类的,看起来好像是
loadInitialDrivers来完成这件事。

但是这样矛盾就来了,可以想一下,DriverManager类所在包是在启动类路径下的,所以他的类加载器实际上就是Bootstrap ClassLoader。如果打印System.out.println(DriverManager.calss.getClassLoader());就能看到打印了null。打印null表示他的类加载器是Bootstrap ClassLoader,会到JAVA_HOME/jre/lib下搜索类,但JAVA_HOME/jre/lib下显然没有mysql-connector-java-5.1.47.jar包,这样问题来了,在DriverManager静态代码块儿中,怎么能正确加载com.mysql.jdbc.Driver驱动类呢?可以继续去查看loadInitialDrivers()方法。

private static void loadInitialDrivers () {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});}catch(Exception ex) {drivers = null;}// 1)使用ServiceLoader机制加载驱动,即SPIAccessController.doPrivileged(new PrivilegedAction<Void> {public void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try {while(driversIterator.hasNext()) {driversIterator.next();}}catch(Throwable t) {// Do noting}return null;}});// 2)使用Java的系统环境变量的叫jdbc.drivers来找到驱动的类名,并加载驱动if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for(String aDriver : driverList) {try {println("DriverManager.Initialize: loading " +  aDriver);// 这里的ClassLoader.getSystemClassLoader() 就是应用程序类加载器Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());}catch(Exception ex) {println("DriverManager.Initialize: load failed:" + ex);}}
}

这个方法里有两个关键的地方,即尝试使用ServiceLoader机制加载驱动,即SPI;然后还有尝试使用Java的系统环境变量的叫jdbc.drivers来找到驱动的类名,并加载驱动。

先看简单的2),在2)里可以发现他实际上是调用了Class.forName来完成了驱动类(aDriver)的类加载,这里他用的是
ClassLoader.getSystemClassLoader(),这里的getSystemClassLoader就是AppClassLoader,也就是应用程序类加载器。所以这块儿他JDK其实
就是打破了这个双亲委派模式,按理来讲,我们在DriverManager这个类初始化的时候,他理应(网友1:理应?网友2:因为这是JVM虚拟机的规范)是用启动类加载器来完成所有与DriverManager相关联的类的加载,但是可以看到这里违反了这一约定,他是使用了应用程序类加载器来加载了驱动类。

其实也可以想象到,在我们用ServiceLoader1)来类加载时,他内部肯定也不能用启动类加载器去加载驱动类,因为根本找不到,他必须还是要用到应用程序类加载器来完成mysql的驱动加载。所以这个问题到这里就相对比较明朗了,那是因为JDK在某些情况下需要打破这个双亲委派模式,那么有时候他会调用应用程序类加载器来完成类加载,否则的话有些类他是找不到的。
网友1:由于JDBC在核心类库中,他有启动类加载器加载。由于驱动是在他的类初始化方法中加载的,所以驱动是DriverManager的依赖,默认是由启动类加载器加载,但找不到,不可能加载到驱动,于是要显式的调用Class的forName方法使用一个能加载驱动的加载器去加载驱动。

那再看1)ServiceLoader,他就是实际上他就是JDK中实现的一种大名鼎鼎的Service Provider Interface(SPI接口)。它主要是为了解耦,他的具体使用规则如下,他要求我们在jar包里加一个特殊的目录,叫META-INF,并且在该目录下有一个services包,在services包下,以你的接口的全限定名名为文件的名称,这个文件的内容就是普通的文本文件,文本文件内部内容就是这个接口的所有的实现类,你只要按照这个约定去设计了这个jar包,那么将来我们就可以配合ServiceLoader来根据接口找到他的实现类,并加以实例化。这样就实现了解耦。(网友1:根据接口找到文件,文件内容是要加载类的类名

在这里插入图片描述
[图片转自黑马]

具体使用如下:

ServiceLoader<接口类型> allTmpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allTmpls.iterator();
while(iter.hasNext()) {iter.next();
}

接口类型一般都是JDK里定义好的,比如上图中的java.sql.Driver这种已经预定义好的接口,可以根据这个接口去进行load,这个load方法就可以找到这个接口的所有的实现类,其实就是根据刚才这个约定找到文件,并找到文件里这些内容,把它们的实现类都拿出来。他是一个集合,可以调用他的迭代器(iterator)然后遍历,每循环一次就调用next方法就可以得到具体的某一个实现类的实例对象了。(这个跟spring容器也有点像,可以根据接口去得到他实现类的实例对象

很多的框架中都运用了这个SPI思想来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet初始化器
  • spring容器
  • Dubbo(对SPI进行了扩展)

那他和类加载有什么关系呢?比如:

// 1)使用ServiceLoader机制加载驱动,即SPI
AccessController.doPrivileged(new PrivilegedAction<Void> {public void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try {while(driversIterator.hasNext()) {driversIterator.next();}}catch(Throwable t) {// Do noting}return null;}}
);
...

比如这里可以看到他调用了ServiceLoader.load(Driver.class),他的内部如下:

public static <S> ServiceLoader<S> load(Class<S> service) {// 获取线程上下文类加载器,Thread.currentThread()是当前线程,然后通过他的getContextClassLoader获取类加载器。ClassLoader c1 = Thread.currentThread().getContextClassLoader();// 通过这种方法获取的类加载器称之为线程上下文(Context)类加载器。return ServiceLoader.load(service,c1);// 然后调用内部方法,把c1(类加载器)传递进去
}

线程类加载器是怎么来的呢,他是在每个线程启动的时候,会由JVM他默认是把应用程序类加载器赋值给当前线程,将来当前线程去调用
getContextClassLoader()方法时,就会拿到应用程序类加载器了。

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,他[ServiceLoader.load(service,c1)]内部又是由Class.forName调用了线程上下文类加载器完成JDBC驱动类的类加载,具体代码在ServiceLoader的内部类LazyInterator中:

private S nextService() {if(!hasNextService())throw new NoSuchElementException();String cn = nextName;nextTime = null;Class<?> c = null;try {/*他又是利用了Class.forName然后去加载按照一个字符串类名(cn)去进行类加载。loader就是上面传递过来的c1(线程上下文类加载器,其实就是应用程序类加载器)。*/c = Class.forName(cn, false, loader);}catch(ClassNotFoundException x) {fail(service, "Provider "+ cn + " not found");}if (!service.isAssignableFrom(c)) {fail(service, "Provider " + cn + " not a subtype");}try {S p = service.cast(c.newInstance());providers.put(cn,p);return p;}catch (Throwable x) {fail(service, "Provider " + cn + " could not be instantiated", x);}
}

所以回过头来看,当DriverManager的静态代码块儿被执行时,虽然DriverManager本身是启动类加载器去加载的,但由于ServiceLoader他内部用的是应用程序类加载器,所以他每调用一次next,他实际上内部其实就是用的线程上下文类加载器完成了类加载,也是破坏了双亲委派的机制,并没有利用启动类加载器去找mysql驱动

自定义类加载器

哪些情况下需要用到自定义类加载器的几种场景:

1)想加载任意路径下的类文件时,此时需要自定义类加载器。比如我们要记载的类文件即不在启动、扩展、classpath下。

2)比如做框架设计的时候,那么需要通过接口来使用不同的实现,这样实现软件的解耦。

3)可能有一个类有多种不同的版本,比如有旧版本有新版本,我希望新旧版本同时工作,虽然这些类的包名、类名都是一样的,但是他里面的字节码有新旧之分,我还希望他同时工作,此时也需要用到自定义的类加载器。这种情况经常是可以在tomcat这样的容器里看到,由于tomcat他就是使用了这种自定义类加载器的方式对不同的应用程序进行了隔离,将来你即使是同名同包的类,也可以在同一个tomcat上运行。

实现自定义加载器的步骤:

1)继承ClassLoader父类

2)要遵从双新委派机制,重写findClass方法,因为只有重写了findClass方法,才会委托上级的类加载器优先去进行类的加载,只有在上级类加载器没有找到该类时,才会调用findClass在本身的类加载器里进行查找,这也是符合双新委派原则。(注意:不要重写loadClass方法,否则不会走双亲委派机制

3)在findClass里读取类文件的字节码,一般都是byte数组

4)读完byte数组后,下一步是调用父类的difineClass方法来加载类,即把byte数组真正完成类加载。

5)对于类加载器的使用者,就让他去调用你类记载其的loadClass方法就可以实现类的加载了。

例子:记载自定义路径下的两个class,比如,D盘中有两个class,分别是MapImpl1.classMapImpl2.class。若反编译的话,大概如下:

MapImpl1.java
public class MapImpl1 extends java.util.AbstractMap implements java.util.Map {public MapImpl1();public java.util.Set<Java.util.Map$Entry> entrySet();public java.lang.String toString();static {};
}MapImpl2.java
public class MapImpl2 extends java.util.AbstractMap implements java.util.Map {public MapImpl2();public java.util.Set<Java.util.Map$Entry> entrySet();public java.lang.String toString();static {};
}

可以看到重写了toString方法,还有static代码块儿。然后写一个自定义的类加载器把他们都加载进来。

Load.java

// 测试类
public class Load {public static void main(String[] args) throws Exception{MyClassLoader classLoader = new MyClassLoader();classLoader.loadClass("MapImpl1");// 你即使多次执行loadClass方法,但实际上类文件只会加载一次,因为第一次加载以后,就会放在自定义类加载器的缓存当中,// 下次再调用loadClass的时候,缓存里已经能找到了,就不会重复的类加载了。Class<?> c1 = classLoader.loadClass("MapImpl1");Class<?> c2 = classLoader.loadClass("MapImpl1");System.out.println(c1 == c2);// true,两个内存地址一样// 那假如用不同的类加载器对象去加载同一个类MyClassLoader classLoader2 = new MyClassLoader();Class<?> c3 = classLoader2.loadClass("MapImpl1");System.out.println(c1 == c3);// false,其实唯一确定类的方式是他的包名类名相同,而且类加载器也要是同一个,他才认为这个类是完全一致的。所以会加载两次,会认为他两是相互隔离的,不会产生冲突。// 最后用反射去调用他// 去创建MapImpl1的对象c1.newInstance();// 打印Map impl1 init,这是静态代码块儿里的内容,所以创建他的实例对象时,就会触发类的静态代码块儿的执行}
}// 自定义的类加载器(要继承ClassLoader,并且要重写findClass方法)
class MyClassLoader extends ClassLoader {// name参数是类名称,要根据这个名称去找到真正的类文件@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {String path = "d:\\" + name + ".class";// 文件的真实路径try {// 读取他,形成一个二进制的字节数组// 把path路径的文件拷贝到输出流os里ByteArrayOutputStream os = new ByteArrayOutputStream();Files.copy(Paths.get(path), os);// 得到字节数组byte[] bytes = os.toByteArray();// 那么字节数组如何变成class类对象呢// 需要调用父类的defineClass方法,他可以把byte[]变成*.class这种类对象// 0是byte数组的第一个开始读取,要读取的长度是bytes.lengthreturn defineClass(name, bytes, 0, bytes.length);}catch(IOException e) {e.printStackTrace();// 网友1:这里为什么要throw?不是已经Print了吗?网友2:自定义异常信息throw new ClassNotFoundException("类文件未找到", e);}}
}

http://chatgpt.dhexx.cn/article/LBud42HJ.shtml

相关文章

类加载器深入理解

虚拟机设计团队把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现&#xff0c;以便让应用程序自己决定如何去获取所需要的类。实现这个动作的模块称为“类加载器”。 类加载器在类层次划分、OSGI、热部署、代码加密等领域…

java获取类加载器

获取类加载器的方法: //扩展类加载器MainClassLoader classLoader MainTest.class.getClassLoader();//表示当前线程的类加载器——应用程序类加载器ClassLoader contextClassLoader Thread.currentThread().getContextClassLoader();//—启动类加载器ClassLoader systemClas…

类加载器的种类

类加载器的种类有四种&#xff0c;如下图所示&#xff1a; 1.启动类加载器&#xff08;引导类加载器&#xff0c;Bootstrap ClassLoader&#xff09; 这个类加载使用C/C语言实现的&#xff0c;嵌套在JVM内部它用来加载Java的核心库&#xff08;JAVA_HOME/jre/lib/rt.jar、res…

Java类加载器详解

1 特点 双亲委派&#xff1a; 如果一个类加载器收到了类加载的请求&#xff0c;它首先不会自己去尝试加载这个类&#xff0c;而是把这个请求委派给父类加载器去完成&#xff0c;每一个层次的类加载器都是如此&#xff0c;因此所有的加载请求最终都应该传送到顶层的启动类加载…

【类加载器】java类加载器

类装载器ClassLoader&#xff08;一个抽象类&#xff09; 描述一下JVM加载class文件的原理机制 类装载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件&#xff0c;在java中类装载器把一个类装入JVM&#xff0c;经过以下步骤&#xff1a; 1、装载&#xff…

什么是类加载器?

类加载器 什么是类加载器&#xff0c;作用是什么&#xff1f; 类加载器就是加载字节码文件(.class)的类 Java语言是一种具有动态性的解释语言&#xff0c;类(CLASS) 只有被加载到 JVM 中后才能运行。当运行指定程序时&#xff0c;JVM会将编译生成的.class文件按照需求和一定的规…

类加载器

类加载过程 加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。 类加载器分类 JVM 中内置了三个重要的 ClassLoader&#xff0c;除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader&#xff1a; 启动类加载器&…

类加载器作用

深入探讨 Java 类加载器 成 富, 软件工程师, IBM 中国软件开发中心 成富任职于 IBM 中国软件开发中心&#xff0c;目前在 Lotus 部门从事 IBM Mashup Center 的开发工作。他毕业于北京大学信息科学技术学院&#xff0c;获得计算机软件与理论专业硕士学位。他的个人网站是 http:…

java中的类加载器

文章目录 前言,一、加载器的作用是什么二、详解类加载器1.不得不说的双亲委派机制2.各个加载器加载的内容3.线程上下文类加载器4.类加载器的庐山真面目 总结 前言, java中一般来说有三种类加载器,分别为: 引导加载器,扩展加载器,应用加载器,还有一个线程上下文类加载器 一、加…

JVM类加载器

文章目录 一、类加载器二、类与类加载器三、双亲委派模型四、破坏双亲委派模型4.1、Tomcat4.1.1、WebApp类加载器4.1.2、Shared类加载器4.1.3、Catalina类加载器4.1.4、Common类加载器4.1.5、Jsp类加载器 4.2、JDBC 一、类加载器 从Java虚拟机的角度来讲&#xff0c;只存在两种…

自定义类加载器

目录 一、为什么要自定义类加载器&#xff1f; 二、常见的场景 三、实现方式 四、自定义类加载器示例 五、Java9新特性 一、为什么要自定义类加载器&#xff1f; 隔离加载类 在某些框架内进行中间件与应用的模块隔离&#xff0c;把类加载到不同的环境。比如&#xff1a;…

类加载器详解(自己实现类加载器)

目录&#xff1a; java虚拟机汇总 class文件结构分析 1).class文件常量池中的常量项结构 2). 常用的属性表的集合类加载过程 1).类加载器的原理以及实现<< 现在位置虚拟机结构分析 1).jdk1.7和1.8版本的方法区构造变化 2).常量池简单区分对象结构分析 1).压缩指针详解gc…

JVM——自定义类加载器

0. 为什么需要自定义类加载器 网上的大部分自定义类加载器文章&#xff0c;几乎都是贴一段实现代码&#xff0c;然后分析一两句自定义ClassLoader的原理。但是我觉得首先得把为什么需要自定义加载器这个问题搞清楚&#xff0c;因为如果不明白它的作用的情况下&#xff0c;还…

JVM - 类加载器

# 类加载器及类加载器执行过程 JDK版本&#xff1a;1.8 # 1、类加载器子系统 下图为类加载子系统&#xff1a; 类加载子系统负责从文件系统或者网络中加载class文件&#xff0c;class文件在文件开头有特定的文件标识&#xff08;CA FE BA BE&#xff09;。Classloader只负责cl…

java的类加载器以及如何自定义类加载器

ClassLoader作用 类加载流程的"加载"阶段是由类加载器完成的。 类加载器结构 结构&#xff1a;BootstrapClassLoader&#xff08;祖父&#xff09;–>ExtClassLoader&#xff08;爷爷&#xff09;–>AppClassLoader(也称为SystemClassLoader)&#xff08;爸…

类加载器(ClassLoader)

一、类加载器&#xff08;ClassLoader&#xff09; 1.1 什么是类加载器 Java的类加载器是Java虚拟机&#xff08;JVM&#xff09;的重要组成部分&#xff0c;它的主要作用是动态地将Java类加载到JVM中&#xff0c;以便在运行时使用这些类。Java类加载器通常是由JVM的实现者提供…

什么是类加载器,类加载器如何分类

一、类加载器 1.什么是类加载器 类加载器&#xff1a;负责将.class文件&#xff08;存储的物理文件&#xff09;加载到内存中 2.类加载时机&#xff1a; ① 创建类的实例&#xff08;对象&#xff09; ② 调用类的实例方法 ③ 访问类或者接口的类变量&#xff0c;或者为该…

深入理解Java虚拟机——再谈类的加载器——第十二章——中篇

深入理解Java虚拟机——Java虚拟机介绍——第一章 深入理解Java虚拟机——类加载子系统——第二章 深入理解Java虚拟机——运行时数据区和本地方法接口——详细篇——第三章 深入理解Java虚拟机——对象的实例化内存布局与访问定位——超级详细篇——第四章 深入理解Java虚拟机…

类加载器详解

类加载器的分类 JVM支持两种类型的类加载器,分别为引导类加载器(BootstrapClassLoader)和自定义类加载器(User-Defined ClassLoader) 从概念上来讲, 自定义类加载器一般指的是程序中由开发人员自定义的一类,类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类…

Java - 类加载器

文章目录 1. 类加载的过程2. 类加载器的分类2.1 引导类加载器 Bootstrap2.2 扩展类加载器 ExtClassLoader2.3 系统类加载器 AppClassLoader2.4 三者之间的关系2.5 自定义类加载器2.6 注&#xff1a;谁来准备类加载器呢? 3. 双亲委派机制4. ClassLoader抽象类5. URLClassLoader…