资讯专栏INFORMATION COLUMN

【修炼内功】[JVM] 深入理解JVM之ClassLoader

荆兆峰 / 1774人阅读

摘要:本文已收录修炼内功跃迁之路在诞生之初便提出,各提供商发布很多不同平台的虚拟机,这些虚拟机都可以载入并执行同平台无关的字节码。设计者在第一版虚拟机规范中便承诺,时至今日,商业机构和开源机构已在之外发展出一大批可以在上运行的语言,如等。

本文已收录【修炼内功】跃迁之路

Java在诞生之初便提出 "Write Once, Run Anywhere",各提供商发布很多不同平台的虚拟机,这些虚拟机都可以载入并执行同平台无关的字节码。

设计者在第一版Java虚拟机规范中便承诺 "In the future, we will consider bounded extensions to the Java virtual machine to provide better support for other languages",时至今日,商业机构和开源机构已在Java之外发展出一大批可以在JVM上运行的语言,如Clojure、Groovy、JRuby、Jpython、Scala、Kotlin等。这些语言都不是本地可执行程序,需要JVM将编译后的class文件加载后才能够运行,负责加载class文件的组件便是ClassLoader.

JVM类加载流程

java语言系统内置了众多类加载器,从一定程度上讲,只存在两种不同的类加载器:一种是启动类加载器,此类加载由C++实现,是JVM的一部分;另一种就是所有其他的类加载器,这些类加载器均由java实现,且全部继承自java.lang.ClassLoader

Bootstrap ClassLoader 启动类加载器,最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib目录中或-Xbootclasspath中参数指定的路径中的,并且是虚拟机识别的(按名称)类库

Extention ClassLoader 扩展类加载器,由启动类加载器加载,实现为sun.misc.Launcher$ExtClassLoader,负责加载目录%JRE_HOME%/lib/ext目录中或-Djava.ext.dirs中参数指定的路径中的jar包和class文件

Application ClassLoader 应用类加载器,也称为系统类加载器(System ClassLoader,可由java.lang.ClassLoader.getSystemClassLoader()获取),实现为sun.misc.Launcher$AppClassLoader,由启动类加载器加载,负责加载当前应用classpath下的所有类

双亲委派模型

java语言系统有众多类加载器,包括用户自定义类加载器,各加载器之间的加载顺序如何?首先从JVM入口应用sun.misc.Launcher聊起

Launcher
public Launcher() {
    ExtClassLoader localExtClassLoader;
    try {
        // 加载扩展类加载器
        localExtClassLoader = ExtClassLoader.getExtClassLoader();
    } catch (IOException localIOException1) {
        throw new InternalError("Could not create extension class loader", localIOException1);
    }
    try {
        // 加载应用类加载器
        this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
    } catch (IOException localIOException2) {
        throw new InternalError("Could not create application class loader", localIOException2);
    }
    // 设置AppClassLoader为线程上下文类加载器
    Thread.currentThread().setContextClassLoader(this.loader);
    // ...
    
    static class ExtClassLoader extends java.net.URLClassLoader
    static class AppClassLoader extends java.net.URLClassLoader
}

Launcher初始化了ExtClassLoaderAppClassLoader,并将AppClassLoader设置为线程上下文类加载器,同时,初始化AppClassLoader时传入了ExtClassLoader实例,WHY? 这里要写一个大大的问号

ExtClassLoaderAppClassLoader都继承自URLClassLoader,而最终的父类则为ClassLoader

查看源码可以得知,初始化AppClassLoader时传入的ExtClassLoader实例最终设置为了AppClassLoader(ClassLoader)的parent属性,parent属性的作用是什么?

父类加载器

每个类都对应一个加载它的类加载器,我们可以通过程序来验证

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("ClassLodarDemo"s ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("DNSNameService"s ClassLoader is " + DNSNameService.class.getClassLoader());
        System.out.println("String"s ClassLoader is " + String.class.getClassLoader());
    }
}

输出为

ClassLodarDemo"s ClassLoader is sun.misc.Launcher$AppClassLoader@135fbaa4
DNSNameService"s ClassLoader is sun.misc.Launcher$ExtClassLoader@6e0be858
String"s ClassLoader is null

ClassLodarDemo为我们自己创建的类,其类加载器为AppClassLoader
DNSNameService为%JRE_HOME%/lib/ext目录下的类,其类加载器为ExtClassLoader
String存在于rt.jar中,但其类加载器为null,这里是应为rt.jar由Bootstrap ClassLoader加载,而Bootstrap ClassLoader是由C++编写,属于JVM的一部分

每个类加载器,都有一个父类加载器(parent),同样通过程序验证

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("ClassLodarDemo"s ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("The Parent of ClassLodarDemo"s ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
        System.out.println("The GrandParent of ClassLodarDemo"s ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
    }
}

输出为

ClassLodarDemo"s ClassLoader is sun.misc.Launcher$AppClassLoader@135fbaa4
The Parent of ClassLodarDemo"s ClassLoader is sun.misc.Launcher$ExtClassLoader@2503dbd3
The GrandParent of ClassLodarDemo"s ClassLoader is null

AppClassLoader的父类加载器为ExtClassLoader
ExtClassLoader的父类加载器为null,null并不代表ExtClassLoader没有父类加载器,而是Bootstrap ClassLoader

双亲委派

类加载器在加载类或者其他资源时,使用的是如上图所示的双亲委派模型,这种模型要求除了顶层的BootStrap 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 {
                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) {
                // 如果父类加载器无法加载,才尝试加载
                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;
    }
}
破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型,在java的世界中大部分的类加载器都遵循这个模型,但也有例外,一个典型的例子便是JNDI服务。

JNDI存放于rt.jar,由启动类加载器加载,JNDI的目的是对资源进行集中管理和查找,它需要调用由各厂商实现并部署在应用程序ClassPath下的JNDI接口实现的代码,如此一来,在双亲委派模型下,启动类加载器根本无法加载这些代码

针对此问题,java设计团队引入了线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过Thread类的setContextClassLoader进行设置,默认继承父线程类加载器
通过线程上下文类加载器,JNDI服务通过此类加载器,由父类加载器请求子类加载器完成类加载动作

破坏双亲委派模型的例子还有很多,如tomcat服务、osgi、jigsaw等等,是否破坏双亲委派模型并没有对与错,只是不同场景下的具体应用而已

自定义ClassLoader

不论是AppClassLoader还是ExtClassLoader还是启动类加载器,其加载类的路径都是固定的,如果我们需要加载外部类或者资源,如某路径下或网络上,这样便需要自定义类加载器
自定义类加载器,只需要继承ClassLoader类,复写findClass方法,在findClass方法中调用defineClass方法即可

一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader
如果需要制定一个ClassLoader的父类加载器为启动类加载器,只需要将其parent指定为null即可

首先,编写一个测试用的类文件

public class BeLoadedClass {
    public void say() {
        System.out.println("I"m Loaded by " + this.getClass().getClassLoader());
    }
}

将其编译,放入/data/classloader目录下

接下来,编写DiskClassLoader

public class DiskClassLoader extends URLClassLoader {

    public DiskClassLoader(URL path) throws MalformedURLException {
        super(new URL[]{path});
    }

    public DiskClassLoader(URL path, ClassLoader parent) throws MalformedURLException {
        super(new URL[]{path}, parent);
    }
}

这里直接继承URLClassLoader类,该类findClass的实现如下

protected Class findClass(final String name) throws ClassNotFoundException {
    final Class result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction>() {
                public Class run() throws ClassNotFoundException {
                    // 类文件全路径
                    String path = name.replace(".", "/").concat(".class");
                    // 指定资源目录下查找
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            // 调用defineClass生成类
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

编写测试程序

public class ClassLoaderDemo {
    public static void main(String[] args) throws Exception {
        URL path = new File("/data/classloader").toURI().toURL();

        DiskClassLoader diskClassLoaderA = new DiskClassLoader(path);
        Class clazzA = diskClassLoaderA.loadClass("BeLoadedClass");
        Method sayA = clazzA.getMethod("say");
        Object instanceA = clazzA.newInstance();
        sayA.invoke(instanceA);
        System.out.println(diskClassLoaderA);
        System.out.println("clazzA@" + clazzA.hashCode());

        System.out.println("====");

        DiskClassLoader diskClassLoaderB = new DiskClassLoader(path, diskClassLoaderA);
        Class clazzB = diskClassLoaderB.loadClass("BeLoadedClass");
        Method sayB = clazzB.getMethod("say");
        Object instanceB = clazzA.newInstance();
        sayB.invoke(instanceB);
        System.out.println(diskClassLoaderB);
        System.out.println("clazzB@" + clazzB.hashCode());

        System.out.println("====");

        DiskClassLoader diskClassLoaderC = new DiskClassLoader(path);
        Class clazzC = diskClassLoaderC.loadClass("BeLoadedClass");
        Method sayC = clazzC.getMethod("say");
        Object instanceC = clazzC.newInstance();
        sayC.invoke(instanceC);
        System.out.println(diskClassLoaderC);
        System.out.println("clazzC@" + clazzC.hashCode());

        System.out.println("====");

        System.out.println("clazzA == clazzB " + (clazzA == clazzB));
        System.out.println("clazzC == clazzB " + (clazzC == clazzB));
    }
}

输出为

I"m Loaded by com.manerfan.jvm.oom.DiskClassLoader@4b67cf4d
com.manerfan.jvm.DiskClassLoader@4b67cf4d
clazzA@312714112
====
I"m Loaded by com.manerfan.jvm.oom.DiskClassLoader@4b67cf4d
com.manerfan.jvm.DiskClassLoader@29453f44
clazzB@312714112
====
I"m Loaded by com.manerfan.jvm.oom.DiskClassLoader@5cad8086
com.manerfan.jvm.DiskClassLoader@5cad8086
clazzC@1639705018
====
clazzA == clazzB true
clazzC == clazzB false

这里我们定义了3个ClassLoader,类搜索路径均为/data/classloader,其中diskClassLoaderB的父类加载器为diskClassLoaderA
clazzA clazzB clazzC 分别由 diskClassLoaderA diskClassLoaderB diskClassLoaderC 加载
instanceA instanceB instanceA 分别由 clazzA clazzB clazzC 创建

从输出可以看出,instanceA及instanceB所对应的class均由diskClassLoaderA加载
虽然diskClassLoaderA与diskClassLoaderB为两个不同的类加载器,但由于diskClassLoaderB的父类加载器为diskClassLoaderA,从输出结果可以看出,clazzA与clazzB完全相同(包括内存地址),这也说明clazzA与clazzB均由同一类加载器加载
而instanceC所对应的class,classC却是由diskClassLoaderC加载

这个例子,能更好的帮助理解双亲委派模型

类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间

如上例,虽然classA(classB)与classC的类路径相同,但由于被不同的类加载器加载,其却属于两个不同的类名称空间

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68622.html

相关文章

  • 修炼内功】[JVM] 虚拟机栈及字节码基础

    摘要:本文已收录修炼内功跃迁之路在浅谈虚拟机内存模型一文中有简单介绍过,虚拟机栈是线程私有的,每个方法在执行的同时都会创建一个栈帧,方法执行时栈帧入栈,方法结束时栈帧出栈,虚拟机中栈帧的入栈顺序就是方法的调用顺序写了很多文字,但都不尽如意,十分惭 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbtSi5?w=1654&h=96...

    VEIGHTZ 评论0 收藏0
  • 修炼内功】[JVM] 类文件结构

    摘要:本文已收录修炼内功跃迁之路学习语言的时候,需要在不同的目标操作系统上或者使用交叉编译环境,使用正确的指令集编译成对应操作系统可运行的执行文件,才可以在相应的系统上运行,如果使用操作系统差异性的库或者接口,还需要针对不同的系统做不同的处理宏的 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbtpPd?w=2065&h=11...

    Eminjannn 评论0 收藏0
  • 修炼内功】[JVM] 浅谈虚拟机内存模型

    摘要:也正是因此,一旦出现内存泄漏或溢出问题,如果不了解的内存管理原理,那么将会对问题的排查带来极大的困难。 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbsP9I?w=1024&h=580); 不论做技术还是做业务,对于Java开发人员来讲,理解JVM各种原理的重要性不必再多言 对于C/C++而言,可以轻易地操作任意地址的...

    sanyang 评论0 收藏0
  • 修炼内功】[Java8] Lambda究竟是不是匿名类的语法糖

    摘要:本文已收录修炼内功跃迁之路初次接触的时候感觉表达式很神奇表达式带来的编程新思路,但又总感觉它就是匿名类或者内部类的语法糖而已,只是语法上更为简洁罢了,如同以下的代码匿名类内部类编译后会产生三个文件虽然从使用效果来看,与匿名类或者内部类有相 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbui4o?w=800&h=600)...

    ?xiaoxiao, 评论0 收藏0
  • 修炼内功】[JVM] 虚拟机视角的方法调用

    摘要:本文已收录修炼内功跃迁之路我们写的方法在被编译为文件后是如何被虚拟机执行的对于重写或者重载的方法,是在编译阶段就确定具体方法的么如果不是,虚拟机在运行时又是如何确定具体方法的方法调用不等于方法执行,一切方法调用在文件中都只是常量池中的符号引 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbuesq?w=2114&h=12...

    shevy 评论0 收藏0

发表评论

0条评论

荆兆峰

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<