深入理解Java虚拟机系列 --03JVM类加载子系统(下) -- 类加载器, 双亲委派机制,沙箱安全机制详解

在这里插入图片描述在这里插入图片描述
因为热爱所以坚持,因为热爱所以等待。熬过漫长无戏可演的日子,终于换来了人生的春天,共勉!!!

1.类加载器

  • ①.ClassLoader的作用

    1.ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作、因此,ClassLoader在整个装载(加载)阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定

    2.类加载器最早出现在Java1.0版本中,那个时候只是单纯地为了满足Java Applet应用而研发出来。但如今类加载器却在OSGI(热部署)、字节码加密解密领域大放异彩。这主要归功于Java虚拟机的设计者当初在设计类加载器的时候,并没有考虑将它绑定在Jvm内部,这样做的好处就是能够更加灵活和动态地执行类加载操作
    https://img-blog.csdnimg.cn/20210507205510198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RaODQ1MTk1NDg1,size_16,color_FFFFFF,t_70

  • ②. class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式(在日常开发以上两种方式一般会混合使用)

    1.显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象

    2.隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。比如 new User()

类的加载器分类

①. 分类

  1. JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)自定义类加载器(User-Defined ClassLoader)

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

  3. 无论类加载器的类型如何划分,在程序中我们常见的类加载器如下所示:
    除了顶层的启动类加载器外,其余的类加载器都应当有自己的"父类"加载器
    在这里插入图片描述

启动(引导)类加载器 Bootstrap

①. 这个类加载使用C/C++语言实现的,嵌套在JVM内部

②. 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sum.boot.class.path路径下的内容),用于提供JVM自身需要的类(String类就是使用的这个类加载器)

③. 由于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

④. 并不继承自java.lang.ClassLoader,没有父加载器

⑤. 加载扩展类和应用程序类加载器,并指定为他们的父类加载器

扩展类加载器 Extension

①. Java语言编写,sum.music.Launcher$ExtClassLoader实现

②. 派生于ClassLoader类,父类加载器为启动类加载

③. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

应用程序(系统)类加载器 AppClassLoader

①. java语言编写,由sum.misc.Launcher$AppClassLoader实现

②. 派生于ClassLoader类,父类加载器为扩展类加载器

③. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库

④. 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载

⑤. 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器

用户自定义类加载器

①. 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们换可以自定义类加载器,来定制类的加载方式(自定义类加载器通常需要继承于 ClassLoader)

②. 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java 开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源

③. 自定义 ClassLoader 的子类时候,我们常见的会有两种做法:

  • 重写loadClass()方法(不推荐,这个方法会保证类的双亲委派机制)

  • 重写findClass()方法 -->推荐

    这两种方法本质上差不多,毕竟loadClass()也会调用findClass(),但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。

手写一个简单的自定义加载器

public class UserClassLoader extends ClassLoader {
    
    
    private String rootDir;

    public UserClassLoader(String rootDir) {
    
    
        this.rootDir = rootDir;
    }

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

    /**
     * 编写获取class文件并转换为字节码流的逻辑 * @param className * @return
     */
    private byte[] getClassData(String className) {
    
    
        // 读取类文件的字节
        String path = classNameToPath(className);
        try {
    
    
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            // 读取类文件的字节码
            while ((len = ins.read(buffer)) != -1) {
    
    
                baos.write(buffer, 0, len);
            }
            return baos.toByteArray();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 类文件的完全路径
     */
    private String classNameToPath(String className) {
    
    
        return rootDir + "\\" + className.replace('.', '\\') + ".class";
    }

    public static void main(String[] args) {
    
    
        String rootDir = "D:\\code\\workspace_teach\\JVMdachang210416\\chapter02_classload\\src\\";

        try {
    
    
            //创建自定义的类的加载器1
            UserClassLoader loader1 = new UserClassLoader(rootDir);
            Class clazz1 = loader1.findClass("com.xiaozhi.java3.User");

            //创建自定义的类的加载器2
            UserClassLoader loader2 = new UserClassLoader(rootDir);
            Class clazz2 = loader2.findClass("com.xiaozhi.java3.User");
            //clazz1与clazz2对应了不同的类模板结构
            System.out.println(clazz1 == clazz2); 
            System.out.println(clazz1.getClassLoader());
            System.out.println(clazz2.getClassLoader());
            
            Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("com.xiaozhi.java3.User");
            System.out.println(clazz3.getClassLoader());
            System.out.println(clazz1.getClassLoader().getParent());

        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }
    }
}

2.双亲委派机制

①. 工作原理

1.如果一个类加载收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行

2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器

3.如果父类的加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

在这里插入图片描述
②. 本质(规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载)

在这里插入图片描述
③. 源码分析(双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)接口中体现。该接口的逻辑如下)

1.先在当前加载器的缓存中查找有无目标类,如果有,直接返回。

2.判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name, false)接口进行加载

3.反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassOrNull(name)接口,让引导类加载器进行加载

4.如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lan g.ClassLoader接口的defineClass系列的native接口加载目标Java类。

5.双亲委派的模型就隐藏在这第2和第3步中

④. 双亲委派机制优势:

1.避免类的重复加载,确保一个类的全局唯一性(当父ClassLoader已经加载了该类的时候,就没有必要子ClassLoader再加载一次)

2.保护程序安全,防止核心API被随意篡改
(自定义类:java.lang.String | java.lang.ShkStart)
在这里插入图片描述

⑤. 双亲委托模式的弊端
(检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类)

⑥. 结论:由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法

⑦. 破坏双亲委派机制及举例

1.双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的
“远古”时代

2.第二次破坏双亲委派机制:线程上下文类加载器(ClassLoader.getSystemClassLoader( ))

3.双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)
在这里插入图片描述

3.沙箱安全机制

①. 如图,虽然我们自定义了一个java.lang包下的String尝试覆盖核心类库中的String,但是由于双亲委派机制,启动加载器会加载java核心类库的String类(BootStrap启动类加载器只加载包名为java、javax、sun等开头的类),而核心类库中的String并没有main方法
在这里插入图片描述
②. 自定义String类,但是在加载自定义String类的时候回率先使用引导类加载器加载,而引导类加载器在加载过程中会先加载jdk自带的文件(rt.jar包中的java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制

③. 沙箱安全机制作用:

1.保证程序安全

2.保护Java原生的JDK代码

在这里插入图片描述在这里插入图片描述

参考视频 : 尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)
参考书籍 : 深入理解Java虚拟机

猜你喜欢

转载自blog.csdn.net/qq_43295483/article/details/119986724