Java类加载器

类加载器

一、类加载器概述

1、类加载器的作用

类加载器:负责将.class文件加载到内存中,并为之生成对应的Class对象(是JVM执行类加载机制的前提)

  • ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的。
  • ClassLoader是否可以运行,则由Execution Engine决定。
  • ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例,然后交给Java虚拟机进行链接、初始化等操作。

因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。

在这里插入图片描述

2、类加载的分类

类的加载分类:显式加载 vs 隐式加载(即JVM加载class文件到内存的方式)

  • 显示加载:在代码中显示调用ClassLoader加载class对象

    如直接使用 Class.forName(name) 或 this.getClass().getClassLoader().loadClass() 加载class对象。

  • 隐式加载:通过虚拟机自动加载到内存中,是不直接在代码中调用ClassLoader的方法加载class对象。

    如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

在日常开发以上两种方式一般会混合使用。

// 隐式加载
User user = new User();
// 显式加载,并初始化
Class clazz = Class.forName("com.test.java.User");
// 显式加载,但不初始化
ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent"); 

3、类加载器的必要性

一般情况下,Java开发人员并不需要在程序中显式地使用类加载器,但是了解类加载器的加载机制却显得至关重要。从以下几个方面说:

  • 避免在开发中遇到 java.lang.ClassNotFoundException异常 或 java.lang.NoClassDefFoundError 异常时,手足无措。

    只有了解类加载器的加载机制,才能够在出现异常的时候 快速地根据错误异常日志定位问题和解决问题。

  • 需要支持类的动态加载 或 需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了。

  • 开发人员可以 在程序中编写自定义类加载器 来重新定义类的加载规则,以便实现一些自定义的处理逻辑。

4、命名空间

每个类加载器都有自己的命名空间,命名空间 由 该类加载器加载的类 及其 父类加载器加载的类 组成

  • 父类加载器 加载的类 对 子类加载器 可见,但是反过来 子类加载器 加载的类 对 父类加载器 不可见。
  • 在同一命名空间中,不会出现全限定名相同的两个类。
  • 在不同的命名空间中,有可能会全限定名相同的两个类。

举例:

public class CustomClassLoader extends ClassLoader {

    // 省略自定义类加载器实现...见第七章的代码举例

    public static void main(String[] args) {
        try {
            // 创建自定义的类加载器1
            CustomClassLoader loader1 = new CustomClassLoader();
            Class<?> clazz1 = loader1.findClass("com.test.java.User");

            // 创建自定义的类加载器2
            CustomClassLoader loader2 = new CustomClassLoader();
            Class<?> clazz2 = loader2.findClass("com.test.java.User");

            // clazz1和clazz2对应了不同的类模版结构
            System.out.println(clazz1 == clazz2);           // false
            System.out.println(clazz1.getClassLoader());    // com.test.java.CustomClassLoader@4aa298b7
            System.out.println(clazz2.getClassLoader());    // com.test.java.CustomClassLoader@28d93b30

        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

5、类的唯一性

对于任意一个类,都需要由「加载它的类加载器」和「这个类本身」共同确认其在 J a v a 虚拟机中的唯一性。 \color{red}{对于任意一个类,都需要由「加载它的类加载器」和「这个类本身」共同确认其在Java虚拟机中的唯一性。} 对于任意一个类,都需要由「加载它的类加载器」和「这个类本身」共同确认其在Java虚拟机中的唯一性。

即使两个类源自同一个Class文件,被同一个JVM加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

因此,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。

二、类加载器的分类

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(C z ClassLoader)。

有些人可能会有疑问,扩展类加载器应用程序类加载器 是由JDK提供的现成的类加载器,怎么也属于自定义类加载器呢?

  • 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的类加载器。
  • 而Java虚拟机规范中,将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:

在这里插入图片描述

1、启动类加载器/引导类加载器(Bootstrap ClassLoader)

  • C/C++语言实现,嵌套在 JVM 内部。
  • 并不派生于 java.lang.ClassLoader
    • 没有父加载器。
    • 加载 扩展类加载器应用程序类加载器,并指定为他们的父类加载器。
  • 用来加载 Java 的核心类库
    • 加载 <JAVA_HOME>/jre/libresources.jarsun.boot.class.path 路径下的内容,
    • 用于提供 JVM 自身需要的类(ObjectStringClassLoaderException
  • 出于安全考虑,引导类加载器只加载包名为 java、javax、sun 等开头的类

下面我们从代码层面看一下:

public class BootstrapClassLoader {
    public static void main(String[] args) {
        // 引导类加载器加载的类库
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL);
        }

        // 从上边的路径中随意选择一个类,查看其类加载器
        ClassLoader classLoader1 = Object.class.getClassLoader();
        System.out.println(classLoader1); // null
        
        // 这里的null并不是说明不需要类加载器,而是因为引导类加载器是用C/C++语言实现的
    }
}
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/cat.jar
file:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/classes

VM options 中 加上 -XX:+TraceClassLoading,就能打印类的加载信息,下面截取其中部分结果展示。

可以看到,启动类加载器会加载 Object、String、ClassLoader、Exception 等核心类。

在这里插入图片描述

还可以看到,启动类加载器还会加载 扩展类加载器应用程序类加载器相关的类

在这里插入图片描述

2、扩展类加载器(Extension ClassLoader)

  • Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现。
  • 派生于 java.lang.ClassLoader
    • 父类加载器为引导类加载器
  • 用来加载 Java的扩展类库
    • 加载 <JAVA_HOME>/lib/ext目录中的 JAR 文件和类
import org.openjsse.sun.security.util.CurveDB;

public class ExtensionClassLoader {
    public static void main(String[] args) {
        // 扩展类加载器加载的类库
        String extDirs = System.getProperty("java.ext.dirs");
        for (String extPath : extDirs.split(":")) {
            System.out.println(extPath);
        }

        // 从上边的路径中随意选择一个类,查看其类加载器
        ClassLoader classLoader2 = CurveDB.class.getClassLoader();
        System.out.println(classLoader2); // sun.misc.Launcher$ExtClassLoader@232204a1
    }
}
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions

3、系统类加载器/应用程序类加载器(Application ClassLoader)

  • Java 语言编写,由 sun.misc.Launcher$AppClassLoader 实现
  • 派生于 java.lang.ClassLoader
    • 父类加载器为扩展类加载器
    • 用户自定义类加载器的默认父类加载器。
    • 是 线程上下文的加载器。
  • 加载 应用程序中的类和资源
    • 自己编写的类、第三方库提供的类、一些其他的资源文件
  • 该类加载是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载
public class ApplicationClassLoader {
    public static void main(String[] args) {
        // 获取应用程序类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 对于自定义的类,查看其类加载器
        ClassLoader classLoader3 = CustomClass.class.getClassLoader();
        System.out.println(classLoader3); // sun.misc.Launcher$AppClassLoader@18b4aac2
    }
}

4、用户自定义类加载器

日常开发中,类的加载几乎是由上述3种类加载器相互配合执行的。必要时,可以自定义类加载器,来定制类的加载方式。

具体内容后续详细展开,这里先了解:

  • 可以通过继承 ClassLoader 并重写 loadClass()findclass() 方法来自定义类加载器。
  • 用户自定义类加载器的父类加载器 默认是 系统类加载器/应用程序类加载器

三、ClassLoader 类

ClassLoader 类是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括引导类加载器)

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/b7581cc518ea43eab1851cb01dc51400.png)			

1、获取三种类加载器

public class GetClassLoader {
    public static void main(String[] args) {
        // 应用程序类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@232204a1

        // 引导类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader); // null
    }
}

2、不同类的类加载器

public class PrintClassLoader {
    public static void main(String[] args) {
        // 线程上下文的加载器(应用程序类加载器)
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        System.out.println(contextClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 自定义类的类加载器(应用程序类加载器)
        ClassLoader customClassClassLoader = CustomClass.class.getClassLoader();
        System.out.println(customClassClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // org.openjsse.sun.security.util.CurveDB的类加载器(扩展类加载器)
        ClassLoader curveDBClassLoader = CurveDB.class.getClassLoader();
        System.out.println(curveDBClassLoader);  // sun.misc.Launcher$ExtClassLoader@4aa298b7

        // String类的类加载器(引导类加载器)
        ClassLoader stringClassLoader = String.class.getClassLoader();
        System.out.println(stringClassLoader); // null
    }

    private static class CustomClass {}
}

3、加载配置文件

类加载器ClassLoader存在一个方法,用来加载src类路径下的文件:

// 获取指定资源的输入流(name就是src中文件的路径)
InputStream getResourceAsStream(String name); 

在src文件中创建一个属性配置文件 db.properties,准备以下数据:

username=root
password=1234

借助类加载器把 src 中 db.properties 配置文件中的数据读取到 Properties 中去:

public class GetResourceByClassLoader {
    public static void main(String[] args) throws IOException {
        // 获取当前类的类加载器
        ClassLoader classLoader = Test.class.getClassLoader();
        // 借助类加载器获取src中的配置文件的输入流
        InputStream inputStream = classLoader.getResourceAsStream("db.properties");
        // 创建属性集对象
        Properties pro = new Properties();
        // 用load方法将流中的数据加载到属性集
        pro.load(is);
        // 打印属性集中的数据
        System.out.println(pro);
    }
}

四、类加载器的关系

1、类加载器的关系

除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

在这里插入图片描述

  • 自定义类加载器 的父类加载器是 系统类加载器
  • 系统类加载器 的父类加载器是 扩展类加载器
  • 扩展类加载器 的父类加载器是 启动类加载器

不同类加载器看似是继承(Inheritance)关系,实际上是包含关系。可以看到,在下层加载器中,包含着上层加载器的引用 parent

public abstract class ClassLoader {
    // 父类加载器
    private final ClassLoader parent;
}

2、Launcher类

public class Launcher {

    public Launcher() {
        // 创建 扩展类加载器
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError("Could not create extension class loader", e);
        }

		// 创建 应用程序类加载器
        try {
            // 这里把ExtClassLoader传进去,最后会赋值给parent
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError("Could not create application class loader", e);
        }

        // 这里可以看到,当前线程使用的类加载器,是 应用程序类加载器
        Thread.currentThread().setContextClassLoader(loader);

        // ...
    }

    static class AppClassLoader extends URLClassLoader {
        /**
         * 「应用程序类加载器」的构造方法
         */
        AppClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent, factory);
            ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
            ucp.initLookupCache(this);
        }
    }

    static class ExtClassLoader extends URLClassLoader {
        /**
         * 「扩展类加载器」的构造方法
         */
        public ExtClassLoader(File[] dirs) throws IOException {
            // 由于引导类加载器是由c/c++实现的,所以这里parent传的是null
            super(getExtURLs(dirs), null, factory);
            SharedSecrets.getJavaNetAccess().
                getURLClassPath(this).initLookupCache(this);
        }
    }

}

Launcher类核心代码可以看出:

  • ExtClassLoaderparentnull
  • AppClassLoaderparentExtClassLoader
  • 当前线程的ClassLoaderAppClassLoader

正是由于子类加载器中包含着父类加载器的引用,所以可以通过子类加载器的方法获取对应的父类加载器。

public class GetClassLoader {
    public static void main(String[] args) {
        // 应用程序类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@232204a1

        // 引导类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader); // null
        
        // 由于引导类加载器是由c/c++实现的,所以这里是null
    }
}

五、双亲委派机制

从JDK1.2开始,类的加载过程采用双亲委派机制,这种机制能更好地保证Java平台的安全。

1、工作原理

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法加载,会下沉到子加载器去加载,一直到最底层;
  4. 如果没有任何加载器能加载,就会抛出ClassNotFoundException

在这里插入图片描述

2、源码分析 - loadClass

双亲委派模型的代码在 java.lang.ClassLoader 类中的 loadClass中实现:

public abstract class ClassLoader {
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 同步操作,保证只加载一次
        synchronized (getClassLoadingLock(name)) {
            // 检查类是否被加载(在当前加载器的缓存中查找)
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 若未加载,递归调用父类加载器的loadClass方法
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // parent==null,则调用引导类加载器的操作
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 若该方法抛出ClassNotFoundException异常,表示父类加载器无法加载
                }
                // 若以上都未成功加载,则调用当前类加载器的findClass方法加载
                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);
            }
            // 若父类加载器可以加载,则直接返回Class对象。
            return c;
        }
    }
}

3、特性

可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。

单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。(如下面的 loader1 和 loader2)

public class CustomClassLoader extends ClassLoader {

    // 省略自定义类加载器实现...见第七章

    public static void main(String[] args) {
        try {
            // 创建自定义的类加载器1
            CustomClassLoader loader1 = new CustomClassLoader();
            Class<?> clazz1 = loader1.findClass("com.test.java.User");

            // 创建自定义的类加载器2
            CustomClassLoader loader2 = new CustomClassLoader();
            Class<?> clazz2 = loader2.findClass("com.test.java.User");

            // clazz1和clazz2对应了不同的类模版结构
            System.out.println(clazz1 == clazz2);           // false
            System.out.println(clazz1.getClassLoader());    // com.test.java.CustomClassLoader@4aa298b7
            System.out.println(clazz2.getClassLoader());    // com.test.java.CustomClassLoader@28d93b30

        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

4、优势

  • 避免类的重复加载,确保一个类的全局唯一性

    • Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载。

      当父亲加载器已经加载了某个类,子类加载器就不会再加载这个类了。

    • 在同一命名空间中,不会出现全限定名相同的两个类。

  • 保护程序安全,防止核心 API 被随意篡改

举例说明:自定义 java.lang.String 并使用,使用的时候会报错!

package java.lang;

public class String {}
package java.lang;

public class StringTest {
    public static void main(String[] args) {
        String str = new String();
    }
}
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:656)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:755)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:419)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:352)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:601)

假设没有双亲委派机制:

  • 自定义的类由系统类加载器加载

    —> 系统类加载器加载 自定义的 java.lang.String

  • 同一命名空间中,不会出现全限定名相同的两个类

    —> 启动类加载器 不会再加载 /jre/lib/rt.jar 下的 java.lang.String

则核心类 java.lang.String 被篡改成了自定义的String类。

5、双亲委派机制的“破坏”

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。

大部分的类加载器都遵循这个模型,但也有例外的情况,主要出现过以下3次“被破坏”的情况。

1)兼容旧代码

由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,

为了兼容已有代码,无法以技术手段避免loadClass()被子类覆盖的可能性。只能在JDK1.2之后的java.lang.ClassLoader
添加一个新的protected方法findClass(),并引导用户自定义时尽可能重写findClass(),而不是loadClass()

分析源码时可以看到,按照loadClass()方法的逻辑,如果父类加载器加载失败,会自动调用自己的findClass()方法完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派机制的。

2)线程上下文类加载器

双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(即越基础的类由越上层的加载器进行加载),但是如果存在基础类型需要调用回用户的代码的情况,那该怎么办呢?(我们之前说过父加载器时无法访问子加载器加载的类型的)

一个典型的例子便是JNDI服务。JNDI是在JDK 1.3时加入到rt.jar的,现在已经是Java的标准服务,它的代码由启动类加载器加载。JNDI提供统一SPI(Service Provider Interface,在Java平台中,通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI)。可由应用层自行实现。而启动类加载器是无法访问这些实现的代码的。

为了解决这个问题,Java的设计团队引入了线程上下文类加载器(Thread Context ClassLoader)的概念。

这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Launcher类)。

这样以线程上下文类加载器为中介,使得启动类加载器中的代码也可以访问系统类加载器中的类。

在这里插入图片描述

不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断。

为了消除这种极不优雅的实现方式,在JDK6时提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。

3)热替换

还有就是由于用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)

IBM公司主导的JSR-291(即OSGIR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGI中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bund1e连同类加载器一起换掉以实现代码的热替换。在OSGI环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构

当收到类加载请求时,OSGI将按照下面的顺序进行类搜索:

  1. 将以java.*开头的类,委派给父类加载器加载。
  2. 否则,将委派列表名单内的类,委派给父类加载器加载。
  3. 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
  6. 否则,查找Dynamic Import列表的Bundle,委派给对应Bund1e的类加载器加载。
  7. 否则,类查找失败。

说明:只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的。

4)小结

双亲委派机制的“破坏”,并不一定是带有贬义的。只要有明确的目的和充分的理由,突破旧有原则无疑是一种创新。

6、热替换

热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。关键需求在于服务不能中断

对Java来说,热替换并非天生就支持,如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重定义这个类。因此,在Java中实现这一功能的一个可行的方法就是灵活运用ClassLoader。

不同ClassLoader加载的同名类属于不同的类型,不能相互转换和兼容。即两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这2个类是完全不同的。

基于这个特点,在程序运行时,只要创建新的ClassLoader去重新加载class文件即可实现热替换。

public class HotReplace {

    public static void main(String[] args) {
       while (true) {
           try {
               // 每个循环都会创建「新的」「自定义类加载器」
               CustomClassLoader customClassLoader = new CustomClassLoader();
               // 加载热替换的类
               Class<?> clazz = customClassLoader.findClass("classLoader.User");
               // 反射调用方法
               Object obj = clazz.newInstance();
               Method method = clazz.getMethod("print");
               method.invoke(obj);
               // 每5s执行一次
               Thread.sleep(5000);
           } catch (Exception e) {
               throw new RuntimeException(e);
           }
       }
    }

}

public class User {
    public void print() {
        System.out.println("old print");
    }
}

这里只要在运行过程中,替换.class文件,就可以实现热替换。(新的ClassLoader实例的命名空间与之前的是隔离的)

六、沙箱安全机制

沙箱安全机制

  • 保证程序安全
  • 保护Java原生的JDK代码

Java安全模型的核心就是Java沙箱(sandbox) \color{red}{} 。什么是沙箱?沙箱是一个限制程序运行的环境。

沙箱机制:将Java代码限定在 JVM 特定的运行范围中,并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。

沙箱主要限制系统资源访问,那系统资源包括什么呢?CPU、内存、文件系统、网络。

不同级别的沙箱对这些资源访问的限制也可以不一样。所有的Java程序运行都可以指定沙箱,定制安全策略。

1、JDK1.0时期

在Java中将执行程序分成本地代码和远程代码两种。

  • 本地代码:默认视为可信任的,可以访问一切本地资源。
  • 远程代码:被看作是不受信的。安全依赖于沙箱(Sandbox)机制

在这里插入图片描述

2、JDK1.1时期

JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。

因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略。允许用户指定远程代码对本地资源的访问权限。

在这里插入图片描述

3、JDK1.2时期

在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。

在这里插入图片描述

4、JDK1.6时期

当前最新的安全机制实现,则引入了**域(Domain)**的概念。虚拟机中不同的域对应不一样的权限,存在于不同域中的类文件就具有了当前域的全部权限。虚拟机会把所有代码加载到不同的 系统域 和 应用域。

  • 系统域:专门负责与关键资源进行交互。
  • 应用域:通过系统域的部分代理来对各种需要的资源进行访问。

在这里插入图片描述

七、自定义类加载器

1、为什么要自定义类加载器?

  • 隔离加载类

    在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。

    比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。

    再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。

  • 修改类加载的方式

    类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,可以根据实际情况在某个时间点按需进行动态加载。

  • 扩展加载源

    比如从数据库、网络、甚至是电视机机顶盒进行加载。

  • 防止源码泄漏

    Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。

2、实现步骤

  1. 开发人员可以通过继承java.lang.ClassLoader的方式,自定义类加载器,以满足一些特殊的需求。

  2. JDK1.2 之前,在自定义类加载器时,总会去继承 ClassLoader 类并重写 loadClass()方法,从而实现自定义的类加载类。

    JDK1.2 之后,不再建议用户去覆盖loadclass()方法,而是建议把自定义的类加载逻辑写在 findClass()方法中。

  3. 编写自定义类加载器时,如果没有太过于复杂的需求,也可以直接继承 URLClassLoader 类,

    这样就可以避免自己去编写 findClass() 方法 及其获取字节码流的方式,使自定义类加载器编写更加简洁。

3、代码举例

public class CustomClassLoader extends ClassLoader {

    /**
     * 加载什么路径下的class
     */
    private String classOutPath = "xxx/xxx/xxx";

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 获取类的class文件字节数组
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        // 直接生成class对象
        return defineClass(name, classData, 0, classData.length);
    }

    /**
     * 获取class文件,并转换为字节码流
     */
    private byte[] getClassData(String className) {
        String classPath = classNameToPath(className);

        try {
            // 读取类文件的字节
            InputStream inputStream = Files.newInputStream(Paths.get(classPath));
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            int len = 0;
            byte[] buffer = new byte[4096];
            // 读取类文件的字节码
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            return outputStream.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private String classNameToPath(String className) {
        return classOutPath + "/" + className.replace(".", "/") + ".class";
    }

    public static void main(String[] args) {
        try {
            // 创建自定义的类加载器1
            CustomClassLoader loader1 = new CustomClassLoader();
            Class<?> clazz1 = loader1.findClass("com.test.java.User");

            // 创建自定义的类加载器2
            CustomClassLoader loader2 = new CustomClassLoader();
            Class<?> clazz2 = loader2.findClass("com.test.java.User");

            System.out.println(clazz1 == clazz2); 			// false
            System.out.println(clazz1.getClassLoader());    // com.test.java.CustomClassLoader@4aa298b7
            System.out.println(clazz2.getClassLoader());    // com.test.java.CustomClassLoader@28d93b30

            // 可以看到,自定义类加载器的父加载器,默认是AppClassLoader
            System.out.println(loader1.getParent()); // sun.misc.Launcher$AppClassLoader@18b4aac2
            System.out.println(loader2.getParent()); // sun.misc.Launcher$AppClassLoader@18b4aac2
            
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

    }

}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

scj1022

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值