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

article/2025/9/18 8:47:14

ClassLoader作用

  • 类加载流程的"加载"阶段是由类加载器完成的。

类加载器结构

结构:BootstrapClassLoader(祖父)–>ExtClassLoader(爷爷)–>AppClassLoader(也称为SystemClassLoader)(爸爸)–>自定义类加载器(儿子)

关系:看括号中的排位;彼此相邻的两个为父子关系,前为父,后为子

注意,这里的父子关系并不是通过继承构建的,而是在创建子加载器时,将父加载器通过setParent设置进去的,也就是组合模式,而非继承模式

BootstrapClassLoader

  • 下边简称为boot
  • C++编写
  • 为ExtClassLoader的父类,但是通过ExtClassLoader的getParent()获取到的是null(在类加载器部分:null就是指boot)
  • 主要加载:\Java\jdk1.6\jre\lib*.jar(最重要的就是:rt.jar)

ExtClassLoader:

  • 下边简称为ext
  • java编写,位于sun.misc包下,该包在你导入源代码的时候是没有的,需要重新去下
  • 主要加载:\Java\jdk1.6\jre\lib\ext*.jar(eg.dnsns.jar)

AppClassLoader:

  • 下边简称为app
  • java编写,位于sun.misc包下
  • 主要加载:类路径下的jar,也就是classpath下的类,包括编译后的classes文件夹下的class文件和jar包中的class文件

自定义类加载器:

  • 下边简称为custom,自定义加载器的parent为AppClassLoader
  • 自己编写的类加载器,需要继承ClassLoader类或URLClassLoader,并至少重写其中的findClass(String
    name)方法,若想打破双亲委托机制,需要重写loadClass方法
  • 主要加载:自己指定路径的class文件 类加载器之间的关系见下图
    image-20220114154738554

不同类加载器的命名空间关系

咱们先回顾一下命名空间的概念:

  • 每个类加载器都有自己的命名空间。命名空间由该加载器和所有父加载器所加载的类组成。(请结合下图一起看,想明白)
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

得出命名空间的关系如下:(请结合下图一起看,想明白)

  • 同一个命名空间的类是相互可见的。
  • 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器能看见根类加载器加载的类。
  • 由父类加载器加载的类不能看见子加载器加载的类。
  • 如果两个加载器没有父子关系,那么他们自己加载的类互相不可见。
    image-20220114155612769
    这样,子加载器的命名空间,包含了父加载器的命名空间,就可以保证,子加载器加载的类,可以使用父加载器加载的类。也就是说,父加载器加载的类,对子加载器可见,同级别加载器加载的类,不可见

类的唯一性

在运行期,一个类的唯一性是由以下2点共同决定:

  • 该类的完全限定名(binary name)。(包+类名)
  • 用于加载该类的[定义类加载器],即defining class loader。
    上述2点都一样,才代表该类(可以理解为该类的Class对象)是一样的。
    如果同样的名字,不同的类加载器加载,那么这2个类是不一样的。即使.class文件完全一样,.class文件路径一样,这2个类也是不一样的。

双亲委托机制

这也是类加载器加载一个类的整个过程。

过程:假设我现在从类路径下加载一个类A,

1)那么app会先查找是否加载过A,若有,直接返回;

2)若没有,去ext检查是否加载过A,若有,直接返回;

3)若没有,去boot检查是否加载过A,若有,直接返回;

4)若没有,那就boot加载,若在E:\Java\jdk1.6\jre\lib*.jar下找到了指定名称的类,则加载,结束;

5)若没找到,boot加载失败;

6)ext开始加载,若在E:\Java\jdk1.6\jre\lib\ext*.jar下找到了指定名称的类,则加载,结束;

7)若没找到,ext加载失败;

8)app加载器加载,若在类路径(classpath)下找到了指定名称的类,则加载,结束;

9)若没有找到,抛出异常ClassNotFoundException

注意:

  • 在上述过程中的1)2)3)4)6)8)后边,都要去判断是否需要进行"解析"过程 ("解析"见 第四章 类加载机制)
  • 类的加载过程只有向上的双亲委托,没有向下的查询和加载,假设是ext在E:\Java\jdk1.6\jre\lib\ext*.jar下加载一个类,那么整个查询与加载的过程与app无关。
  • 假设A加载成功了,那么该类就会缓存在当前的类加载器实例对象C中,key是(A,C)(其中A是类的全类名,C是加载A的类加载器对象实例),value是对应的java.lang.Class对象
  • 上述的1)2)3)都是从相应的类加载器实例对象的缓存中进行查找
  • 进行缓存的目的是为了同一个类不被加载两次
  • 使用(A,C)做key是为了隔离类,假设现在有一个类加载器B也加载了A,key为(A,B),则这两个A是不同的A。这种情况怎么发生呢?
  • 假设有custom1、custom2两个自定义类加载器,他们是兄弟关系,同时加载A,这就是有可能的了

总结:

  • 从底向上检查是否加载过指定名称的类;从顶向下加载该类。(在其中任何一个步骤成功之后,都会中止类加载过程)
  • 双亲委托的好处:假设自己编写了一个java.lang.Object类,编译后置于类路径下,此时在系统中就有两个Object类,一个是rt.jar的,一个是类路径下的,在类加载的过程中,当要按照全类名去加载Object类时,根据双亲委托,boot会加载rt.jar下的Object类,这是方法结束,即类路径下的Object类就没有加载了。这样保证了系统中类不混乱。

ClassLoader.java类提供了loadClass方法,确保双亲委派机制,如下


```bash
/*** 根据指定的binary name加载class。* 步驟:* 假设我现在从类路径下加载一个类A,* 1)那么app会先查找是否加载过A(findLoadedClass(name)),若有,直接返回;* 2)若没有,去ext检查是否加载过A(parent.loadClass(name, false)),若有,直接返回;* findBootstrapClassOrNull(name) 3)4)5)都是这个方法* 3)若没有,去boot检查是否加载过A,若有,直接返回;* 4)若没有,那就boot加载,若在E:\Java\jdk1.6\jre\lib\*.jar下找到了指定名称的类,则加载,结束;* 5)若没找到,boot加载失败;* findClass(name) 6)7)8)9)都是这个方法* 在findClass中调用了defineClass方法,该方法会生成当前类的java.lang.Class对象* 6)ext开始加载,若在E:\Java\jdk1.6\jre\lib\ext\*.jar下找到了指定名称的类,则加载,结束;* 7)若没找到,ext加载失败;* 8)app加载,若在类路径下找到了指定名称的类,则加载,结束;* 9)若没有找到,抛出异常ClassNotFoundException* 注意:在上述过程中的1)2)3)4)6)8)后边,都要去判断是否需要进行"解析"过程*/protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {Class c = findLoadedClass(name);//检查要加载的类是不是已经被加载了if (c == null) {//没有被加载过try {if (parent != null) {//如果父加载器不是boot,递归调用loadClass(name, false)c = parent.loadClass(name, false);} else {//父加载器是boot/** 返回一个由boot加载过的类;3)* 若没有,就去试着在E:\Java\jdk1.6\jre\lib\*.jar下查找 4)* 若在bootstrap class loader的查找范围内没有查找到该类,则返回null 5)*/c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//父类加载器无法完成加载请求}if (c == null) {//如果父类加载器未找到,再调用本身(这个本身包括ext和app)的findClass(name)来查找类c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}
![image-20220114161133272](https://img-blog.csdnimg.cn/ab9197edf4d5416f836b813f29e58500.png)
ClassLoader类提供了findClass方法的空实现,jdk1.2以后,用户如果需要自定义类加载器,只需要继承自ClassLoader,然后覆写findClass方法,这样可以确保双亲委派机制。jdk1.2以前,用户需要重写loadClass方法。```bash
AppClassLoader(系统加载器)和ExtClassLoader(扩展类加载器)都继承自URLClassLoader,URLClassLoader覆写了findClass方法

说明:

  • 该段代码中引用的大部分方法实质上都是native方法
  • 其中findClass方法的类定义如下:
/*** 查找指定binary name的类* 该类应该被ClassLoader的实现类重写*/protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}

关于findClass可以查看URLClassLoader.findClass(final String name),其中引用了defineClass方法,在该方法中将二进制字节流转换为了java.lang.Class对象。

递归基于栈实现。

上述的代码如果不清楚递归的意义是看不清的。

解释:

  • app的loadClass()方法执行到ext的loadClass(),这时候对于app_loadClass()中剩余的findClass()会在栈中向下压;
  • 然后执行ext_loadClass(),当执行到findBootstrapClassOrNull(name),这时候ext_loadClass()中剩余的findClass()也会从栈顶向下压,此时ext_loadClass()_findClass()仅仅位于app_loadClass()_findClass()的上方;
  • 然后执行findBootstrapClassOrNull(name),当boot检测过后并且执行完加载后并且没成功,boot方法离开栈顶;
  • 然后执行此时栈顶的ext_loadClass()_findClass()
  • 然后执行此时栈顶的app_loadClass()_findClass()

这样,就完成了双亲委托机制。

实验

  1. 定义pojo类
public class MyPerson {private MyPerson myPerson;public void  setMyPerson(Object obj){this.myPerson = (MyPerson)obj;}
}
  1. 定义自定义类加载器
package com.hisense.testbean.classloader;import java.io.*;public class CustomizedClassLoader extends ClassLoader {private String classLoaderName;private String path;private String fileExtension = ".class";public CustomizedClassLoader(String classLoaderName) {super();this.classLoaderName = classLoaderName;}public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {super(parent);this.classLoaderName = classLoaderName;}@Overridepublic Class<?> findClass(String className) throws ClassNotFoundException {System.out.println("findClass invoked : " + className);System.out.println("class loader name : " + this.classLoaderName);byte[] data = this.loadClassData(className);return this.defineClass(className, data, 0, data.length);}private byte[] loadClassData(String className) {byte[] data = null;className = className.replace(".", "/");try(InputStream is = new FileInputStream(new File(this.path + className + this.fileExtension));ByteArrayOutputStream baos = new ByteArrayOutputStream()) {int ch;while(-1 != (ch = is.read())) {baos.write(ch);}data = baos.toByteArray();} catch (Exception e) {e.printStackTrace();}return data;}public void setPath(String path) {this.path = path;}
}

测试1

public class TestClassLoaderNameSpace {public static void main(String[] args) throws Exception {CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");String path = "";System.out.println(path);loader1.setPath(path);loader2.setPath(path);Class<?> clazz1 = loader1.loadClass("com.hisense.testbean.classloader.MyPerson");Class<?> clazz2 = loader2.loadClass("com.hisense.testbean.classloader.MyPerson");System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());System.out.println( clazz1 == clazz2);Object object1 = clazz1.newInstance();Object object2 = clazz2.newInstance();Method method = clazz1.getMethod("setMyPerson", Object.class);method.invoke(object1, object2);}
}

设置path为“”,实际上时设置类的加载路径为classpath,此时根据类的加载机制

第一次调用

loader1.loadClass("com.hisense.testbean.classloader.MyPerson");

CustomizedClassLoader(未找到)->app(未找到)->ext(未找到,向bootstrap查找,调用findBootstrapClassOrNull(name))->ext(调用ext的findclass,不是/java/ext路径,递归栈返回)->app(调用app的findclass(),发现类属于classpath路径,加载类,通过defineclass完成加载类)

第二次调用

loader2.loadClass("com.hisense.testbean.classloader.MyPerson");

CustomizedClassLoader(未找到)->app(已经加载过,缓存中找到)直接返回加载的类person

因此,person的加载器实际上为AppClassLoader,因此我们不能把com.hisense.testbean.classloader.MyPerson放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载

以上输出为:

clazz1的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
clazz2的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
true

测试2

注意:

需要删除target下的person.class文件,否则会优先通过app加载器,取classpath下加载,自定义的CustomizedClassLoader就不会加载了

public class TestClassLoaderNameSpace {public static void main(String[] args) throws Exception {CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");String path = "D:/test/";System.out.println(path);loader1.setPath(path);loader2.setPath(path);Class<?> clazz1 = loader1.loadClass("com.hisense.testbean.classloader.MyPerson");Class<?> clazz2 = loader2.loadClass("com.hisense.testbean.classloader.MyPerson");System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());System.out.println( clazz1 == clazz2);Object object1 = clazz1.newInstance();Object object2 = clazz2.newInstance();Method method = clazz1.getMethod("setMyPerson", Object.class);method.invoke(object1, object2);}
}

手动把Person.java编译为Person.class,拷贝到"D:/test/“下,设置类的加载路径为"D:/test/”,由于"D:/test/"不在系统classpath路径下,因此加载时,

CustomizedClassLoader(未找到)->app(未找到)->ext(未找到,向bootstrap查找,调用findBootstrapClassOrNull(name))->ext(调用ext的findclass,不是/java/ext路径,递归栈返回)->app(调用app的findclass(),不是classpath下的路径,不加载,递归栈返回)->CustomizedClassLoader(调用自己的findClass方法,自己取D:/test/下加载类),加载完成,返回类

此时,调用自定义加载器CustomizedClassLoader的findClass加载类,但是由于CustomizedClassLoader不是单例,此处我们定义了两个CustomizedClassLoader的实例loader1和loader2,loader1加载完以后,同级别的loader2并不知道person类的存在,因此loader2会再次加载一次。因此,二者不是同一个class类实例。彼此不可见

输出

D:/test/
findClass invoked : com.hisense.testbean.classloader.MyPerson
class loader name : loader1
findClass invoked : com.hisense.testbean.classloader.MyPerson
class loader name : loader2
clazz1的classLoader是com.hisense.testbean.classloader.CustomizedClassLoader@340f438e
clazz2的classLoader是com.hisense.testbean.classloader.CustomizedClassLoader@2d6e8792
false
Exception in thread "main" java.lang.reflect.InvocationTargetExceptionat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:497)at com.hisense.testbean.classloader.TestClassLoaderNameSpace2.main(TestClassLoaderNameSpace2.java:23)
Caused by: java.lang.ClassCastException: com.hisense.testbean.classloader.MyPerson cannot be cast to com.hisense.testbean.classloader.MyPersonat com.hisense.testbean.classloader.MyPerson.setMyPerson(MyPerson.java:8)... 5 more

报错,说明两个person类时不可见的,因为二者时不同加载器加载的

自定义类加载器的父类为什么是AppClassLoader

首先有一个概念要了解,我们通常说在jdk中,默认有三个类加载器,bootstrapClassLoader、ExtClassLoader、AppClassLoader,我们说前者是后者的父类加载器,但是实际上,这里所谓的父类加载器,并不是Java中的父子类继承关系,而是说:

  • AppClassLoader中有一个parentClassLoader设置的值是ExtClassLoader
  • ExtClassLoader中的parentClassLoader设置的是bootstrapClassLoader
    我们自定义的类加载器,对应的parentClassLoader是AppClassLoader
  • 我们如果自定义一个类加载器,默认设置的父类加载器是AppClassLoader,这个原因是在因为在初始化自定义类加载器的时候,会指定其parentClassLoader为AppClassLoader

Launcher
这个类是jre中用来启动main()方法的入口,在这个类中,我们着重关注的是初始化构造方法

public Launcher() {Launcher.ExtClassLoader var1;try {// 初始化extClassLoadervar1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {// 初始化appClassLoader,这里的这个赋值是比较重要的this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}Thread.currentThread().setContextClassLoader(this.loader);String var2 = System.getProperty("java.security.manager");if (var2 != null) {SecurityManager var3 = null;if (!"".equals(var2) && !"default".equals(var2)) {try {var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();} catch (IllegalAccessException var5) {} catch (InstantiationException var6) {} catch (ClassNotFoundException var7) {} catch (ClassCastException var8) {}} else {var3 = new SecurityManager();}if (var3 == null) {throw new InternalError("Could not create SecurityManager: " + var2);}System.setSecurityManager(var3);}
}

这个构造方法中,我目前所了解到的,就是初始化了AppClassLoader和ExtClassLoader,并且,和这篇博客相关的,我们只需要关心this.loader这个全局变量,这个全局变量存放的是AppClassLoader的对象信息

构造一个自定义类加载器

我们要自定义一个类加载器,只需要继承classLoader,并重写findClass()方法即可

class MyClassLoaderTest extends ClassLoader {private String loadPath;public String getLoadPath() {return loadPath;}public void setLoadPath(String loadPath) {this.loadPath = loadPath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] bytes = loadByte(name);return defineClass(name, bytes, 0, bytes.length);} catch (Exception e) {System.out.println(e.getMessage());}return null;}private byte[] loadByte(String className) throws Exception {className = className.replaceAll("\\.", "/");FileInputStream fileInputStream = new FileInputStream(loadPath + "/" + className + ".class");int available = fileInputStream.available();byte[] data = new byte[available];fileInputStream.read(data);fileInputStream.close();return data;}
}

那么,我在使用的时候,只需要调用new MyClassLoaderTest()即可

new MyClassLoaderTest()
我们说,自定义的类解析器对应的parentClassLoader,就是在空参构造函数中被赋值的
因为MyClassLoaderTest继承了ClassLoader,所以,会调用到ClassLoader的空参构造函数

protected ClassLoader() {this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {this.parent = parent;if (ParallelLoaders.isRegistered(this.getClass())) {parallelLockMap = new ConcurrentHashMap<>();package2certs = new ConcurrentHashMap<>();domains =Collections.synchronizedSet(new HashSet<ProtectionDomain>());assertionLock = new Object();} else {// no finer-grained lock; lock on the classloader instanceparallelLockMap = null;package2certs = new Hashtable<>();domains = new HashSet<>();assertionLock = this;}
}

我们会发现,parentClassLoader就是getSystemClassLoader()返回的,

java.lang.ClassLoader#getSystemClassLoaderjava.lang.ClassLoader#initSystemClassLoader
// 这个方法中的其他变量我们可以暂时先不关心,我们看到有获取到一个Launcher对象
private static synchronized void initSystemClassLoader() {if (!sclSet) {if (scl != null)throw new IllegalStateException("recursive invocation");sun.misc.Launcher l = sun.misc.Launcher.getLauncher();if (l != null) {Throwable oops = null;// 在这里调用其getClassLoader()方法,将返回的值,赋值给scl,而这个scl就是入参中的parentscl = l.getClassLoader();try {scl = AccessController.doPrivileged(new SystemClassLoaderAction(scl));} catch (PrivilegedActionException pae) {oops = pae.getCause();if (oops instanceof InvocationTargetException) {oops = oops.getCause();}}if (oops != null) {if (oops instanceof Error) {throw (Error) oops;} else {// wrap the exceptionthrow new Error(oops);}}}sclSet = true;}
}

这里的getClassLoader()返回的就是上面我说明的,需要特别关注的this.classLoader这个全局变量
scl是从l.getClassLoader()这个方法获取到的结果,那我们看下这个方法

sun.misc.Launcher#getClassLoader
public ClassLoader getClassLoader() {return this.loader;
}

可以看到,这里的getClassLoader,就是博客上面 Launcher 这个方法中赋值的this.loader;所以,通过上面的代码,可以发现,我们自定义的类解析器,是在初始化的时候,指定了parent为AppClassLoader

自定义类加载器

1、类加载器

类加载器ClassLoader的作用有两个:

①是用于将class文件加载到JVM。
②是用于判断JVM运行时两个类是否相等。

2、类加载的时机

类的加载可分为隐式加载和显示加载。

隐式加载

隐式加载包括以下几种情况:

  • 遇到new(new
    一个实例对象的时候)、getstatic(获取一个类的静态字段的时候)、putstatic(设置一个类的静态字段的时候)、invokestatic(调用一个类的静态方法的时候)这4条字节码指令时。
  • 对类进行反射调用时。
  • 初始化一个类时,如果父类还没有初始化,则先加载其父类并初始化(但是初始化接口时,不要求先初始化父接口)
  • 虚拟机启动时,需要指定一个包含main函数的主类,优先加载并初始化这个主类。

显式加载

显示加载包含以下几种情况:

  • 通过Class.forName()加载
  • 通过ClassLoader的loaderClass方法加载
  • 通过ClassLoader的findClass方法

3、Class.forName()加载类

Class.forName()和ClassLoader都可以对类进行加载。ClassLoader就是遵循双亲委派模型最终调用启动类加载器的类加载器,实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到二进制流后放到JVM中。Class.forName()方法实际上也是调用的CLassLoader来实现的。

先看看Class.forName()的源码:

/*** 参数解释:* 1、className:要加载的类名* 2、true:class被加载后是否要被初始化。初始化即执行static的代码(静态代码)* 3、caller:指定类加载器*/
public static Class<?> forName(String className) throws ClassNotFoundException {Class<?> caller = Reflection.getCallerClass();return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

可以看出来最后正在实现forName()方法的是forName0这一个native方法。而使用forName0需要传递的参数之一,就是ClassLoader类加载器。因此可以知道Class.forName()本质还是使用classloader来进行类的加载的。

4、使用ClassLoader加载类

  • ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。
  • loadClass()
    方法是加载目标类的入口,它首先会查找当前ClassLoader以及它的父类classloader里面是否已经加载了目标类,如果没有找到就会让父类Classloader尝试加载,如果父类classloader都加载不了,就会调用findClass()让自定义加载器自己来加载目标类。这实际上就是双亲委派机制的原理。

看一下loadClass()的源码:

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loaded//查看类是否已被加载,findLoadedClass最后调用native方法findLoadedClass0Class<?> c = findLoadedClass(name);//还未加载if (c == null) { long t0 = System.nanoTime();try {//若有父类加载器,则调用其loadClass(),请求父类进行加载if (parent != null) {c = parent.loadClass(name, false);} else {//若父类加载器为null,说明父类为BootstrapClassLoader(该类加载器无法被Java程序直接使用,用null代替即可),请求它来加载c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader//加载失败,抛出ClassNotFoundException异常}//父类无法加载,调用findClass方法,尝试自己加载这个类//注意:在这个findClass方法中,目前只是抛出一个异常,没有任何进行类加载的动作//因此,想要自己进行类加载,就要重写findClass()方法。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 statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

ClassLoader 的 findClass() 方法是需要子类来覆盖重写的,不同的加载器将使用不同的逻辑来获取目标类的字节码。得到字节码之后会调用 defineClass() 方法将字节码转换成 Class 对象。

findClass()的源码如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name); //仅抛出异常
}

  • 可见,ClassLoader的findClass()方法是没有具体实现的,如果要自定义类加载器,就需要重写findClass()方法,并且配合defineClass()
    方法一起使用,defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象。
  • 以上就是CLassLoader进行类加载的简单流程。

虽然Class.forName()方法本质上还是使用Classloader来进行类的加载的,但它和使用Classloader来进行类加载依然有着区别:

①Class.forName()方法除了将类的字节码加载到jvm中之外,还会执行类中的static块,即会导致类的初始化。Class.forName(name, initialize, loader)带参方法也可以指定是否进行初始化,执行静态块。

②ClassLoader只是将类的字节码加载到jvm中,不会执行static中的内容,即不会进行类加载,只有在newInstance才会去执行static块。

5、自定义类加载器

我们知道,除了BootstrapClassLoader是由C/C++实现的,其他的类加载器都是ClassLoader的子类。所以如果我们想实现自定义的类加载器,首先要继承ClassLoader。

根据我们前面的分析,ClassLoader进行类加载的核心实现就在loadClass()方法中。再根据loadClass()方法的源码,我们可以知道有两种方式来实现自定义的类加载,分别如下:

①如果不想打破双亲委派机制,那么只需要重写findClass方法。
②如果想要打破双亲委派机制,那么就需要重写整个loadClass方法。

如果没有特殊要求,Java官方推荐重写findClass方法,而不是重写整个loadClass方法。这样既让我们能够按照自己的意愿加载类,也能保证自定义的类加载器符合双亲委派机制。

明确了如何实现,我们只需要两步就可以实现自定义的类加载器:第一步是继承classloader,第二步是重写findClass方法。

不过由于在findClass()内需要调用defineClass()方法将字节数组转换成Class类对象,因此要先对输入的class文件做一些处理,使其变为字节数组。

实现自定义的类加载器:

public class MyClassLoader extends ClassLoader{//默认ApplicationClassLoader为父类加载器public MyClassLoader(){super();}//加载类的路径private String path = "";//重写findClass,调用defineClass,将代表类的字节码数组转换为Class对象@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] dataByte = new byte[0];try {dataByte = ClassDataByByte(name);} catch (IOException e) {e.printStackTrace();}return this.defineClass(name, dataByte, 0, dataByte.length);}//读取Class文件作为二进制流放入byte数组, findClass内部需要加载字节码文件的byte数组private byte[] ClassDataByByte(String name) throws IOException {InputStream is = null;byte[] data = null;ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();name = name.replace(".", "/"); // 为了定位class文件的位置,将包名的.替换为/is = new FileInputStream(new File(path + name + ".class"));int c = 0;while (-1 != (c = is.read())) { //读取class文件,并写入byte数组输出流arrayOutputStream.write(c);}data = arrayOutputStream.toByteArray(); //将输出流中的字节码转换为byte数组is.close();arrayOutputStream.close();return data;}
}

使用自定义的类加载器:

public static void main(String[] args) {MyClassLoader myClassLoader = new MyClassLoader();Class<?> clazz = myClassLoader.loadClass("com.fengjian.www.MyClassLoader");clazz.newInstance();
}

Java类加载器(类加载的流程、三大类加载器BootstrapClassLoader、ExtClassLoader、AppClassLoader)

一、三大类加载介绍
1.1、BootstrapClassLoader
BootstrapClassLoader是顶级加载器,默认加载的是%JAVA_HOME%中lib下的jar包和class类文件,他也是ExtClassLoader的父类,但是不是继承(extends)关系,是ExtClassLoder中有一个parent变量是BootstrapClassLoader

1.2、ExtClassLoader
ExtClassLoader扩展类加载器,负责加载%JAVA_HOME%中lib/ext文件下的jar包和class类文件,ExtClassLoader加载器是AppClassLoader的父类,当然也不是继承(extends)关系,也是类中有parent变量

1.3. AppClassLoader
AppClassLoader(应用程序加载器/系统类加载器)是自定义加载器的父类,负责加载classPath下的类文件,平时引用的jar包以及我们自己写的类都是这个加载器进行加载的,同时AppClassLoader还是线程上下文加载器,如果想实现一个自定义加载器的话就继承(extends)ClassLoader来实现.

注: 当然了也可以自定义类加载, 通过继承ClassLoader来实现.

二、类加载的流程
2.1、向上委派
AppClassLoader是加载我们自己编写的class类的,当他遇到一个新的class类的时候,不会直接进行加载,而是向上委派给ExtClassLoader,向上委派就是去查找ExtClassLoader是否缓存了这个class类,如果有则返回,如果没有则继续委派给BootstrapClassLoader,如果BootstrapClassLoader中缓存有则加载返回.

2.2、向下查找
开始进行向下查找了,就意味着当前class类向上委派到BootstrapClassLoader时还是没有该类的缓存,此时BootstrapClassLoader会查找加载自己路径也就是%JAVA_HOME%/lib下的jar与class类文件,如果有则加载返回,没有则继续向下查找。ExtClassLoader也是做同样的操作。查找加载ExtClassLoader对应路径的文件,如果有则加载返回,没有则继续向下到AppClassLoader查找加载,AppClassLoader是加载classPath也就是我们程序员自己编写的class类,如果AppClassLoader找不到则会抛出找不到class类异常

2.3、流程简介
向往委派是到顶层类加载器为止,向下查找是到发起的加载器为止,如果是有自定义类加载的情况,发起和截至会是这个自定义加载器。

2.4、作用
这样做的原因主要是为了安全,避免程序员编写类动态替换Java的核心类比如说String,同时也是避免了相同的class类被不同的ClassLoader重复加载

2.5、​​​​​​​类加载简图
在这里插入图片描述

类加载器如何打破双亲委派加载机制(SPI原理)

1.类加载器命名空间可见性
子类加载器可以见到父类加载器加载的类,而父类加载器看不见子类加载器加载的类

2.打破双亲委派加载机制
1.双亲委派模型的第一次“被破坏”是重写自定义加载器的loadClass(),jdk不推荐。一般都只是重写findClass(),这样可以保持双亲委派机制.而loadClass方法加载规则由自己定义,就可以随心所欲的加载类了
2.双亲委派模型的第二次“被破坏”是ServiceLoader和Thread.setContextClassLoader()
双亲委派模型的这个模型存在一些缺陷,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。
这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
以JDBC为例:

//传统加载方式 1
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");
//传统加载方式 2
System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");
//SPI加载方式
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");

SPI加载机制中,不需要手动设置驱动为com.mysql.jdbc.Driver。DriverManager有一个静态代码块,这会在执行getConnection前运行,即loadInitialDrivers(),此方法会调用ServiceLoader.load(Class service, ClassLoader loader)寻找ClassPath:META-INF/services文件夹下面java.sql.Driver的内容,即实现类。load(
由于ServiceLoader位于rt.jar包下面(并没有继承ClassLoader)是启动类加载器加载的,所以是无法看见厂商的实现类jdbc.Driver,由父类加载器加载的类是看不见子类加载器加载的类的。所以此时采用自己传入的loader结合Thread.setContextLoader(),将jdbc.Driver偷偷加载到内存,并通过以下详细基本原理,进而实现SPI机制

image.png
3.双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。

3.基本原理
当前项目中定义好接口,实现类不在当前类路径下。实现类实现当前项目提供的接口。在当前项目中调用自定义classLoder.load().根据双亲委托机制,会先尝试使用父类加载器加载,加载不成功则使用子类加载器。子类加载器加载当前Student1类,需要用到Student接口,而Student接口是使用父类加载器加载的(在类路径下面),由于父类加载器加载的类对于子类可见,则不会报错).拿到反射实例的class后调用反射(此处不能直接new ,直接new或者直接使用Student1都会造成主动使用,从而造成appClassLoder来加载这个类,由于AppclassLoder无法加载这个类,父类加载器无法访问子类加载器加载的类,此时就会报错)。根据预先定义好的接口Student,就可以使用这个具体实现类的某些方法了
image.png

/**@author: logan
@Date: 2019/10/30 13:15
@Description:
*/
public class Test01 {
public static void main(String[] args)throws ClassNotFoundException, IllegalAccessException, InstantiationException {
//这个类class的路径
String classPath = "C:\\Users\\Administrator\\Desktop\\test\\com\\student\\";MyClassLoader myClassLoader = new MyClassLoader(classPath, "my");
String packageNamePath = "Student1";Class<?> a = myClassLoader.loadClass(packageNamePath);System.out.println("类加载器是:" + a.getClassLoader());
Student student = (Student) a.newInstance();
student.doSth();
}
}

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

相关文章

类加载器(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…

DAU 和 MAU

DAU 和 MAU 日活跃用户占月活跃用户的比例越高&#xff0c;表明用户对App的使用粘性越高。 DAU&#xff0c;即&#xff1a;Daily Active User&#xff0c;指日活跃用户数 MAU&#xff0c;即&#xff1a;Monthly Active User&#xff0c;指月活跃用户数。 *例子1&#xff1a;…

如何通过DAU分析活跃用户?(案例:python绘制箱体图)

前言&#xff1a;本文内容以游戏产品为基础进行讲解&#xff0c;内容为以下4部分&#xff1a; 1. 如何理解DAU反映了哪些问题&#xff1f; 2. 有哪些因素会影响DAU变动&#xff1f; 3. 如何解读DAU的“箱体图”&#xff1f; 4. 如何使用python绘制“箱体图”&#xff1f; DAU的…

【数据分析】产品日活DAU下降,怎么分析

目录 案例简介 第一步&#xff1a;确认数据真实性 第二步&#xff1a;明确定义&#xff0c;并拆解指标&#xff0c;进一步定位原异常部分 第三步&#xff1a;根据几个常见维度初步拆分数据 第四步&#xff1a;进一步做假设并细分深入&#xff0c;得出结论 案例分析 例题…

数据分析——DAU下降问题(转)

文章转自&#xff1a; DAU异常下降该如何分析 1. 梳理公司的用户增长模式 尽管不同业务形态、以及不同发展阶段的公司&#xff0c;其用户增长模式各有差异&#xff0c;但都可以从拉新策略和促活策略进行分解。 常见的拉新策略有&#xff1a; 流量采购。比如通过厂商预装、…

有赞一面:亿级用户DAU日活统计,有几种方案?

说在前面 在40岁老架构师 尼恩的读者社区(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如极兔、有赞、希音、百度、网易、滴滴的面试资格&#xff0c;遇到一几个很重要的面试题&#xff1a; (1) 亿级用户场景&#xff0c;如何高性能统计日活&#xff1f; (2) 如何实现亿…

数据分析——用户粘性指标 DAU/MAU

&#xff08; 一 &#xff09;定义 DAU&#xff0c;即&#xff1a;Daily Active User&#xff0c;指日活跃用户数&#xff1b; MAU&#xff0c;即&#xff1a;Monthly Active User&#xff0c;指月活跃用户数。 日活/月活就是体现用户粘性最频繁使用的指标。日活跃用户占月活…

数据分析体系 - 用户粘性(DAU/MAU 和 月人均活跃天数)

对于常见的App&#xff0c;用户粘性的取值范围就是3%~100%&#xff0c; 不同领域的App也会有不同的基准值&#xff0c; 例如移动游戏会以20%为基线&#xff0c; 而工具类App会以40%为基线。 用户粘性的两个计算指标&#xff1a; 1、DAU/MAU 2、月平均活跃人数 这里其实…

数据分析——DAU下降/上升原因分析

本文是对“DAU变动原因”问题进行的思考整理&#xff0c;仅作记录&#xff0c;欢迎讨论。 &#xff08; 一 &#xff09;思维框架 图版&#xff1a; 文版&#xff1a; 内部原因&#xff1a; 1. 数据验证 如果DAU上升或者下降&#xff0c;且非日常波动&#xff0c;需先确…

数据分析 — 用户粘性的两个计算指标(DAU/MAU和月人均活跃天数)

很多运营都了解DAU&#xff08;日活跃用户数&#xff09;和MAU&#xff08;月活跃用户数&#xff09;的重要性&#xff0c;但在某些情况下这两个数值本身并不能反映出太多问题&#xff0c;这个时候就要引用到【DAU/MAU】的概念&#xff0c;即【日活/月活】。 用户粘性的两个计…

峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?

小游戏《羊了个羊》 短短的7天内&#xff0c;DAU突破了1亿、吞吐量峰值21WQps。 《羊了个羊》运营后台数据显示&#xff0c;在短短的7天内&#xff0c;这款小游戏的DAU就突破了1亿。 要知道&#xff0c;除了王者荣耀、原神等屈指可数的现象级手游之外&#xff0c;1亿DAU是这个…

dau、mau、pcu、dnu、wau、acu、uv分别是什么意思?

dau、mau、pcu、dnu、wau、acu、uv的意思是什么?怎么分析? DAU(Daily Active User)日活跃用户数量。常用于反映网站、互联网应用或网络游戏的运营情况。 MAU(monthly active users)月活跃用户人数。是在线游戏的一个用户数量统计名词&#xff0c;数量越大意味着玩这款游戏的人…

数据化运营04 DAU、MAU、UV:谁是最有参考价值的活跃指标?

活跃类指标是重要的用户质量指标&#xff0c;代表了产品上真正的用户&#xff0c;代表了具备营销价值的用户群&#xff0c;是几乎所有产品运营的重点。 在这一讲中&#xff0c;我会主要向你介绍 UV、DAU、MAU&#xff0c;以及每日使用时长和每日打开频次等指标。 你是否思考过…

如何理解、分析DNU/DAU?(案例:DNU、DAU面积图)

前言&#xff1a;本文内容以游戏产品为基础进行讲解&#xff0c;内容为以下4部分&#xff1a; 1. 如何理解DNU和DAU 2. 如何使用python绘制DNU和DAU的面积图 3. 如何分析DNU/DAU 4. 如何计算当日DAU有多少由往日N日DAU贡献的&#xff0c;并简单计算用户生命周期&#xff1f; DN…

DAU是啥,数据指标是啥?必知必会的数据分析常识

在刚迈入数据的大门时&#xff0c;我经常对一些数据指标或者数据本身的概念很模糊&#xff0c;尤其是当跟运营、数据分析师扯需求的时候&#xff0c;会被这些密密麻麻的指标给弄糊涂。为了更好的在行业里面摸打滚爬&#xff0c;花了很多时间阅读一些指标相关的文章、书籍&#…

dau、mau、pcu、dnu、wau、acu、uv的意思是什么?怎么分析?

dau、mau、pcu、dnu、wau、acu、uv的意思是什么?怎么分析? DAU(Daily Active User)日活跃用户数量。常用于反映网站、互联网应用或网络游戏的运营情况。 MAU(monthly active users)月活跃用户人数。是在线游戏的一个用户数量统计名词&#xff0c;数量越大意味着玩这款游戏的人…