(3)JVM 类加载之双亲委派机制

目录

  • 前言
  • 一、双亲委派机制
    • 1、各个类加载器之间的关系
    • 2、自定义类加载器
    • 3、双亲委派逻辑的实现
  • 二、双亲委派机制的优缺点
  • 三、打破双亲委派机制
  • 总结

前言

上一节(2)JVM 类加载之类加载器初始化记录了JVM自带的3中类加载器,也分析了类加载器初始化的流程。那么问题来了:

  1. 这么几种类加载器之间的关系是什么?
  2. 有这么几种类加载器,怎么确定一个类由哪个类加载器加载?
  3. 为什么要分这么几种类加载器呢?一个类加载器不也能加载吗?
  4. 能够打破双亲委派机制吗?

本文涉及到的源码都在JDK的jre\lib\rt.jar包中。

一、双亲委派机制

1、各个类加载器之间的关系

双亲委派机制用在一个类被加载之前,因为需要判断由哪个类加载器去加载这个类。双亲委派机制在Java运行一个类的流程中的位置如图:
(3)JVM 类加载之双亲委派机制_第1张图片
从图中可以看出,引导类加载器是拓展类加载器的父加载器,拓展类加载器是应用程序类加载器的父加载器,应用程序类加载器是自定义类加载器的父加载器。在加载一个类之前,若没有自定义类加载器,则默认是从应用程序类加载器开始加载,逐级委托父级类加载器,最终委托到引导类加载器。若引导类加载器不能加载需要加载的类,则委派拓展类加载器进行加载;若拓展类加载器仍不能进行加载,则委派应用程序类加载器来完成加载。若自定义了类加载器,并使用自定义类加载器对某个类进行加载,则从自定义类加载器开始逐级委托,然后逐级委派。

为什么这几种类加载器是这样的关系呢?因为源码是这样设计的。

在下面分析之前,先放上一个类图:
(3)JVM 类加载之双亲委派机制_第2张图片
这是在IDEA中点击ExtClassLoader然后按组合键Ctrl + alt + U看见的类图,其实这个类图没有画完,我在visio中画了一下:
(3)JVM 类加载之双亲委派机制_第3张图片
可以发现,拓展类加载器虽然是应用程序类加载器的父级加载器,但应用程序类加载器并不是继承于拓展类加载器,只不过在应用程序类加载器的类中有个parent属性,这个属性是从ClassLoader继承过来的,里面存的值就是拓展类加载器对象的引用,所以看起来他们像是父子的关系。其它的加载器情况与上面的类似,自定义类加载器parent应用程序类加载器拓展类加载器parent是null,因为其父类加载器引导类加载器是用C++实现的,在Java里获取不到。

我在(2)JVM 类加载之类加载器初始化中记录过,拓展类加载器应用程序类加载器是在sun.misc.Launcher.getLauncher()这里创建的,那么我们具体再跟进一下,先进入sun.misc.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);
    }
    ...
}

在(2)JVM 类加载之类加载器初始化中分析得到:

  1. var1是创建的拓展类加载器;
  2. this.loader是创建的应用程序类加载器。

继续跟进getExtClassLoader()

public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
    final File[] var0 = getExtDirs();

    try {
        return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
            public Launcher.ExtClassLoader run() throws IOException {
                int var1 = var0.length;

                for(int var2 = 0; var2 < var1; ++var2) {
                    MetaIndex.registerDirectory(var0[var2]);
                }

                return new Launcher.ExtClassLoader(var0);
            }
        });
    } catch (PrivilegedActionException var2) {
        throw (IOException)var2.getException();
    }
}

看到返回值是:

return new Launcher.ExtClassLoader(var0);

那么继续跟进ExtClassLoader(var0)

public ExtClassLoader(File[] var1) throws IOException {
    super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
    SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}

发现进入到了拓展类加载器的一个有参构造器,里面有一行调用其父类的构造器的代码:

super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);

发现传进了一个**(ClassLoader)null**,这其实是其父类加载器,继续跟进super

(3)JVM 类加载之双亲委派机制_第4张图片
继续跟进superparent是上回传进来的null

(3)JVM 类加载之双亲委派机制_第5张图片
再次跟进,parent依然是null

(3)JVM 类加载之双亲委派机制_第6张图片
跟进thisparentnull

(3)JVM 类加载之双亲委派机制_第7张图片
咦!发现赋值了:

this.parent = parent;

这个this.parentExtClassLoader的一个属性,它最终被赋值为null,印证了上面的说法。

应用程序类加载器的分析差不多,从下面的Launcher构造器源码开始:

(3)JVM 类加载之双亲委派机制_第8张图片
跟进getAppClassLoader(var1),注意传值var1是上面创建的拓展类加载器对象

(3)JVM 类加载之双亲委派机制_第9张图片
跟进AppClassLoader(var1x, var0),注意传值var0拓展类加载器对象,进入到AppCLassLoader的有参构造器:

(3)JVM 类加载之双亲委派机制_第10张图片
跟进super,注意传值var2拓展类加载器对象

(3)JVM 类加载之双亲委派机制_第11张图片
跟进super,注意传值parent拓展类加载器对象

(3)JVM 类加载之双亲委派机制_第12张图片
跟进super,注意传值parent拓展类加载器对象

(3)JVM 类加载之双亲委派机制_第13张图片
跟进this,注意传值parent拓展类加载器对象

(3)JVM 类加载之双亲委派机制_第14张图片
到了赋值的地方,this.parent被初始化为拓展类加载器

经过上面的源码分析,前言问题1的答案一目了然。

2、自定义类加载器

下面的代码是一个自定义的类加载器:

package com.jim.jvm.classload;

import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {

    private String path; //默认加载路径
	
	// 初始化自定义类加载器
    MyClassLoader(String path) {
        // TODO Auto-generated constructor stub
        super();
        this.path = path;
    }

	// 重写ClassLoader的finClass函数(为了加载自定义路径下的字节码文件)
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        // TODO Auto-generated method stub
        byte[] b = new byte[0];
        try {
            b = loadData(name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return defineClass(name, b, 0, b.length);
    }

	// 读取自定义路径的字节码文件
    private byte[] loadData(String name) throws IOException {
    	// 将com.jim.jvm.classload.Test的'.'替换成'/'
        name = name.replaceAll("\\.", "/");
        // 从自定义类路径D:/test/com/jim/jvm/classload/Test.class读取字节码文件
        FileInputStream fis = new FileInputStream(this.path + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
}

接下来测试自定义加载器:

package com.jim.jvm.classload;

public class MyClassLoaderTest {
     public static void main(String[] args) throws Exception {
     	 // 创建自定义类加载器对象
         MyClassLoader loader1 = new MyClassLoader("D:/test");
         String name = "com.jim.jvm.classload.Test";
         loadClassByMyClassLoader(loader1, name);
     }

     private static void loadClassByMyClassLoader(ClassLoader loader, String name) throws Exception{
         Class<?> c = loader.loadClass(name);
         // 输出加载自己这个类的类加载器
         System.out.println(c.getClassLoader());
     }

}

在测试中,我在D盘创建了一个test文件夹,然后创建com、jim、jvm和classload文件夹,将需要加载的字节码文件Test.class放进去。

在程序中设置自定义加载器的加载路径是D:/test,要加载的文件是com.jim.jvm.classload.Testcom.jim.jvm.classload这个路径一定要和正在运行的这个程序所在目录的路径一致

最后的输出是:

com.jim.jvm.classload.MyClassLoader@677327b6

这样就成功实现了一个自定义类加载器。

上面自定义类加载器的有参构造器代码如下,

// 初始化自定义类加载器
MyClassLoader(String path) {
    // TODO Auto-generated constructor stub
    super();
    this.path = path;
}

其中有这行代码:

super();

我们知道,在面向对象中,一个类在初始化时,会先初始化构造其父类,而我们自定义类加载器的父类是ClassLoader,跟进代码,跳转到父类的构造器:
(3)JVM 类加载之双亲委派机制_第15张图片
再跟进

getSystemClassLoader()

进入到:
(3)JVM 类加载之双亲委派机制_第16张图片
进入initSystemClassLoader()
(3)JVM 类加载之双亲委派机制_第17张图片
我在(2)JVM 类加载之类加载器初始化中贴了一张sum.misc.Launcher类的成员属性部分的截图:
(3)JVM 类加载之双亲委派机制_第18张图片
那么执行上一张截图中这一行代码

sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

就能获取到由引导类加载器加载创建的单例sun.misc.Launcher对象。

接下来执行上张图的

scl = l.getClassLoader();

跟进函数getClassLoader()

(3)JVM 类加载之双亲委派机制_第19张图片
返回的是this.loader!!!在上一小节已经分析得到:this.loader是创建的应用程序类加载器,所以这里返回的是应用程序类加载器

src = 应用程序类加载器

再返回继续执行上层函数:

(3)JVM 类加载之双亲委派机制_第20张图片
上层函数最终也返回了scl,也就是应用程序类加载器

再返回上层调用:

(3)JVM 类加载之双亲委派机制_第21张图片
进入执行this

(3)JVM 类加载之双亲委派机制_第22张图片
最后进行赋值啦!

this.parent = parent;

这里的this.parent是自定义类加载器MyClassLoader继承自ClassLoader的属性,最终被赋值为应用程序类加载器对象,也就是说,自定义类加载器的默认父类加载器是应用程序类加载器

3、双亲委派逻辑的实现

前面两小节详细分析了四种类加载器之间的关系。那么既然有四种类加载器,在对一个类进行加载时,怎么判断由哪个类加载器来加载呢?

JVM的设计者们设计了双亲委派机制。

下面是我自己写的一个加法类:

package com.jim.jvm.classload;

public class Add {

    private int add(int a, int b){
        return a + b;
    }

    public static void main(String[] args) {
        int a = 10, b = 20;
        Add ad = new Add();
        int result = ad.add(a, b);
    }
}

在相应地地方加上断点,并在如下图需要传name参数的地方添加debug表达式,以此来过滤其它类(核心类等)的加载过程,直达Add类的加载:

(3)JVM 类加载之双亲委派机制_第23张图片
输入的表达式是:

name.equals("com.jim.jvm.classload.Add")

现在开始debug:

(3)JVM 类加载之双亲委派机制_第24张图片
即将进入loadClass函数加载com.jim.jvm.classload.Add

(3)JVM 类加载之双亲委派机制_第25张图片
进入到应用程序类加载器

(3)JVM 类加载之双亲委派机制_第26张图片

Class<?> c = findLoadedClass(name);

这行代码的功能是寻找应用程序类加载器是否已经加载了Add类,底层实现是本地方法(C++),加载了就赋值给c,没有加载则返回null

因为Add类还没被加载过,因此

c = null

进入下一个断点执行:

c = parent.loadClass(name, false);

(3)JVM 类加载之双亲委派机制_第27张图片
上面这行代码是调用了应用程序类加载器父类加载器loadClass方法,也就是将Add委托给拓展类加载器加载,于是进入下一个断点执行:

Class<?> c = findLoadedClass(name);

(3)JVM 类加载之双亲委派机制_第28张图片
因为拓展类加载器也没有加载过Add,所以执行结果肯定是

c = null

接下来判断:

parent != null ?

我们知道,拓展类加载器的父类加载器是null,因此会进入执行:

c = findBootstrapClassOrNull(name);

(3)JVM 类加载之双亲委派机制_第29张图片
该方法的功能是调用本地方法(C++实现)来查找引导类加载器是否加载过Add

很明显没有加载过,因此:

c = null

进入下一个断点执行:

c = findClass(name);

(3)JVM 类加载之双亲委派机制_第30张图片
findClass(name)在上节的自定义类加载器中也用到过,功能是寻找当前类加载器所能够加载的目录下是否有Add

拓展类加载器的加载目录是jre.lib.ext,很明显Add不在此目录,因此拓展类加载器loadClass函数最终返回的cnull

回到上一层应用程序类加载器loadClass函数,继续往下执行,发现cnull,则进入断点执行:

c = findClass(name);

(3)JVM 类加载之双亲委派机制_第31张图片
我们知道,Add就在应用程序类加载器的加载目录中,因此成功被加载,最终返回:

c = Add对象

(3)JVM 类加载之双亲委派机制_第32张图片
最后将Add的对象赋值给了ad变量,整个类加载流程完毕。

到这里,就能回答前言中的问题2了。

二、双亲委派机制的优缺点

优点:

  1. 沙箱安全机制。试想一下,引导类加载器加载的是Java的核心类库,这些类库肯定是不能轻易被修改的,因此若是我们自己实现了一个List类,在里面添加了一些新的方法,按照上节的流程,当List被委托到引导类加载器后,发现该同名类已经被加载了,因此就返回了原来加载的核心类,这样我们自己写的List类就不起作用了。
  2. 避免重复加载。当发现父类加载器发现自己已经加载了某个类时,会直接返回已经加载过的类的对象,而不会再去委派子类加载器来加载,这样既避免了重复加载,保证了被加载类的唯一性,也减少了类加载的时间。

缺点:

  1. 不能同时支持多种不同版本的同名类同时运行。双亲委派机制能够保护核心类库,能够避免重复加载,但这也是它的缺点。要想同时支持多种不同版本的同名类同时运行,比如Tomcat同时运行多个不同版本的Spring项目,就需要打破双亲委派机制,为每个项目都单独设置一个类加载器来加载,这样就能隔离同名不同版本的类。这就能够回答前言中的问题3了。

三、打破双亲委派机制

回答前言中的问题四:双亲委派机制当然能够打破。

第一节第三小节中我们从源码上分析了双亲委派机制的实现逻辑,也知道每个类加载器的顶级父类是ClassLoader,顶级父类中有两个核心方法findClassloadClass。我们在第一节第二小节中的自定义类加载器里面重写过findClass,而双亲委派机制的核心实现在loadClass里,那想要打破这个机制,就得在自定义类加载器中重写loadClass,下面是打破双亲委派机制的自定义类加载器代码:

要重写loadClass,可以在ClassLoadr中直接把源码复制过来:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        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) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            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 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;
    }
}

然后删除双亲委派的部分:

try {
    if (parent != null) {
        c = parent.loadClass(name, false);
    } else {
        c = findBootstrapClassOrNull(name);
    }
} catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
}

最后将

c = findClass(name);

修改成:

if (!name.startsWith("com.jim.jvm.classload")){
    c = this.getParent().loadClass(name);
}else{
    c = findClass(name);
}

为什么呢?因为在加载自己的类之前还需要加载很多其它类库,比如核心类库,如果不修改,那么在自定义的路径下肯定找不到核心类库,因此需要将这些类委托给父加载器,也就是引用程序类加载器,然后通过双亲委派的流程来进行加载。

最终得到:

package com.jim.jvm.classload;

import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {

    private String path; //默认加载路径

    MyClassLoader(String path) {
        // TODO Auto-generated constructor stub
        super();
        this.path = path;
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        // TODO Auto-generated method stub
        byte[] b = new byte[0];
        try {
            b = loadData(name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return defineClass(name, b, 0, b.length);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t1 = System.nanoTime();
                if (!name.startsWith("com.jim.jvm.classload")){
                    c = this.getParent().loadClass(name);
                }else{
                    c = findClass(name);
                }

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    private byte[] loadData(String name) throws IOException {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(this.path + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
}

运行测试:

package com.jim.jvm.classload;

public class MyClassLoaderTest {
     public static void main(String[] args) throws Exception {
         MyClassLoader loader1 = new MyClassLoader("D:/test");
         String name = "com.jim.jvm.classload.Test";
         loadClassByMyClassLoader(loader1, name);
     }

     private static void loadClassByMyClassLoader(ClassLoader loader, String name) throws Exception{
         Class<?> c = loader.loadClass(name);
         System.out.println(c.getClassLoader());
     }

}

最终结果:

(3)JVM 类加载之双亲委派机制_第33张图片
可以看到,我在应用程序类加载器的加载目中中也放置了Test类,而我想要加载的是D:/test/com/jim/jvm/classload/Test.class,最终输出的Test类的加载器也是自定义的类加载器,可见上面的自定义类加载器成功打破了双亲委派机制。

总结

第一次发现肝源码如此有趣,继续加油!

你可能感兴趣的:(JVM学习,java,jvm)