Java的类加载器与双亲委托机制

2020年03月21日 06:09:02   [来源:互联网]   [阅读:1]
字体:【

目录:

  • 类加载器
  • java.lang.ClassLoader类 URLClassLoader与SecureClassLoader ClassLoader常见方法源码分析
  • 双亲委托机制 图解 源码角度分析
  • 常见的问题分析

前言:我们刚刚接触Java时,在IDE(集成开发环境) 或者文本编辑器中所写的都是.java文件,在编译后会生成.class文件,又称字节码文件。

javac HelloWorld.java ---> HelloWorld.class 复制代码

对于.class文件来说,需要被加载到虚拟机中才能使用,这个加载的过程就成为类加载。如果想要知道类加载的方式,就需要知道类加载器双亲委托机制的概念。也就是我们本篇所要介绍的内容。

1. 类加载器

Java中的类加载器可以分为两种:

  • 系统类加载器
  • 自定义类加载器

而系统类加载器又有3个:

  • Bootstrap ClassLoader:启动类加载器
  • Extensions ClassLoader:扩展类加载器
  • App ClassLoader:也称为SystemAppClass,系统类加载器

1.1 Bootstrap ClassLoader

Bootstrap ClassLoader用来加载JVM(Java虚拟机)运行时所需要的系统类,其使用c++实现。

从以下路径来加载类:

  1. %JAVA_HOME%/jre/lib目录,如rt.jar、resources.jar、charsets.jar等
  2. 可以在JVM启动时,指定-Xbootclasspath参数,来改变Bootstrap ClassLoader的加载目录。

Java虚拟机的启动就是通过 Bootstrap ClassLoader创建一个初始类来完成的。 可以通过如下代码来得出Bootstrap ClassLoader所加载的目录:

public class ClassLoaderTest {    public static void main(String[]args) {        System.out.println(System.getProperty("sun.boot.class.path"));    }}复制代码

打印结果为:

C:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_102\jre\classes复制代码

可以发现几乎都是$JAVA_HOME/jre/lib目录中的jar包,包括rt.jar、resources.jar和charsets.jar等等。

1.2 Extensions ClassLoader

Extensions ClassLoader(扩展类加载器)具体是由ExtClassLoader类实现的,ExtClassLoader类位于sun.misc.Launcher类中,是其的一个静态内部类。对于Launcher类,可以先看成是Java虚拟机的一个入口。

ExtClassLoader的部分代码如下:


Extensions ClassLoader负责将JAVA_HOME/jre/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。

通过以下代码可以得到Extensions ClassLoader加载目录:

System.out.println(System.getProperty("java.ext.dirs"));复制代码

打印结果为:

C:\Program Files\Java\jdk1.8.0_102\jre\lib\ext;C:\Windows\Sun\Java\lib\ext复制代码

1.3 App ClassLoader

也称为SystemAppClass(系统类加载器),具体是由AppClassLoader类实现的,AppClassLoader类也位于sun.misc.Launcher类中。

部分代码如下:


  1. 主要加载Classpath目录下的的所有jar和Class文件,是程序中的默认类加载器。这里的Classpath是指我们Java工程的bin目录。
  2. 也可以加载通过-Djava.class.path选项所指定的目录下的jar和Class文件。

通过以下代码可以得到App ClassLoader加载目录:

System.out.println(System.getProperty("java.class.path"));复制代码

打印结果为:

C:\workspace\Demo\bin复制代码

这个路径其实就是当前Java工程目录bin,里面存放的是编译生成的class文件。


在Java中,除了上述的3种系统提供的类加载器,还可以自定义一个类加载器。

1.4. 自定义类加载器

为了可以从指定的目录下加载jar包或者class文件,我们可以用继承java.lang.ClassLoader类的方式来实现一个自己的类加载器。

在自定义类加载器时,我们一般复写findClass方法,并在findClass方法中调用defineClass方法。

接下来会先介绍下ClassLoader类相关的具体内容,之后看一个自定义类加载器demo。

2 java.lang.ClassLoader类

2.1 ClassLoader、URLClassLoader与SecureClassLoader的关系

从上面关于ExtClassLoader、AppClassLoader源码图中我们可以看到,他们都继承自URLClassLoader,那这个URLClassLoader是什么,其背后又有什么呢?

先来一张很重要的继承关系图:


  • ClassLoader是一个抽象类,位于java.lang包下,其中定义了ClassLoader的主要功能。
  • SecureClassLoader继承了抽象类ClassLoader,但SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。
  • ExtClassLoader和AppClassLoader都继承自URLClassLoader,它们都是Launcher 的内部类,Launcher 是Java虚拟机的入口应用,ExtClassLoader和AppClassLoader都是在Launcher中进行初始化的。

2.2 普通的类、AppClassLoader与ExtClassLoader之间的关系

关系:

  • 加载普通的类(这里指得是我们所编写的代码类,下文demo中的Test类)加载器是AppClassLoader,AppClassLoader的父加载器为ExtClassLoader
  • 而ExtClassLoader的父加载器是Bottstrap ClassLoader

还有2个结论:

  • 每个类都有类加载器
  • 每个类加载器都有父加载器

我们准备一个简单的demo 自建的一个Test.java文件。

public class Test{}复制代码
public class Main {    public static void main(String[] args) {ClassLoader cl = Test.class.getClassLoader();System.out.println("ClassLoader is:"+cl.toString());}}复制代码

这样就可以获取到Test.class文件的类加载器,然后打印出来。结果是:

sun.misc.Launcher$AppClassLoader@75b83e92复制代码

也就是说明Test.class文件是由AppClassLoader加载的。

那AppClassLoader是谁加载的呢? 其实AppClassLoader也有一个父加载器,我们可以通过以下代码获取

public class Test {    public static void main(String[] args) {        ClassLoader loader = Test.class.getClassLoader();        while (loader != null) {            System.out.println(loader);            loader = loader.getParent();        }    }}复制代码

上述代码结果如下:

sun.misc.Launcher$AppClassLoader@7565783bsun.misc.Launcher$ExtClassLoader@1b586d23复制代码
  • 加载Test的类加载器是AppClassLoader,AppClassLoader的父加载器为ExtClassLoader
  • 而ExtClassLoader的父加载器是Bottstrap ClassLoader

至于为何没有打印出ExtClassLoader的父加载器Bootstrap ClassLoader,这是因为Bootstrap ClassLoader是由C++编写的,并不是一个Java类,因此我们无法在Java代码中获取它的引用。

2.3 java.lang.ClassLoader类常见的方法

上一节我们看到了ClassLoader的getParent方法,getParent获取到的其实就是其父加载器。这一节将通过源码,来介绍ClassLoader中的一些重要方法。

getParent()
ClassLoader类---------public final ClassLoader getParent() {    if (parent == null) return null;    SecurityManager sm = System.getSecurityManager();    if (sm != null) {        checkClassLoaderPermission(parent, Reflection.getCallerClass());    }    return parent;}复制代码

我们可以看到,其返回值有两种可能,为空或者是parent变量。

从源码中还可以发现其是一个final修饰的方法,我们知道被final修饰的说明这个方法提供的功能已经满足当前要求,是不可以重写的, 所以其各个子类所调用的getParent()方法最终都会由ClassLoader来处理。

parent变量又是什么呢?我们在查看源码时可以发现parent的赋值是在构造方法中。

ClassLoader类---------private ClassLoader(Void unused, ClassLoader parent) {    this.parent = parent;    ... //省略了无关代码}复制代码

而此构造方法又是私有的,不能被外部调用,所以其调用者还是在内部。于是接着查找到了另外两个构造方法。

ClassLoader类---------protected ClassLoader() {    this(checkCreateClassLoader(), getSystemClassLoader());}    protected ClassLoader(ClassLoader parent) {    this(checkCreateClassLoader(), parent);}复制代码

所以:

  1. 可以在调用ClassLoder的构造方法时,指定一个parent。
  2. 若没有指定的话,会使用getSystemClassLoader()方法的返回值。

接着看上面代码中的getSystemClassLoader的源码:

ClassLoader类---------public static ClassLoader getSystemClassLoader() {    initSystemClassLoader();    if (scl == null) {        return null;    }    SecurityManager sm = System.getSecurityManager();    if (sm != null) {        checkClassLoaderPermission(scl, Reflection.getCallerClass());    }    return scl;}复制代码

其返回的是一个scl。在initSystemClassLoader()方法中发现了对scl变量的赋值。

ClassLoader类---------private static synchronized void initSystemClassLoader() {    if (!sclSet) {        if (scl != null)            throw new IllegalStateException("recursive invocation");        sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); //1        if (l != null) {            Throwable oops = null;            scl = l.getClassLoader();            ...//省略代码        }        sclSet = true;    }}复制代码

重点来了,注释1处其获取到的是Launcher类的对象,然后调用了Launcher类的getClassLoader()方法。

Launcher类---------public ClassLoader getClassLoader() {    return this.loader;}复制代码

那这个this.loader是什么呢?在Launcher类中发现,其赋值操作在Launcher的构造方法中,其值正是Launcher类中的AppClassLoader

Launcher类---------public Launcher() {    Launcher.ExtClassLoader var1;    try {        var1 = Launcher.ExtClassLoader.getExtClassLoader();    } catch (IOException var10) {        throw new InternalError("Could not create extension class loader", var10);    }    try {        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);    } catch (IOException var9) {        throw new InternalError("Could not create application class loader", var9);    }    ...}复制代码

到这里谜团全部解开了:

在创建ClassLoder时,

  1. 可以指定一个ClassLoder作为其parent,也就是其父加载器。
  2. 若没有指定的话,会使用getSystemClassLoader()方法的返回值(也就是Launcher类中的AppClassLoader)作为其parent。
  3. 通过getParent()方法可以获取到这个父加载器。
defineClass()

能将class二进制内容转换成Class对象,如果不符合要求的会抛出异常,例如ClassFormatError、NoClassDefFoundError。

在自定义ClassLoader时,我们通常会先将特定的文件读取成byte[]对象,再使用此方法,将其转为class对象。

ClassLoader类---------/*** String name:表示预期的二进制文件名称,不知道的话,可以填null。* byte[] b:此class文件的二进制数据* int off:class二进制数据开始的位置* int len:class二进制数据的总长度*/protected final Class<?> defineClass(String name, byte[] b, int off, int len)    throws ClassFormatError{    return defineClass(name, b, off, len, null);}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;}复制代码
findClass()

findClass()方法一般被loadClass()方法调用去加载指定名称类。

ClassLoader类---------/*** String name:class文件的名称*/protected Class<?> findClass(String name) throws ClassNotFoundException {    throw new ClassNotFoundException(name);} 复制代码

通过源码看到ClassLoader类中并没有具体的逻辑,而是等待着其子类去实现,通过上面的分析我们知道两个系统类加载器ExtClassLoader和AppClassLoader都继承自URLClassLoader,那就来看一下URLClassLoader中的具体代码。

URLClassLoader类---------protected Class<?> findClass(final String name) throws ClassNotFoundException{    final Class<?> result;    try {        result = AccessController.doPrivileged(            new PrivilegedExceptionAction<Class<?>>() {                public Class<?> run() throws ClassNotFoundException {                    String path = name.replace('.', '/').concat(".class");                    Resource res = ucp.getResource(path, false);                    if (res != null) {                        try {                            return defineClass(name, res);                        } catch (IOException e) {                            throw new ClassNotFoundException(name, e);                        }                        ...    return result;}private Class<?> defineClass(String name, Resource res) throws IOException {    ...    URL url = res.getCodeSourceURL();    ...    java.nio.ByteBuffer bb = res.getByteBuffer();    if (bb != null) {        ...        return defineClass(name, bb, cs);    } else {        byte[] b = res.getBytes();        ...        return defineClass(name, b, 0, b.length, cs);    }}复制代码

可以看到其对传入的name进行处理后,就调用了defineClass(name, res);在这个方法里主要是通过res资源和url,加载出相应格式的文件,最终还是通过ClassLoader的defineClass方法加载出具体的类。

loadClass()

上节说到findClass()一般是在loadClass()中调用,那loadClass()是什么呢? 其实loadClass()就是双亲委托机制的具体实现,所以在我们先介绍下双亲委托机制后,再来分析loadClass()。

3 双亲委托机制介绍

3.1 图解双亲委托机制

先简单介绍下双亲委托机制: 类加载器查找Class(也就是在loadClass时)所采用的是双亲委托模式,所谓双亲委托模式就是

  1. 首先判断该Class是否已经加载
  2. 如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的Bootstrap ClassLoader
  3. 如果Bootstrap ClassLoader找到了该Class,就会直接返回
  4. 如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找


(图片来自http://liuwangshu.cn/application/classloader/1-java-classloader-.html)


  • 其中红色的箭头代表向上委托的方向,如果当前的类加载器没有从缓存中找到这个class对象,就会请求父加载器进行操作。直到Bootstrap ClassLoader。
  • 而黑色的箭头代表的是查找方向,若Bootstrap ClassLoader可以从%JAVA_HOME%/jre/lib目录或者-Xbootclasspath指定目录查找到,就直接返回该对象,否则就让ExtClassLoader去查找。
  • ExtClassLoader就会从JAVA_HOME/jre/lib/ext或者-Djava.ext.dir指定位置中查找,找不到时就交给AppClassLoader,AppClassLoader就从当前工程的bin目录下查找
  • 若还是找不到的话,就由我们自定义的CustomClassLoader查找,具体查找的结果,就要看我们怎么实现自定义ClassLoader的findClass方法了。

3.2 源码分析双亲委托机制

接下来我们看看双亲委托机制在源码中是如何体现的。 先看loadClass的源码:

ClassLoader类---------protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{    synchronized (getClassLoadingLock(name)) {        //首先,根据name检查类是否已经加载,若已加载,会直接返回        Class<?> c = findLoadedClass(name);        if (c == null) {            long t0 = System.nanoTime();            try {                if (parent != null) {                    //若当前类加载器有父加载器,则调用其父加载器的loadClass()                    c = parent.loadClass(name, false);                } else {                    //若当前类加载器的parent为空,则调用findBootstrapClassOrNull()                    c = findBootstrapClassOrNull(name);                }            } catch (ClassNotFoundException e) {            }                    if (c == null) {                // 1.如果到这里c依然为空的话,表示一直到最顶层的父加载器也没有找到已加载的c,那就会调用findClass进行查找                // 2.在findClass的过程中,如果指定目录下没有,就会抛出异常ClassNotFoundException                // 3.抛出异常后,此层调用结束,接着其子加载器继续进行findClass操作                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;    }}复制代码

findBootstrapClassOrNull()方法:可以看到其对name进行校验后,最终调用了一个native方法findBootstrapClass()。在findBootstrapClass()方法中最终会用Bootstrap Classloader来查找类。

ClassLoader类---------private Class<?> findBootstrapClassOrNull(String name){    if (!checkName(name)) return null;    return findBootstrapClass(name);}    private native Class<?> findBootstrapClass(String name);复制代码

4 常见的问题

4.1 为什么使用双亲委托机制?

  1. 避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。
  2. 安全方面的考虑,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这样便会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类。

4.2 由不同的类加载器加载的类会被JVM当成同一个类吗?

不会。 在Java中,我们用包名+类名作为一个类的标识。 但在JVM中,一个类用其包名+类名和一个ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间.

通过一个demo来看,

  1. 用两个自定义类加载器去加载一个自定义的类
  2. 然后获取到的Class进行java.lang.Object.equals(…)判断。
public class Main {    public static void main(String[] args) {            ClassLoaderTest myClassLoader = new ClassLoaderTest("F:\\");        ClassLoaderTest myClassLoader2 = new ClassLoaderTest("F:\\");        try {            Class c = myClassLoader.loadClass("com.example.Hello");            Class c2 = myClassLoader.loadClass("com.example.Hello");            Class c3 = myClassLoader2.loadClass("com.example.Hello");            System.out.println(c.equals(c2)); //true            System.out.println(c.equals(c3)); //flase    }}复制代码

输出结果:

truefalse复制代码

只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类。

上面demo中用到的自定义ClassLoader:

自定义的类加载器注意点:1.覆写findClass方法2.让其可以根据name从我们指定的path中加载文件,也就是将文件正确转为byte[]格式3.使用defineClass方法将byte[]数据转为Class对象-------------public class ClassLoaderTest extends ClassLoader{    private String path;    public ClassLoaderTest(String path) {        this.path = path;    }    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        Class clazz = null;        byte[] classData = classToBytes(name);        if (classData == null) {            throw new ClassNotFoundException();        } else {            clazz= defineClass(name, classData, 0, classData.length);        }        return clazz;    }    private byte[] classToBytes(String name) {        String fileName = getFileName(name);        File file = new File(path,fileName);        InputStream in=null;        ByteArrayOutputStream out=null;        try {            in = new FileInputStream(file);            out = new ByteArrayOutputStream();            byte[] buffer = new byte[1024];            int length=0;            while ((length = in.read(buffer)) != -1) {                out.write(buffer, 0, length);            }            return out.toByteArray();        } catch (IOException e) {            e.printStackTrace();        }finally {            try {                if(in!=null) {                    in.close();                }            } catch (IOException e) {                e.printStackTrace();            }            try{                if(out!=null) {                    out.close();                }            }catch (IOException e){                e.printStackTrace();            }        }        return null;    }    private String getFileName(String name) {        int index = name.lastIndexOf('.');        if(index == -1){            return name+".class";        }else{            return name.substring(index+1)+".class";        }    }}复制代码

结语

到此Java的类加载器以及双亲委托机制都讲了个大概,如果文中有错误的地方、或者有其他关于类加载器比较重要的内容又没有介绍到的,欢迎在评论区里留言,一起交流学习。


作者:某人Valar
链接:https://juejin.im/post/5e094d8df265da339772b7f6
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

推荐阅读:河南蒙古族自治县郁谷翠水产资讯中心


相关新闻
新闻焦点
  红孩儿The Boys三十周年复出,华语乐坛话题不断,跟着师妹Amanda王欣晨进行两周紧锣密鼓的内地宣传行程,最终站回到上海,担任“最爱[更多]
  • 阿迪达斯已经成功使用突破性的“可循环再制工艺”生产整体可回收的运动鞋履  • [FUTURECRAFT.LOOP]是一款整体可回收的运动[更多]
  拥有最神秘华语创作新人之称的持修,从网络窜起,用一首〈Imma Get A New One〉短短三天瞬间成为让所有人都关注的声音;而他即将[更多]
阅璟山为您开启未来美好生活!尊崇REVERE高成本奢阔门庭打造只为归家尊崇有度约300㎡现代极简入户门庭高成本打造予归家尊崇礼遇智慧WISDOM[更多]
关于我们 | 联系我们 | 老版地图 | 网站地图 | 版权声明
版权所有:福州热线未经授权禁止复制或建立镜像
相关作品的原创性、文中陈述文字以及内容数据庞杂本站无法一一核实,如果您发现本网站上有侵犯您的合法权益的内容,请联系我们,本网站将立即予以删除!
中国互联网违法和不良信息举报中心 网络警察报警岗亭