深入理解Java虚拟机二 虚拟机类加载机制

2019-04-09  本文已影响0人  Cloud_Leung

前言

文章是看了《深入理解Java虚拟机》书后进行的整理和总结,算是一个读书笔记吧。

一、类加载过程

在java语言里,类型的加载、连接和初始化过程都是在程序运行期完成的,赭红设计方式虽然会让类在加载时有一定的性能开销,但是能够为java提供高度的灵活性,java能够动态扩展就是依赖运行期动态加载和动态连接实现的,例如比较出名的OSGi技术也是利用类的动态加载机制来实现的,当然这里不细讲OSGi,因为我也不太懂.

二、类加载器

虚拟机设计团队把类加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器。

类加载器是java语言的一项创新,并且现在在OSGi、热部署、代码加密等领域大方异彩,是java技术中的一个重要基石。

每个类加载器都有自己的命名空间,对于任意一个类,都需要它的类加载器和这个类本身一同确立起在java虚拟机中的唯一性。这代表说即使是同一个类,使用不同的类加载器来加载,这两个类也是不同的

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 {
                String file = name.substring(name.lastIndexOf(".") + 1) + ".class";
                try (InputStream is = ClassLoaderTest.class.getResourceAsStream(file)) {
                    if(Objects.isNull(is)){
                        return super.loadClass(name);
                    }
                    byte[] bytes = new byte[is.available()];
                    int i = is.read(bytes);
                    return defineClass(name, bytes, 0, bytes.length);
                } catch (Exception e) {
                    throw new ClassNotFoundException(file);
                }
            }
        };
        Object o = classLoader.loadClass(ClassLoaderTest.class.getName()).newInstance();
        ClassLoaderTest test = new ClassLoaderTest();

        System.out.println(o.getClass().getName());
        System.out.println(o instanceof ClassLoaderTest);
        System.out.println(test instanceof ClassLoaderTest);
    }

}
cn.learn.test.design.factory.classloader.ClassLoaderTest
false
true

上面这个例子是用我们自己写的类加载器去加载了一个类 然后和虚拟机自己加载的这个类进行比较,发现系统确实认为他们不相同。

实际上ClassLoaderTest这个类本身已经被另一个应用类加载器加载了,这里就要带入java的类加载器结构。

双亲委派模型

从java虚拟机角度来说,只存在两种类加载器,一种是启动类加载器,一种是其他类加载器。
启动类加载器是虚拟机内部实现的一部分。
而其他类加载器则是由java实现,独立于虚拟机外部。

类加载器结构图

我们的程序就是由这三类类加载器配合进行加载的。
双亲委派模型要求除了顶层类加载器,其余的类加载器都有自己的父类类加载器。
双亲委派的工作过程是:如果一个类加载器收到类加载请求,它首先不会自己去尝试加载这个类加载器,而是把请求委派给父类加载器去加载,每一个层次都是如此,因此每一个类加载请求最终都会被委派到最顶层的类加载器来处理,只有当父加载器反馈自己无法完成这个请求,子类加载器才会尝试自己去加载。
这样做的好处是,类加载器具备一种优先级的层次关系,无论哪个类被请求加载,最终都只会被一个固定的类加载器来加载,从而保证这个类在虚拟中的唯一性,前面说了,不同类加载器有不同的命名空间,这样也就保证了系统的安全性。

破坏双亲委派模型

双亲委派模型并不是一个强制约束,而是Java设计者推荐给开发者的类加载方式,在通常情况下,这是最好的选择。
但既然设计为非强制约束,就代表着这种约束有被破坏的需求,双亲委派模型有两种知名的破坏场景。

JNDI服务是由启动类加载器去加载,但JNDI是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序ClassPath下的JNDI接口提供者的代码,但启动类加载的路径下不可能会有这样的代码,为了解决这个问题,java设计团队引入一个新的设计:线程上下文类加载器。这个类加载器保存在Thread类里,打开这个类我们能够看到这个变量:

    /* The context ClassLoader for this thread */
    private ClassLoader contextClassLoader;

创建线程时可以选择手动设置一个类加载器进去,如果不设置,系统会默认从父类线程继承一个,如果全局都没继承过,这个类加载器默认就是应用程序类加载器。有了这个类加载器,JNDI服务就可以在启动类加载器需要加载JNDI接口服务(SPI)时,直接调用线程的上下文加载器来加载。
JDBC的驱动类就是这样实现的,早期我们应该还记得需要创建JDBC连接时需要手动调用Class.forName("xxxDriver"),但现在已经不用了,这是因为现在都已经实现了SPI接口服务。

还有一个OSGi模块化编程的话,我也不够了解,只了解一些基本概念,未来我也会把这个列为学习计划。
OSGi的理念就是把程序做成一个可以热插拔的组件,就像我们的鼠标一样,插上就能用,拔掉就可以换上另一个鼠标,而电脑从来都不需要关机。
OSGi的理念也是这样,让我们的程序在不停机的情况下就能实现功能的替换,只需要我们把我们应用的功能做成一个个可以热插拔的模块即可。
而要实现这样的功能就必然需要依赖类加载器来实现。
在OSGi的环境中,类加载器不再是双亲委派模型的树形结构,而是进一步发展的网状接口,刚才我们提到了一个个模块的热插拔,但是这一个个模块之间也可能存在依赖,这就导致我们需要的类可能需要由其他模块(Bundle)来加载,这种模块的依赖关系实际上也就是类加载器的依赖关系。
这里的东西也就不细讲了,实在是我也不太懂,未来学习后再来分享。

上一篇 下一篇

猜你喜欢

热点阅读