类加载器

写在前面

本文作为阅读了周志明作者的 <<深入理解Java虚拟机>> 的读书笔记,同时,也结合了 SE 8 的 JAVA 虚拟机规范。

类加载阶段的 ”通过一个类的全限定名来获取描述此类的二进制字节流“ 这个动作是放在 JVM 外部去实现的,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块被称为 “类加载器” 。

文档上描述为只有两种类装载器:JVM 提供的引导类装载器和用户定义的类装载器。并且每个用户定义的类装载器都是抽象类 ClassLoader 的子类。应用程序使用用户定义的类装载器,能够拓展JVM 动态装载并创建类

从开发人员的角度来看,类加载器就划分的细致一些了,分为启动类加载器,拓展类加载器,应用程序类加载器。其中,启动类加载器无法被 Java 程序直接引用。


类与类加载器

类加载器虽然只用于实现类的加载动作,但它在 Java 程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性。

这在文档中被称为同一运行时包,运行时包可以描述为 <N, L> ,其中 N 代表接口或类,L 代表类装载器。

注意这里的类装载器指的是定义装载器,参考下面来自文档的这段话;

类装载器L可以通过直接定义C或委托给另一个类装载器来创建C。如果L直接创建了C,我们说L定义了C,或者说,L是C的定义加载器。

当一个类装器委托给另一个类装载器时,发起装入的载入器不一定是完成装入并定义类的装载器。如果L通过直接定义或委托创建C,我们说L启动C的加载,或者等价地说,L是C的初始加载器。

通俗来讲,比较两个类是否 “相等” ,只有在这两个类是由同一个类加载器加载的前提下之下才有意义,否则,即使这两个类是来源于同一个 Class 文件,只要加载它们的类加载器不同,那这两个类就必不相等。

这里的 “ 相等” , 包括代表类的 Class 对象的 equals 方法、isAssignableFrom 方法、isInstance 方法的返回结果,也包括使用了 instanceof 关键字做对象所属关系判定等情况。

下面的代码演示参考自书中:

public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader classLoader = new ClassLoader(){
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                if(name.startsWith("java.lang")){
                    return getParent().loadClass(name);
                }
                String fileName = name.substring(name.lastIndexOf('.') + 1) + ".class";
                InputStream inputStream = getClass().getResourceAsStream(fileName);
                byte[] bytes = new byte[2048 * 5];
                int len = 2048 * 5;
                try {
                    len = inputStream.read(bytes);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return defineClass(name, bytes, 0, len);
            }
        };
        Class<ClassLoaderTest> clt1 = (Class<ClassLoaderTest>) classLoader.loadClass("com.duofei.classloader.ClassLoaderTest");
        System.out.println("clt1.newInstance instanceof ClassLoaderTest : " + (clt1.newInstance() instanceof ClassLoaderTest));
        System.out.println("clt1.equals(ClassLoaderTest.class) : " + clt1.equals(ClassLoaderTest.class));
    }
}
/**
* 运行结果:
* clt1.newInstance instanceof ClassLoaderTest : false
* clt1.equals(ClassLoaderTest.class) : false
*/

文档中还提到一种情形:在访问控制时,也需要是相同的运行时包,才能够互相访问。

改动上面代码:

	public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
        // 省略了未变动的代码
        Class<ClassLoaderTest> clt1 = (Class<ClassLoaderTest>) classLoader.loadClass("com.duofei.classloader.ClassLoaderTest");
        Method invoke = clt1.getMethod("invokeTest1");
        invoke.invoke(clt1.newInstance());
    }
	public void invokeTest1(){
        Test1 test1 = new Test1();
        test1.invoke();
        System.out.println("test1 invoke complete!");
    }

    public static class Test1{
        public void invoke(){
            System.out.println("i am invoked!");
        }
    }

其实通过调用 invokeTest1 方法,可以知道,虚拟机会去使用加载了 ClassLoaderTest 的类记载器去加载 Test1 类,我上面所写的代码当然是不能加载成功的,所以我将类加载器的代码做了一些调整,整个代码现在如下:

public class ClassLoaderTest {

    protected static Test1 test1 ;

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
        ClassLoader classLoader = new ClassLoader(){
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                if(name.startsWith("java.lang")){
                    return getParent().loadClass(name);
                }
                String fileName = name.substring(name.lastIndexOf('.') + 1) + ".class";
                InputStream inputStream = getClass().getResourceAsStream(fileName);
                if(inputStream == null || name.contains("$")){
                    return getParent().loadClass(name);
                }
                byte[] bytes = new byte[2048 * 5];
                int len = 2048 * 5;
                try {
                    len = inputStream.read(bytes);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return defineClass(name, bytes, 0, len);
            }
        };
        Class<ClassLoaderTest> clt1 = (Class<ClassLoaderTest>) classLoader.loadClass("com.duofei.classloader.ClassLoaderTest");
        System.out.println("clt1.newInstance instanceof ClassLoaderTest : " + (clt1.newInstance() instanceof ClassLoaderTest));
        System.out.println("clt1.equals(ClassLoaderTest.class) : " + clt1.equals(ClassLoaderTest.class));
        test1 = (Test1) ClassLoaderTest.class.getClassLoader().loadClass("com.duofei.classloader.ClassLoaderTest$Test1").newInstance();
        Method invoke = clt1.getMethod("invokeTest1");
        invoke.invoke(clt1.newInstance());
    }

    public void invokeTest1(){
        test1.invoke();
        System.out.println("test1 invoke complete!");
    }

    public static class Test1{
        public void invoke(){
            System.out.println("i am invoked!");
        }
    }
}

最终运行的结果会得到一个空指针异常,这发生在 invokeTest1 方法中。其实,可以发现,我在调用该方法的时候,已经为 test1 赋值了。但在断点调试时发现,即便如此,在 invokeTest1 执行的时候,仍然会去调用我自定义的类加载器来加载这个 Test1。也就是说一个类的静态成员只有在相同的类加载器下才是有效的。文档中描述的访问控制,我很难去模拟到,因为它总是自动的使用相同的类加载器去加载,这里我还有点困惑。


双亲委派模型

上文提到过的三种类加载器:启动类加载器,拓展类加载器,应用程序类加载器,在应用程序中,一般都是它们互相配合进行加载的,如果有必要的话,还可以加入自己定义的类加载器。

下面的图是参考书上所画:双亲委派模型
图中所展示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承方式来实现,而都是使用组合的关系来复用父加载器的代码。

其实,组合的关系更优于继承,因为受限于 Java 单继承的缘故;

双亲委派的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求时,子加载器才会尝试自己去加载。

双亲委派模型对于保证 Java 程序的稳定运作很重要。


破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型,而是 Java 设计者们推荐给开发者们的类加载器实现方式。大部分的类加载器都遵循这个模型,但也有例外的情况。

双亲委派很好地解决了各个类加载器的基础类的同一问题,但如果基础类又要调用回用户的代码,那该怎么办呢?为了解决这个困境,引入了一个不太优雅的设计:线程上下文类加载器。这个类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置。

本来还想写一下详细的案例的,但在搜索资料的过程中,发现这篇文章写的很好(真正理解线程上下文类加载器(多案例分析)),所以,我也就没有必要再写了。

不过文章中,评论第一条讨论的蛮激烈的,这里也写下自己的看法。

问题:spi 接口 和 实现类。 不就跟object类 和 其他子类 是一样的道理吗? 有什么特殊的吗?其他的都可以。为什么说spi就不行?非要一个线程上下文加载器?

个人见解:首先需要明白 spi 需要用到 ServiceLoader 这个类,而这个类处于原生的包中(/JAVA_HOME/lib 下),所以,这个类就只能被引导类加载器加载。而根据类加载的原则(加载类的时候,会默认使用当前类的类加载器去加载),也就是说这里会使用一个引导类加载器去加载 Person(文中提到的用户自定义的类),这是没法加载到的。根据双亲委派原则,自己先委托父类找,父类找不到,再反过来自己找。而这个地方作为最顶层了,它就只能自己找,但又因为引导类加载器只负责加载 lib 目录下的,所以,这里就肯定找不到了,这是第一个矛盾点。

为了解决这个问题,那又不得不需要打破这种层级的概念,也就是说,我顶层的类加载器也想委托给底层的类加载器(但我不知道是哪一层)来加载某些类。你想想能做到这件本身就不太优雅的事的方式能有几种?哪种才是更能为人所接受呢?

方法1:你可以在 java 原生包中找到一个地方(或者新建类),定义一个全局的静态成员,这个静态成员存储了我需要的这么一个类加载器。这可以解决问题,但这只能指定一个,也就是整个应用程序都用这一个,这肯定不行。因为你不能假设你所使用的所有 ServiceLoader 就需要这么一个类加载器。

方法2:那就你自己调用 ServiceLoader 的时候, 自己去指定好了,所以,你会发现 ServiceLoader 只有一个私有的构造器,它要求传参 Classloader

	private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

并且它提供的静态方法,也是支持传参 ClassLoader 的。所以,这里是不是线程上下文的类加载器已经不重要了,它已经能够做到”从指定的类加载器去加载类“这件事了。

但是,事情不可能这样就完了,客户端用起来很不好啊,我哪能每次都去找这么一个啊,所以呢,JVM 团队就有理由认为,你在一个线程里所使用到的类应该是比较通用的,那我不如就在线程这个类里放一个吧( ThreadLocal 不也放在里面的嘛?,Thread 这个类简直是生命不能承受之重啊!)。这些分析呢,我也找不到资料证明,仁者见仁智者见智嘛,欢迎讨论哈。

Thread 类中的加载器也可以自己指定,如果自己不指定的话,会从父线程中继承一个,如果全局范围内都没有设置过的话,那么这个类加载器就会是应用程序类加载器了,也就是上面构造函数代码中的 ClassLoader.getSystemClassLoader 所返回的值了。


我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!


比心

如果觉得这篇文章对你有所帮助,动动小手,点点赞,这将使我能够为你带来更好的文章。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值