0x01 概念

Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中,用于加载系统、网络或者其他来源的类文件。Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。

0x02 类文件编译流程图

我们以上图为例子,比如我们创建一个ClassLoaderTest.java文件运行,经过javac编译,然后生成ClassLoaderTest.class文件。这个java文件和生成的class文件都是存储在我们的磁盘当中。但如果我们需要将磁盘中的class文件在java虚拟机内存中运行,需要经过一系列的类的生命周期(加载、连接(验证-->准备-->解析)和初始化操作,最后就是我们的java虚拟机内存使用自身方法区中字节码二进制数据去引用堆区的Class对象。

通过这个流程图,我们就很清楚地了解到类的加载就是由java类加载器实现的,作用将类文件进行动态加载到java虚拟机内存中运行。

0x03 应用场景

1、资源隔离

实现不同项目或者同一个项目上的不同版本的jar包隔离,避免集群错误或者冲突的产生。

2、热部署

对于Java应用程序来说,热部署就是在运行时更新Java类文件。在基于Java的应用服务器实现热部署的过程中,类装入器扮演着重要的角色。类装入器不能重新装入一个已经装入的类,但只要使用一个新的类装入器实例,就可以将类再次装入一个正在运行的应用程序。这样我们应用正在运行的时候升级软件,却不需要重新启动应用。

3、代码保护

我们可以对字节码文件进行加密,然后再使用特定的ClassLoader解密文件内容,再加载这些字节码文件。这样就能够实现对我们的代码项目加密保护,别人无法进行反编译查看源代码信息。

0x04 类加载器分类

在简单了解的类加载器的概念作用之后,我们可以看看java中类加载器的分类。类加载器大致分为两种,一种是JVM自带的类加载器,分别为引导类加载器、扩展类加载器和系统类加载器。另外一种就是用户自定义的类加载器,可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。

4.1 JVM默认类加载器

4.1.1 引导类加载器(BootstrapClassLoader)

引导类加载器(BootstrapClassLoader),底层原生代码是C++语言编写,属于jvm一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中。(同时处于安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类)。

这里以我的电脑中jdk1.7为例,这里我们看到/jre/lib/rt.jar目录,这里面的类都是由BootstrapClassLoader来加载。

之后我们使用Object类为例,看看其是否存在父加载器。因为Object类是所有子类的父类,归属于BootstrapClassLoader。发现并没有父类加载器,结果为null。

4.1.2 扩展类加载器(ExtensionsClassLoader)

扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载java的扩展库。Java虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载java类。

以我的电脑为例,这个目录下的jar包都是由ExtensionsClassLoader进行加载。

这里我们可以选择一个zipfs.jar包,然后查看下其jar中的ZipPath类加载器,发现存在于ExtensionsClassLoader。

4.1.3 App类加载器/系统类加载器(AppClassLoader)

App类加载器/系统类加载器(AppClassLoader),由sun.misc.Launcher$AppClassLoader实现,一般通过通过(java.class.path或者Classpath环境变量)来加载Java类,也就是我们常说的classpath路径。通常我们是使用这个加载类来加载Java应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。

这里我们使用本身写的ClassLoaderTest类进行测试,发现存在于AppClassLoader。

4.2 自定义类加载器(UserDefineClassLoader)

自定义类加载器(UserDefineClassLoader),除了上述java自带提供的类加载器,我们还可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。具体实现方法我们等下单独讲解。

0x05 双亲委派机制

通常情况下,我们就可以使用JVM默认三种类加载器进行相互配合使用,且是按需加载方式,就是我们需要使用该类的时候,才会将生成的class文件加载到内存当中生成class对象进行使用,且加载过程使用的是双亲委派模式,及把需要加载的类交由父加载器进行处理。

概念有点绕,我们可以先看类加载器委派关系

如上图类加载器层次关系,我们可以将其称为类加载器的双亲委派模型。但注意的是,他们之间并不是"继承"体系,而是委派体系。当上述特定的类加载器接到加载类的请求时,首先会先将任务委托给父类加载器,接着请求父类加载这个类,当父类加载器无法加载时(其目录搜素范围没有找到所需要的类时),子类加载器才会进行加载使用。这样可以避免有些类被重复加载。

5.1 双亲委派好处

1、这样就是能够实现有些类避免重复加载使用,直接先给父加载器加载,不用子加载器再次重复加载。

2、保证java核心库的类型安全。比如网络上传输了一个java.lang.Object类,通过双亲模式传递到启动类当中,然后发现其Object类早已被加载过,所以就不会加载这个网络传输过来的java.lang.Object类,保证我们的java核心API库不被篡改,出现类似用户自定义java.lang.Object类的情况。

假设我们创建一个java.lang包,自定义一个TestObject类,但由于双亲委派机制的存在,是不允许加载运行的。

CLassLoader类核心方法

除了上述BootstrapClassLoader,其他类加载器都是继承了CLassLoader类,我们就一起看看其类的核心方法。以下代码都是截取了其方法的源码。

1、loadClass:加载指定的java类

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();
                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) {
                    // 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;
        }

在loadClass方法中,它先使用了findLoadedClass(String)方法来检查这个类是否被加载过。

接着使用父加载器调用loadClass(String)方法,如果父加载器为null,类加载器加载jvm内置的加载器。

点击收藏 | 8 关注 | 4
登录 后跟帖