深入理解Java ClassLoader原理

是什么ClassLoader

每一个类类型,对应有自己的Class类,通过someObject.getClass()或者SomeClass.class的方式可以获取,而运行时这个Class类由JVM或者所处容器,甚至自定义的ClassLoader加载,这是一个必须的过程。由于所有类型的ClassLoader都是Java.lang.ClassLoader的实例,所以可以通过继承该类来实现自己的ClassLoader,改变类加载行为。再通俗点讲,ClassLoader就是加载.class文件的,.java文件通过编译器编译成字节码即.class文件,由ClassLoader加载得到可用于生成该类实例的Class类对象,最后,由该Class对象生成实例,如下图:

这里写图片描述

JVM类加载器

这里写图片描述

  • BootStrapClassLoader
    启动类加载器用于加载java的核心类,例如:rt.jar中的类。它是其他类加载器的parent,也是唯一 一个没有parent的类加载器,处于ClassLoader的继承体系中的最高层级,由于它本身由C++编写,不是Java类,所以不需要其他类来加载它。
  • ExtClassLoader
    扩展类加载器是BootStrapClassLoader的子加载器,用于加载环境变量java.ext.dir 路径下的jar包中的类。
  • AppClassLoader
    应用类加载器是BootStrapClassLoader的子加载器,用于加载classpath下的类。
  • CustomClassLoader
    当JVM类加载器不满足用户需求时,可以自定义类加载器,改写加载行为。

JVM类加载顺序:

  • 用户自己的类加载器,把加载请求传给父加载器,父加载器再传给其父加载器,一直到加载器树的顶层。
  • 最顶层的类加载器首先针对其特定的位置加载,如果加载不到就转交给子类。
  • 如果一直到底层的类加载都没有加载到,那么就会抛出异常ClassNotFoundException。

Tomcat类加载器

这里写图片描述

  • BootStrapClassLoader
    在Tomcat中,BootstrapClassLoader同时兼顾加载Java核心jar包如rt.jar和扩展类路径java.lang.ext下的类。
  • SystemClassLoader
    系统类加载器,加载catalina.bat(win)/catalina.sh(linux)下指定的类
  • CommonClassLoader
    加载Tomcat及应用的通用类,位于CATALINA/lib下。
  • WebAppClassLoader
    用于加载WEB-INF/classes和WEB-INF/lib路径下的类,加载顺序前者优先。

Tomcat类加载顺序

  • 使用bootstrap引导类加载器加载
  • 使用system系统类加载器加载
  • 使用应用类加载器在WEB-INF/classes中加载
  • 使用应用类加载器在WEB-INF/lib中加载
  • 使用common类加载器在CATALINA_HOME/lib中加载

全盘负责 + 委托机制

ClassLoader之间,不是真正的继承关系,而是组合关系。classloader加载类时,使用全盘负责委托机制,可以分开两部分理解:全盘负责,委托。

  • 全盘负责机制:若类A调用了类B,则类B和类B所引入的所有jar包,都由类A的类加载器统一加载。
public class A {
     public void doSomething() {
          B b = new B();
          b.doSomethingElse();
     }
}
B b = new B(); 
<===等同于===> 
B b = Class.forName(“B”, false,A.class.getClassLoader()).newInstance();
  • 委托机制:类加载器在加载类A时,会优先让父加载器加载,如果找到,返回,如果父加载器加载不到,再找父加载器的父加载器(如果存在),一直找到bootstrap classloader都找不到,才自己去相关的路径去寻找加载。

机制的好处:这样做的好处有两点。
第一:重复加载问题。比如两个类A和类B都要加载某一个类C,如果不采用委派机制,当加载类A时,会加载一份C的字节码,加载类B时,C会被再加载一次,内存中将会出现两份一样的C字节码。然而在委派机制下,由于父类优先,C将会被父类(假如是Bootstrap)加载一次,当下次再加载时发现C的字节码是已有的,将会直接返回,不会重复加载。

第二:不会出现用户自定义类阻碍核心类加载的情况。比如用户自定义一个java.lang.String,结合前面的加载顺序图可以看到,由于父加载器优先加载,最先加载到内存的是java自身的String类,而非用户自定义的类。用户自定义的String类根本没有机会加载。
在特殊场景下,如果希望加载自定义类而非Java自身的类,可以自定义ClassLoader,改变加载行为。
这一点JVM和Tomcat有所区别,作为容器,tomcat根据自己的需求,重新实现了加载过程。JVM和Tomcat加载顺序的区别见上图。

源码分析

public abstract class ClassLoader {
     public Class loadClass(String name);
     protected Class defineClass(byte[] b);
     public URL getResource(String name);
     public Enumeration getResources(String name);
     public ClassLoader getParent();
}
  • loadClass方法,它接受一个全类名,然后返回一个Class类型的实例。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //首先检查是否已近加载过, 已加载则直接返回。
            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) {
                    // 抛出异常
                }
                // 如果父加载器没有加载成功,才交由子加载器加载
                if (c == null) {
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  • defineClass方法接受一组字节,然后将其具体化为一个Class类型实例,它一般从磁盘上加载一个文件,然后将文件的字节传递给JVM,通过JVM(native 方法)对于Class的定义,将其具体化,实例化为一个Class类型实例。
  • getParent方法返回其parent ClassLoader。
  • getResource和getResources方法,从给定的repository中查找URLs,同时它们也具备类似loadClass一样的代理机制,我们可以将loadClass视为:defineClass(getResource(name).getBytes())。

通常,我们可以通过继承java.lang.ClassLoader并实现findClass或loadClass逻辑来改变类加载行为。

如何唯一确定一个实例

在JVM中,仅靠全类名无法唯一确定一个实例,还需要类加载器。在JVM中,类型被定义在一个叫 SystemDictionary 的数据结构中,该数据结构接受类加载器和全类名作为参数,返回类型实例。如下图所示:
这里写图片描述

其中,A同时被两个不同的类加载器L1和L2加载,虽然是相同的类,但不同类加载器加载得到的结果去完全不同。A和B是两个不用的类,由同一个类加载器加载,得到的自然也是不同的类类型。

一些关于ClassLoader的错误和排查方法

未完待续…

举例

未完待续…

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值