深入OpenJDK源码全面理解Java类加载器(下 — Java源码篇)

深入OpenJDK源码全面理解Java类加载器(下 — Java源码篇)目录前言一 双亲委派 1 1 类加载器结构 1 2 双亲委派二 使用步骤 1 引入库 2 读入数据总结前言 在深入 openjdk 源码全面理解 Java 类加载器 上 JVM 源码篇 我们分析了 JVM 是如何启动 并且初始化 BootStrapCla 的 也提到了 sun misc Launcher 被加载后会创建 ExtClassLoad 和 AppClassLoad 这篇文章主要从 Java 源码层面总结一下双亲委派 TCCL 的应用等 然后在聊聊自定义类加载器的注意事项 一 双亲委派 1 1 类加载器

前言

  在深入openjdk源码全面理解Java类加载器(上 – JVM源码篇)我们分析了JVM是如何启动,并且初始化BootStrapClassLoader的,也提到了sun.misc.Launcher被加载后会创建ExtClassLoader和AppClassLoader。关于类加载的基础知识请参考虚拟机类加载机制(上)。这篇文章主要从Java源码层面总结一下双亲委派、TCCL的应用等,然后再聊聊自定义类加载器的注意事项。

一、双亲委派

1.1 类加载器结构

1.2 双亲委派

  加载类的核心方法是loadClass,默认实现在ClassLoader中:

 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 
    synchronized (getClassLoadingLock(name)) { 
    //同步锁,可能是一个和name对应的Object,也可能是this //取决于类加载器是否具备并行能力 //首先检查类是否被本类加载器加载了 Class<?> c = findLoadedClass(name); if (c == null) { 
    //如果没有找到需要加载的类 long t0 = System.nanoTime(); try { 
    //使用父类加载器加载类 //如果parent不为null,说明设置了父加载器,直接用parent if (parent != null) { 
    c = parent.loadClass(name, false); } else { 
    //如果parent为null,使用BootStrapClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { 
    // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { 
    //如果父类加载器没能加载到类,使用本类加载器加载 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; } } 

注:关于getClassLoadingLock,可参考:关于类加载的并发控制锁。

二、自定义类加载器

  自定义类加载器需要直接或间接继承ClassLoader,最简单的一个自定义类加载器就是继承ClassLoader,重写其findClass方法,通过ClassLoader.defineClass方法创建一个Class类(defineClass最终会调用ClassLoader的native方法):

public class MyClassLoader extends ClassLoader { 
    private URLClassPath ucp; public MyClassLoader(String path, ClassLoader parent) throws Exception { 
    super(parent); this.ucp = new URLClassPath(new URL[]{ 
   new File(path).toURI().toURL()}); } static { 
    ClassLoader.registerAsParallelCapable(); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { 
    String usePath = name.replace('.', File.separatorChar).concat(".class"); Resource resource = ucp.getResource(usePath, false); if (resource != null) { 
    try { 
    byte[] bytes = resource.getBytes(); return defineClass(name, bytes, 0, bytes.length); } catch (IOException var) { 
    return null; } } else { 
    return null; } } } 
public class MyClassLoader2 extends ClassLoader { 
    private URLClassPath ucp; public MyClassLoader2(String path, ClassLoader parent) throws MalformedURLException { 
    super(parent); this.ucp = new URLClassPath(new URL[]{ 
   new File(path).toURI().toURL()}); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 
    if (name.startsWith("com.demo")) { 
    Resource resource = ucp.getResource(name.replace('.', File.separatorChar).concat(".class"), false); if (resource != null) { 
    try { 
    byte[] bytes = resource.getBytes(); Class clazz = defineClass(name, bytes, 0, bytes.length); if (resolve) { 
    resolveClass(clazz); } return clazz; } catch (IOException e) { 
    throw new ClassNotFoundException(e.getMessage()); } } else { 
    throw new ClassNotFoundException(); } } else { 
    return super.loadClass(name, resolve); } } } 

  这个类加载器对于com.demo包的类都由自己加载,其余的才委托给父类。测试一下:

public class JavaMain { 
    public static void main(String[] args) throws Exception { 
    String classPath = JavaMain.class.getClassLoader().getResource("").getPath(); MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader()); Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false); System.out.println(clazz.getClassLoader()); } } //输出 com.demo.classloader.MyClassLoader2@5cad8086 

2.1 全盘委派

  在我们的这个工程中,有一个问题,如果运行以下代码:

public static void main(String[] args) throws Exception { 
    String classPath = JavaMain.class.getClassLoader().getResource("").getPath(); MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader()); Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false); System.out.println(clazz.getClassLoader()); System.out.println(TestClass1.class.getClassLoader()); TestClass1 testClass1 = (TestClass1) clazz.newInstance(); } //输出: com.demo.classloader.MyClassLoader2@5cad8086 sun.misc.Launcher$AppClassLoader@18b4aac2 ClassCastException 

  类型强转操作会抛出java.lang.ClassCastException异常。造成这个的原因已经在输出结果中体现了,clazz是由自定义类加载加载的,而TestClass1.class是由AppClassLoader加载的。可以打印看看AppClassLoaer的加载目录:

System.out.println(System.getProperty("java.class.path")); 

  在mac下结果如下:

/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/tools.jar:/Users/loren/work/github/test/out/production/test:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar 

  输出的目录包含了当前项目目录,所以目录中的class可以被AppClassLoader加载。

注:除了当前项目目录,还有很多系统jar包,包括rt.jar、jce.jar等,当然由于AppClassLoader遵循双亲委派,路径包含这些jar包也不会有什么问题。

  那么TestClass1是什么时候被AppClassLoader加载的呢?对于上述代码中的:

...... 1.Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false); 2.System.out.println(clazz.getClassLoader()); 3.System.out.println(TestClass1.class.getClassLoader()); ...... 
public class TestClass2 { 
    public TestClass2() { 
    System.out.println("testClass2.classLoader:" + this.getClass().getClassLoader()); } } 

  然后在TestClass1中创建一个方法触发TestClass2的实例化:

public class TestClass1 { 
    public void run() { 
    new TestClass2(); } } 

  由于main方法中使用TestClass1会被AppClassLoader加载,所以我们不能强转类型,只能通过反射调用该方法:

public class JavaMain { 
    public static void main(String[] args) throws Exception { 
    String classPath = JavaMain.class.getClassLoader().getResource("").getPath(); MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader()); Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false); Object obj = clazz.newInstance(); Method method = obj.getClass().getDeclaredMethod("run", null); method.setAccessible(true); method.invoke(obj, null); } } 

  输出如下:

testClass2.classLoader:com.demo.classloader.MyClassLoader2@5cad8086 

2.2 覆盖核心类?

package java.util; public class HashMap { 
    } 

  然后创建一个自定义类加载器,这个和之前类似:

public class MyClassLoader3 extends ClassLoader { 
    private URLClassPath ucp; public MyClassLoader3(String path, ClassLoader parent) throws MalformedURLException { 
    super(parent); this.ucp = new URLClassPath(new URL[]{ 
   new File(path).toURI().toURL()}); } static { 
    ClassLoader.registerAsParallelCapable(); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 
    Resource resource = ucp.getResource(name.replace('.', File.separatorChar).concat(".class"), false); if (resource != null) { 
    try { 
    byte[] bytes = resource.getBytes(); Class clazz = defineClass(name, bytes, 0, bytes.length); if (resolve) { 
    resolveClass(clazz); } return clazz; } catch (IOException e) { 
    throw new ClassNotFoundException(e.getMessage()); } } else { 
    throw new ClassNotFoundException(); } } } 

  在main方法中创建自定义类加载器,加载路径为当前项目路径,然后尝试加载java.util.HashMap:

public static void main(String[] args) throws Exception { 
    String classPath = JavaMain.class.getClassLoader().getResource("").getPath(); MyClassLoader3 myClassLoader = new MyClassLoader3(classPath, JavaMain.class.getClassLoader()); Class clazz = myClassLoader.loadClass("java.util.HashMap", false); } 

  当然不出意外的是,有异常堆栈抛出:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.util at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655) at java.lang.ClassLoader.defineClass(ClassLoader.java:754) at java.lang.ClassLoader.defineClass(ClassLoader.java:635) at com.demo.classloader.MyClassLoader3.loadClass(MyClassLoader3.java:29) at com.demo.classloader.JavaMain.main(JavaMain.java:13) 

  提示禁止加载包:java.util,看堆栈信息异常是ClassLoader.preDefineClass抛出来的,看看相应的代码:

private ProtectionDomain preDefineClass(String name, ProtectionDomain pd){ 
    ...... if ((name != null) && name.startsWith("java.")) { 
    throw new SecurityException ("Prohibited package name: " + name.substring(0, name.lastIndexOf('.'))); } ...... } 
protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain) throws ClassFormatError { 
    protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); Class<?> c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; } 

  该方法是一个final方法,我们无法重写,那能在自定义类加载器中调用defineClass1方法吗?defineClass1方法定义在ClassLoader中,是一个private native方法:

private native Class<?> defineClass0(String name, byte[] b, int off, int len,ProtectionDomain pd); private native Class<?> defineClass1(String name, byte[] b, int off, int len,ProtectionDomain pd, String source); private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,int off, int len, ProtectionDomain pd,String source); 

  所以我们只能通过父类的defineClass创建Class,也就没法绕过preDefineClass方法的检查。既然如此,那能不能从本地方法入手呢?理论上是可行的,但是需要修改动态链接文件。但是都能操作dll了,还需要费尽心思去覆盖核心类库吗?

三、TCCL

 /* The context ClassLoader for this thread */ private ClassLoader contextClassLoader; 

  通过相应的set方法:

Thread.currentThread().setContextClassLoader(classloader); 

  将一个类加载器和线程绑定。这样在一个线程中,需要加载当前类加载器无法加载的类的时候,可以从当前线程中获取TCCL进行加载:

public static <S> ServiceLoader<S> load(Class<S> service) { 
    ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } 

  TCCL默认为AppClassLoader,初次在sun.misc.Launcher的构造方法中设置:

public Launcher() { 
    ...... try { 
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { 
    throw new InternalError("Could not create application class loader", var9); } //TCCL默认为AppClassLoader Thread.currentThread().setContextClassLoader(this.loader); ...... } 

四、spring的类加载

ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { 
    //spring在webapp下,类加载器相同 currentContext = this.context; } else if (ccl != null) { 
    //加载spring的类加载器和TCCL不同,将classLoader和WebApplicationContext用map //保存起来,用的时候根据classLoader获取context currentContextPerThread.put(ccl, this.context); } 

  当然,如果在SpringBoot中使用内嵌servlet容器的时候,就不会出现一个servlet容器包含多个应用的情况了,也就不用再用map维护不同的context了,直接使用TCCL即可:

 ClassLoader cl = null; try { 
    cl = Thread.currentThread().getContextClassLoader(); } catch (Throwable ex) { 
    // Cannot access thread context ClassLoader - falling back... } if (cl == null) { 
    // No thread context class loader -> use class loader of this class. cl = ClassUtils.class.getClassLoader(); if (cl == null) { 
    // getClassLoader() returning null indicates the bootstrap ClassLoader try { 
    cl = ClassLoader.getSystemClassLoader(); } catch (Throwable ex) { 
    // Cannot access system ClassLoader - oh well, maybe the caller can live with null... } } } return cl; 
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/222598.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月17日 下午3:24
下一篇 2026年3月17日 下午3:25


相关推荐

  • EL表达式语法「建议收藏」

    EL表达式语法「建议收藏」EL(是ExpressionLanguage的缩写),使用EL对JSP输出进行优化,可以使得页面结构更加清晰,代码可读性高,也更加便于维护。使用EL表达式的目的:从作用域中获取指定属性名的共享数据<%@pageisELIgnored=”true”%>表示是否禁用EL语言,TRUE表示禁止.。FALSE表示不禁。1、EL表达式的语…

    2022年7月28日
    9
  • delphi 单引号用quotedstr()就是爽

    delphi 单引号用quotedstr()就是爽sp_qry.Close;  sp_qry.SQL.Clear;  sp_qry.SQL.Add(‘select*fromitem_infowhereitem_clsno=’+quotedstr(sp_lb.KeyValue));  sp_qry.Open; 用quotedstr()函数不用去算””个数

    2022年10月9日
    4
  • 操作系统实验一:进程管理(含成功运行C语言源代码)[通俗易懂]

    操作系统实验一:进程管理(含成功运行C语言源代码)[通俗易懂]目录操作系统实验一:进程管理实验目的实验内容操作系统实验一:进程管理1.实验目的1.理解进程的概念,明确进程和程序的区别2.理解并发执行的实质3.掌握进程的创建、睡眠、撤销等进程控制方法2.实验内容用C语言编写程序,模拟实现创建新的进程;查看运行进程;换出某个进程;杀死运行进程等功能。3.实验准备以下将分别介绍①进程的概念,以及进程的各类状态(就绪状态、执行状态、阻塞状态);②进程控制块PCB的作用及内容信息③进程的创建与撤销(????重.

    2022年10月20日
    2
  • python 导入数据错误:UnicodeDecodeError: ‘utf-8’ codec can’t decode byte 0xb5 in position 0: invalid start

    python 导入数据错误:UnicodeDecodeError: ‘utf-8’ codec can’t decode byte 0xb5 in position 0: invalid start正想导入数据到python作分析找到这个教程https://www.cnblogs.com/OliverQin/p/8966321.html我要导入CSV文件,已经放在相同目录之下。importpandasaspddata=pd.read_csv("电信客户流失.csv",encoding="utf8")报错如下———————–…

    2022年7月16日
    21
  • 爬虫福利一 之 27报网MM

    爬虫福利一 之 27报网MM《爬虫福利二》点击:https://blog.csdn.net/PY0312刚学爬虫花了4个小时写的,每一步备注的都很清楚,喜欢的朋友自己可以研究研究……目标网站:https://www.27bao.com环境:Python3.x相关第三方模块:requests、lxmlRe:各位在测试时只需要打开终端,使用pythonxxx.py运行即可。源码如下…

    2022年6月22日
    81
  • #Java算法设计与分析1–递归算法

    #Java算法设计与分析1–递归算法1.递归算法1.1递归的概念所谓递归,就是程序方法在运行过程中自身调用自身。定义如下所示。fn(){ if(递归出口条件){ returnx;}else{ //somecodes…returnfn();}}1.2递归的使用条件1.2.1必须要有明确的递归出口所谓递归出口就是需要有明确的结束条件。1.2.2每次递归都要使问题的规模减小1.2.3递归的规模…

    2022年7月8日
    24

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号