JVM16_类的概述分类ClassLoader源码分析自定义类的加载器双亲委派机制沙箱安全机制

Posted TZ845195485

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM16_类的概述分类ClassLoader源码分析自定义类的加载器双亲委派机制沙箱安全机制相关的知识,希望对你有一定的参考价值。

①. 类的加载器

  • ①. 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内部,这样做的好处就是能够更加灵活和动态地执行类加载操作

在这里插入图片描述

  • ②. class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式(在日常开发以上两种方式一般会混合使用)
  1. 显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象
  2. 隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。比如 new User()

②. 类的加载器分类与测试

①. 类加载器的介绍

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

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

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

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

②. 启动(引导)类加载器 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 的子类时候,我们常见的会有两种做法:

  1. 重写loadClass()方法(不推荐,这个方法会保证类的双亲委派机制)
  2. 重写findClass()方法 -->推荐
  3. 这两种方法本质上差不多,毕竟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();
        }
    }
}

⑥. 测试不同的类加载器

  • ①. 每个Class对象都会包含一个定义它的ClassLoader的一个引用

  • ②. 获取ClassLoader的途径

	(1). 获得当前类的ClassLoader
	clazz.getClassLoader()
	(2). 获得当前线程上下文的ClassLoader(系统类加载器)
	Thread.currentThread().getContextClassLoader()
	(3). 获得系统的ClassLoader
	ClassLoader.getSystemClassLoader()
  • ③. 站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加载器,引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载器压根儿就不是一个Java类,因此在Java程序中只能打印出空值

  • ④. 数组类的Class对象,不是由类加载器去加载的,而是在Java运行期JVM根据需要自动创建的。对于数组的类加载器来说,是通过Class.getClassLoader()返回的,与数组中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的(基本数据类型由虚拟机预先定义)

public class ClassLoaderDemo {
    public static void main(String[] args) {
        ClassLoader classloader1 = ClassLoader.getSystemClassLoader();
        //sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classloader1);
        //获取到扩展类加载器
        //sun.misc.Launcher$ExtClassLoader@424c0bc4
        System.out.println(classloader1.getParent());
        //获取到引导类加载器 null
        System.out.println(classloader1.getParent().getParent());
        //获取系统的ClassLoader
        ClassLoader classloader2 = Thread.currentThread().getContextClassLoader();
        //sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classloader2);
        String[]strArr=new String[10];
        ClassLoader classLoader3 = strArr.getClass().getClassLoader();
        //null,表示使用的是引导类加载器
        System.out.println(classLoader3);
        ClassLoaderDemo[]refArr=new ClassLoaderDemo[10];
        //sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(refArr.getClass().getClassLoader());
        int[]intArr=new int[10];
        //null,如果数组的元素类型是基本数据类型,数组类是没有类加载器的
        System.out.println(intArr.getClass().getClassLoader());
    }
}

③. ClassLoader源码剖析

①. ClassLoader与现有类加载器的关系

  • ①. ClassLoader是一个抽象类。如果我们给定了一个类的二进制名称,类加载器应尝试去定位或生成构成定义类的数据。一种典型的策略是将给定的二进制名称转换为文件名,然后去文件系统中读取这个文件名所对应的class文件

  • ②. ClassLoader与现有类加载器的关系
    在这里插入图片描述

  • ③. ExtClassLoader并没有重写loadClass()方法,这足矣说明其遵循双亲委派模式

  • ④. AppClassLoader重载了loadClass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。

②. 抽象类ClassLoader的主要方法(内部没有抽象方法)

  • ①. public final ClassLoader getParent():返回该类加载器的超类加载器

  • ②. public Class<?> loadClass(String name) throws ClassNotFoundException
    (加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回ClassNot FoundException 异常。该方法中的逻辑就是双亲委派模式的实现)

  • ③. protected Class<?> findClass (String name) throws ClassNotFoundException
    查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用。

  • ④. protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用。

  • ⑤. protected final void resolveClass(Class<?> c)
    链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用

  • ⑥. 源码解析Classloader方法

 测试代码:
 ClassLoader.getSystemClassLoader().loadClass("com.xiaozhi.java.User");
 //resolve==true,加载class的同时需要进行解析操作
 protected Class<?> loadClass(String name, boolean resolve) 
        throws ClassNotFoundException
    {
		//同步操作,保证只能加载一次
        synchronized (getClassLoadingLock(name)) {
            // 在缓存中判断是否已经加载同名的类
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
					//获取当前类的父类加载器
                    if (parent != null) {
						//如果存在父类加载器,则调用父类加载器进行类的加载(双亲委派机制)
                        c = parent.loadClass(name, false);
                    } else {
						//parent==null 父类加载器是引导类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				// 当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类
                if (c == null) {
                    // 调用当前classloader的findClass
                    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;
        }
    }

③. SecureClassLoader与URLClassLoader

  • ①. 接着SecureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联

  • ②. 前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁

④. Class.forName()与ClassLoader.loadClass()对比

  • ①. Class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个 Class 对象。该方法在将 Class 文件加载到内存的同时,会执行类的初始化。如:Class.forName(“com.atguigu.java.HelloWorld”);

  • ②. ClassLoader.loadClass():这是一个实例方法,需要一个 ClassLoader 对象来调用该方法。该方法将 Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器。

④. 双亲委派机制

  • ①. 工作原理
  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)

在这里插入图片描述

⑤. 沙箱安全机制

  • ①. 如图,虽然我们自定义了一个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代码
  • ④. JDK1.6时期,当前最新的安全机制实现,则引入了域(Domain)的概念
    虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示,最新的安全模型(jdk1.6)
    在这里插入图片描述

以上是关于JVM16_类的概述分类ClassLoader源码分析自定义类的加载器双亲委派机制沙箱安全机制的主要内容,如果未能解决你的问题,请参考以下文章

JVM02_类加载子系统

别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析JVM篇二

JVM进阶之类加载器详解

JVM进阶之类加载器详解

JVM学习---类加载子系统

JVM类加载详解