java类加载过程及双亲委派机制

  • Post author:
  • Post category:java


java的类加载过程及双亲委派机制



java类加载过程及双亲委派机制



四种类加载器

在jvm加载class文件主要由类加载器完成,不同类库下的class加载的加载器也不相同,jvm中有以下四种类加载器:


  • boostrapLoader:

    引导类加载器:

    负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等

  • ExtClassLoader:

    拓展类加载器:

    负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包,以上两种均加载jre中的类

  • AppClassLoader:

    应用程序类加载器:

    负责加载ClassPath路径下的类包,主要就是加载自己写的类

  • 自定义类加载器:


    除此之外我们也可以自定义类加载器,负责加载用户自定义路径下的类包


以下将通过一段代码更好地展示java类加载的过程:

/**
 * jvm获取类加载器过程
 */
public class GetClassLoader {
    public static void main(String[] args) {
//        核心类库下面的class加载,结果为null,此类加载器为c++本地方法调用获得,java无法打印
        System.out.println(String.class.getClassLoader());
//        拓展类库下面的class加载
        System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
//        自己定义的类(此处为当前类)class加载
        System.out.println(GetClassLoader.class.getClassLoader().getClass().getName());
        System.out.println("------------------------------------------------------");

//        应用程序类加载器:负责加载ClassPath路径下的类包,加载自己的类
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
//        拓展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
        ClassLoader extClassLoader = appClassLoader.getParent();
//        引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println("bootStrapLoader: "+bootstrapClassLoader);
        System.out.println("extClassLoader: "+extClassLoader);
        System.out.println("appClassLoader: " + appClassLoader);
        System.out.println();
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i]);
        }
        System.out.println("--------------------------------------------");
        System.out.println("extClassLoader 加载以下文件: ");
        System.out.println(System.getProperty("java.ext.dirs"));

    }
}

执行结果:

null

sun.misc.Launcher $ ExtClassLoader sun.misc.Launcher $

AppClassLoader


-------------------------------------------------------


bootStrapLoader: null

extClassLoader: sun.misc.Launcher $ExtClassLoader@2d209079

appClassLoader: sun.misc.Launcher $AppClassLoader@18b4aac2


-------------------------------------------------------


bootstrapLoader加载以下文件:

file:/C:/JDK/jre/lib/resources.jar

file:/C:/JDK/jre/lib/rt.jar

file:/C:/JDK/jre/lib/sunrsasign.jar

file:/C:/JDK/jre/lib/jsse.jar

file:/C:/JDK/jre/lib/jce.jar

file:/C:/JDK/jre/lib/charsets.jar

file:/C:/JDK/jre/lib/jfr.jar

file:/C:/JDK/jre/classes


-------------------------------------------------------


extClassLoader 加载以下文件: C:\JDK\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

值得注意的是String类的类加载器打印出来为null,这是因为string是由引导类加载器加载完成,引导类加载器则是由c++发起调用,创建的类加载器,java无法将本地方法创建的类加载器打印出来



类加载器初始化过程:

  • 运行我们所写的应用程序时,java程序运行,C++调用底层dll文件创建java虚拟机,C++创建一个引导类加载器实例sun.misc.Launcher,其初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。
  • 在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。
  • JVM默认使用Launcher的getClassLoader()方法返回的对应的类加载器AppClassLoader的实例加载我们的应用程序。
  • 类加载完成,jvm执行入口函数,程序运行结束后销毁当前jvm虚拟机

    整体流程如下

    jvm执行类加载过程



双亲委派机制

jvm的类加载器具有层级关系,关系如下

在这里插入图片描述
jvm加载类的时候,会先委托父加载器在其路径下寻找目标类,父加载器找不到再委托上层父加载器加载,如果最上层的父加载器仍然没有找到目标类,那么则会交由该类加载器在自己的类路径中查找并载入目标类。



从源码理解双亲委派

用APPclassload源码学习双亲委派机制是最适合的

下面是AppClassLoader的loadClass方法

 public synchronized Class loadClass(String var1, boolean var2) throws ClassNotFoundException {
        int var3 = var1.lastIndexOf(46);
        if (var3 != -1) {
            SecurityManager var4 = System.getSecurityManager();
            if (var4 != null) {
                var4.checkPackageAccess(var1.substring(0, var3));
            }
        }

        try {
        // 这里调用了ClassLoader类的loadClass方法
            return super.loadClass(var1, var2);
        } catch (ClassNotFoundException var5) {
            throw var5;
        } catch (RuntimeException var6) {
            throw var6;
        } catch (Error var7) {
            throw var7;
        }
    }

代码中关键的处调用了父类(ClassLoader)的loadClass方法

下面是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 {
                // 如果有上级类加载器且没有加载类
                    if (parent != null) {
                    // 调用父加载器的loadClass()方法
                        c = parent.loadClass(name, false);
                    } else {
                    // 没有父加载器,则当前加载器为引导类加载器,该方法最后为C++执行的本地方法
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 如果仍然没有加载到,调用URLclassLoader重写的findClass()方法,在当前类加载器路径中寻找需要加载的类
                    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;
        }
    }

说明:

  • 当父加载器都没找到类的时候就会执行classLoader的findClass()方法,该方法在classLoader中没有实现,需要子类重写实现
  • APPClassLoader继承自UrlClassLoader,而UrlClassLoader重写了该方法,APPClassLoader调用父类的findClass方法.
  • 其方法的主要作用为:从URL搜索路径中查找并加载具有指定名称的类。所有引用JAR文件的URL都会根据需要加载并打开,直到找到该类为止



双亲委派机制的意义

  • 沙箱安全机制:即使我们自己重写了一个String类也不会被类加载器所加载,可以防止核心库被篡改
  • 避免类的重复加载:当父类已经加载了该类的时候,没必要子类classLoader重新加载

  • 全盘负责委托机制


    “全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。



实现自定义类加载器

在这里插入图片描述

在这里插入图片描述

从loadClass类的这两段代码中我们可以知道,如果父加载器都没有找到该类,则调用子类重写的findClass()方法,所以实现自定义类加载的关键为重写findClass()方法

  • 我们模仿URLClassLoader写一个简单的自己的加载器MyClassLoader
public class MyClassLoader extends ClassLoader {

    private String path;

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
         path = path+"/"+name.replace('.', '/').concat(".class");

        FileInputStream fis = null;
        try {
            fis = new FileInputStream(path);
            int lenth = fis.available();
            byte[] bytes = new byte[lenth];
            fis.read(bytes);
            return defineClass(name, bytes,0,lenth);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
    
}

  • 一个用于测试的类MyTestClass
public class MyTestClass1 {

    public void printStr(String name) {
        System.out.println("Hello "+name);
    }
}

将这个类的class文件复制出来,放在自己新建的文件夹下(不要放在当前项目类路径下,当前类路径下的class文件需要删除或者改名,否认会默认被APPClassLoader加载)

  • 测试一下我们自己写的加载器
public class GetClassLoader {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
        MyClassLoader classLoader = new MyClassLoader("C:/code/com/test");
        Class clazz = classLoader.loadClass("testGetClassLoader.MyTestClass");
        Object obj = clazz.newInstance();
        Method printStr = clazz.getMethod("printStr", String.class);
        printStr.invoke(obj, "小明");
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

  • 执行结果

Hello 小明

testGetClassLoader.MyClassLoader

从打印的类加载器名可以看到我们已经成功用自己写的类加载器加载了这个类,一个简单的类加载器就完成了。



打破双亲委派机制

学会写自己的类加载器,那么如何打破双亲委派机制应该就不难了。

双亲委派主要靠classLoader的loadClass方法实现,想要打破双亲委派,实现加载自己写的核心类,那么就需要重写loadClass方法

  • 我们在MyClassLoader的基础上增加如下代码(我们copy一下源码删掉双亲委派部分)
/**
     * 重写loadClass方法
     *
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    @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 t0 = System.nanoTime();
//                如果是核心类,由于有安全保护机制,所以不允许从外部加载,只能交由上层类加载器加载
                if (name.startsWith("java")) {
                    c = this.getParent().loadClass(name);
                }
                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;
        }
    }
  • 刚才我们将MyTestClass1 .class从我们的classPath下删除了,因为没有打破双亲委派的时候将会默认由APPClassLoader进行加载,现在我们将MyTestClass1 .class复制到classPath下,并且用我们自定义的classLoader进行加载
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
		MyClassLoader classLoader = new MyClassLoader("C:/code/com/test");
        Class clazz = classLoader.loadClass("testGetClassLoader.MyTestClass1");
        Object obj = clazz.newInstance();
        Method printStr = clazz.getMethod("printStr", String.class);
        printStr.invoke(obj, "小明");
        System.out.println(clazz.getClassLoader().getClass().getName());

    }

执行结果

Hello 小明

testGetClassLoader.MyClassLoader

从执行结果上来看已经实现了打破双亲委派机制的目的

以上就是java中的双亲委派及双亲委派的打破,在工作中可能会很有帮助



版权声明:本文为Darlight原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。