美文网首页程序员
这篇关于JVM类加载器和双亲委派机制的笔记写的太好了,建议收藏起

这篇关于JVM类加载器和双亲委派机制的笔记写的太好了,建议收藏起

作者: 程序员伟杰 | 来源:发表于2020-08-11 14:08 被阅读0次

前言

Java里有如下几种类加载器

  1. 启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库比如 rt.jar、charsets.jar等。
  2. 扩展类加载器(ExtClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
  3. 应用程序类加载器(AppClassLoader):负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
  4. 自定义加载器:负责加载用户自定义路径下的类包。

通过以下实例来了解各个类加载器:

public class ClassLoaderTest {

    public static void main(String[] args) {
        System.out.println(Object.class.getClassLoader());
        // java提供的与DNS服务交互的api
        System.out.println(DNSNameService.class.getClassLoader());
        System.out.println(ClassLoaderTest.class.getClassLoader());

    }
}

运行结果如下:

null
sun.misc.Launcher$ExtClassLoader@6d6f6e28
sun.misc.Launcher$AppClassLoader@58644d46

启动类加载器是在有jvm底层创建的实例,所以在获取时为null,Object类是有启动类加载器进行加载的,所以获取其加载器时为null,而DNSNameService为JAVA_HOME/jre/lib目录下ext文件夹在的dnsns.jar包中的类,由扩展类加载器(ExtClassLoader)加载。而自己编写的类ClassLoaderTest 则由AppClassLoader进行加载。

Java中各个类加载器的层次关系

在上面已经介绍过,java的类加载器也是普通的类,ExtClassLoader和AppClassLoader均是URLClassLoader的子类,而URL的继承关系如下:


那么AppClassLoader和ExtClassLoader为ClassLoader的子类。在上面已经已经介绍过在jvm启动时会通过sun.misc.Launcher的getLauncher方法从而获取Launcher的实例,那么在这个过程中Launcher会通过构造方法创建该类的实例。sun.misc.Launcher的构造方法如下:

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        // 创建ExtClassLoader
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        // 创建AppClassPoader
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
      // 省略代码 ....
    }
}

通过分析sun.misc.Launcher构造方法我们知道在sun.misc.Launcher类的实例创建是会创建AppClassLoader实例和ExtClassLoader实例。同时由于两个类加载器均继承自ClassLoader,而ClassLoader中有一个ClassLoader的全局变量parent,该类类型也是ClassLoader:

public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;

    // 省略代码 ....
}

而在sun.misc.Launcher创建时,实例化ExtClassLoader和AppClassLoader时均指定其parent属性分别为null和ExtClassLoader。那么java中的类加载器的机构就如下:


自定义类加载器

自定义类加载器需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,大体逻辑

  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再 加载, 直接返回。

  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器, 则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用 Bootstrap类加载器来加载。

  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类 加载器 的findClass方法来完成类加载。还有一个方法是findClass,默认 实现是抛出异常,所以我们自定义类加载器主要是重写 findClass方法。

接下来看一个示例,首先我们编写需要自定义类加载器加载的类,如下:

package com.dp.jvm;

import java.io.PrintStream;

public class User
{
  public void say()
  {
    System.out.println("hello");
  }
}

需要注意的是,该类编写完成需要在工程中删除,避免AppClassLoader加载。编译完成后将该类的class文件放置指定的目录下:


然后编写自定义的类加载器,代码如下:

class MyClassLoader extends ClassLoader{

    private final String path;

    MyClassLoader(String path) {
        this.path = path;
    }

    /**
     * 重写ClassLoader的findClass方法,获取到类的Class对象
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] byteArrayFromClassName = getByteArrayFromClassName(name);
        return defineClass(name, byteArrayFromClassName, 0, byteArrayFromClassName.length);
    }

    /**
     * 通过类的全限定名称获取到类的二进制数据
     * @param name
     * @return
     */
    private byte[] getByteArrayFromClassName(String name) {
        String classPath = convertNameToPath(name);
        byte[] data = null;
        int off = 0;
        int length = 0;
        try(BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(classPath))) {
            data = new byte[bufferedInputStream.available()];
            while ((length = bufferedInputStream.read(data, off, data.length - off)) > 0) {
                off += length;
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return data;
    }

    /**
     * 通过类的全限定名称获取到对应类文件的的字节码文件路径
     * @param name
     * @return
     */
    private String convertNameToPath(String name) {
        String relativePath = name.replace(".", File.separator);
        String absolutePath = path + File.separator + relativePath + ".class";
        return absolutePath;
    }
}

编写测试类,通过使用自定义的类加载将User加载并实例化,然后调用其say方法,如下:

public class CustomClassLoaderTest {

    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("F:\\test");
        Class<?> clazz = myClassLoader.loadClass("com.dp.jvm.User");
        Object o = clazz.newInstance();
        Method say = clazz.getDeclaredMethod("say");
        say.invoke(o);
    }
}

通过上面了实例,简单的实现了一个自定义的类加载器。接留下来了解一下类加载器的双亲委派机制。

双亲委派机制

JVM类加载器是有亲子层级结构的,如下图:


需要注意的是,这里的额亲子层级结构不是指的java中的继承关系,而是每一个类加载实现类都具有一个parent全局变量,而该全局变量的类型为ClassLoader。这里可能有一个疑问,在自定义类加载器中并未看到名称parent的全局变量。这是因为这个全局变量是在ClassLoader中定义声明的。

public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;

    // 省略代码 ....

还有一个问题也需要说明一下,我们自定义的类加载器的parent属性是如何设置的呢?怎么知道设置的为AppClassLoader呢?因为自定义的类加载器继承自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 instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

从ClassLoader的实现来看,通过getSystemClassLoader()方法获取系统类加载器然后将其赋值给parent属性。那么来看一下getSystemClassLoader()具体实现:

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

// 初始化系统类加载器
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;
            scl = 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 exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}

在getSystemClassLoader方法中获取sun.misc.Lanucher实例(单例),然后调用其getClassLoader方法获取系统类加载器,然后设置给parent方法。最后来看一下sun.misc.Lanucher的getClassLoader方法:

public ClassLoader getClassLoader() {
    return this.loader;
}

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

结合sun.misc.Lanucher的getClassLoader和构造方法可知系统类加载器就是AppClassLoader。
在了解了jvm中类加载器的组成结构后,我们再来看一下jvm中各个类加载器的组成的结构:


在了解了jvm中各个类加载器的层次结构之后,加下来来解析双亲委派机制就相对来说简单多了,首先从双亲委派的流程说起。

双亲委派流程

双亲委派流程如下:


加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
比如我们的PrintTest 类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托启动类加载器,顶层启动类加载器在自己的类加载路径里找了半天没找到PrintTest 类,则向下退回加载PrintTest 类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到PrintTest 类,又向下退回PrintTest 类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载。

那么为什么要设置双亲委派机制呢?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

双亲委派机制源码剖析

双亲委派的原理体现在ClassLoader的loadClass方法中:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先检查类是够已经被加载
        Class<?> c = findLoadedClass(name);

        // 类还未加载则加载,使用双亲委派机制
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 判断当前类加载器是否设置了父加载器,设置了则
                // 调用父加载器的loadClass进行加载,如果父加载也是ClassLoader
                // 的子类则会再次进入该方法,判断是否有父类加载器,依次递归
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 当类加载没有设置parent父加载,那么就使用启动类加载器加载
                    // 由于启动类加载器是底层创建的实例,所以该方法会调用本地
                    // native方法
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {

            }

            if (c == null) {
                // 向上委派所有父加载器仍然没有加载到参数类,那么调用当前
                // 类加载器进行类的加载
                long t1 = System.nanoTime();
                c = findClass(name);
                // ... 省略 

            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

打破双亲委派

以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一 个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份, 因此要保证每个应用程序的类库都是独立的,保证相互隔离。比如:在tomcat容器中存在存在两个应用A和B,A使用的是Spring4,而应用B使用的是Spring5,如果使用双亲委派,那么可能会导致版本冲突从而报错,如果在版本4中不存在x方法,但是先加载了版本4的字节码,那么版本5的就不会在加载了(类限定名相同),那么在程序B中调用x方法则会抛出方法不存在异常。
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。我们常见的web应用中的Servlet依赖,一般在maven中依赖作用于都是provided的,web程序都是使用的容器的Serlvert,如果都是各自的那么造成类的重复加载。
  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  4. web容器要支持jsp的修改,我们知道,jsp文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,web容器需要支持 jsp修改后不用重启.了解jsp机制的都知道,是将jsp解析成一个对应的Servlet(就是常说的一个jsp就是一个servlet),jsp就是通过动态生成.class文件从而实现动态资源的。

再看看我们的问题:
Tomcat 如果使用默认的双亲委派类加载机制行不行?

  • 第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,
    默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
  • 第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
  • 第三个问题和第一个问题一样。
  • 我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

相关文章

网友评论

    本文标题:这篇关于JVM类加载器和双亲委派机制的笔记写的太好了,建议收藏起

    本文链接:https://www.haomeiwen.com/subject/jhtydktx.html