类加载器(双亲委派模型,自定义加载器)

        本文对类加载系统做系统性介绍关于类加载过程在CSDN文章有介绍。

一、类加载子系统

1.类加载子系统介绍

1.类加载子系统负责从文件系统或是网络中加载.class文件,class文件在文件开头有特定的文件标识;

2.把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);

3.ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;

4.如果调用构造器实例化对象,则该对象存放在堆区;

类加载器(双亲委派模型,自定义加载器)_第1张图片

2.类加载器ClassLoader的角色

1.class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。

2.class file 加载到JVM中,被称为DNA元数据模板。

3.在 .class文件 --> JVM --> 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。

类加载器(双亲委派模型,自定义加载器)_第2张图片

二、类加载器

1.类加载器的作用

        类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建 一个java.lang.Class对象,用来封装类在方法区内的数据结构。

注意:JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

2.类加载器的分类

        jvm支持两种类型的加载器,分别是引导类加载器自定义加载器 。引导类加载器是由c/c++实现的,自定义加载器是由java实现的。 jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器。按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)。

类加载器(双亲委派模型,自定义加载器)_第3张图片

注意:上图中的加载器划分为包含关系并非继承关系。

1.启动类加载器

        这个类加载器使用c/c++实现,嵌套再jvm内部 。它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、 resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。并不继承自 java.lang.ClassLoader,没有父加载器

2.扩展类加载器

        java语言编写,由sun.misc.Launcher$ExtClassLoader实现。从java.ext.dirs系统属性所指定的目录中加载类库, 或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动扩展类加载器加载。派生于 ClassLoader。父类加载器为启动类加载器。

3.系统类加载器

        java语言编写,由 sun.misc.Lanucher$AppClassLoader 实现。该类加载是程序中默认的类加载器,一般来说, Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类 库;派生于 ClassLoader 。父类加载器为扩展类加载器。通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。

4.用户自定义类加载器

        在日常的Java开发中,类加载几乎是由三种加载器配合执行的,在必要时我们还可以自定义类加载器,来定制类的加载方式。(文章后面会有自定义加载器的详细介绍)

三、双亲委派模型

1.什么是双亲委派模型

        双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时 (即 ClassNotFoundException ),子加载器才会尝试自己去加载。

2.为什么需要双亲委派模型

假设没有双亲委派模型,试想一个场景:

        黑客自定义一个 java.lang.String 类,该 String 类具有系统的 String 类一样的功能,只是在某个函数稍作修改。比如 equals 函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到 JVM 中。此时,如果没有双亲委派模型,那么 JVM 就可能误以为黑客自定义的java.lang.String 类是系统的 String 类,导致“病毒代码”被执行。

        而有了双亲委派模型,黑客自定义的 java.lang.String 类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的 java.lang.String 类,最终自定义的类加载器无法加载 java.lang.String 类。

        或许你会想,我在自定义的类加载器里面强制加载自定义的 java.lang.String 类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在 JVM 中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

举个简单例子:

        ClassLoader1 、 ClassLoader2 都加载 java.lang.String 类,对应Class1、Class2对象。那么 Class1 对象不属于 ClassLoad2 对象加载的 java.lang.String 类型。

3.如何实现双亲委派模型

        双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实 ClassLoader 类默认的 loadClass 方法已经帮我们写好了,我们无需去写。

几个重要函数:

1.loadClass
 
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(String, boolean) 函数即实现了双亲委派模型!整个大致过程如下:

1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。

2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用 parent.loadClass(name, false); ),或者是调用 bootstrap 类加载器来加载。

3. 如果父加载器及 bootstrap 类加载器都没有找到指定的类,那么调用当前类加载器的 findClass 方 法来完成类加载。换句话说,如果自定义类加载器,就必须重写 findClass 方法!

2.findClass

findClass默认实现:

protected Class findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

        可以看出,抽象类 ClassLoader 的 findClass  函数默认是抛出异常的。而前面我们知道, loadClass 在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的 findeClass 函数,因此我们必须要在 loadClass 这个函数里面实现将一个指定类名称转换为 Class 对象。

        如果是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为 Class 对象呢?很简单, Java 提供了 defineClass 方法,通过这个方法,就可以把一个字节数组转为Class对象。

3.defineClass

 defineClass 主要的功能是:

        将一个字节数组转为 Class 对象,这个字节数组是 class 文件读取后最终的字节数组。如,假设 class 文件是加密过的,则需要解密后作为形参传入 defineClass 函数。

defineClass 默认实现如下:

protected final Class defineClass(String name, byte[] b, int off, int len) 
        throws ClassFormatError {
    return defineClass(name, b, off, len, null);
}

四、自定义类加载器

1.为什么需要类自定义类加载器

1.隔离加载类

        模块隔离,把类加载到不同的应用选中。比如tomcat这类web应用服务器,内部自定义了好几中类加载 器,用于隔离web应用服务器上的不同应用程序。

2.修改类加载方式

        除了Bootstrap加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。

3.扩展加载源

        比如还可以从数据库、网络、或其他终端上加载。

4.防止源码泄漏

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

2.自定义函数的调用过程

类加载器(双亲委派模型,自定义加载器)_第4张图片

3.自定义加载器的实现

实现方式: 所有用户自定义类加载器都应该继承ClassLoader类 在自定义ClassLoader的子类是,我们通常有两种做法:

1) 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)

2)重写findClass方法 (推荐)

public class MyClassLoader extends ClassLoader{
	
    // 指定的要加载 class 文件的目录
    private File classPathFile;  
    
    public MyClassLoader(String absolutePath) {
        this.classPathFile = new File(absolutePath);
    }

    @Override
    // 根据类名将指定类加载进 JVM
    // 注:该 class 必修在指定的 absolutePath 下
    protected Class findClass(String name) throws ClassNotFoundException {

        // 拼接全类名
        String className = MyClassLoader.class.getPackage().getName() + "." + name;
        if(classPathFile  != null){
            // 根据绝对路径,以及 class 文件名,拿到 class 文件
            // 注意全类名的情况 
            File classFile = new File(classPathFile,name.replaceAll("\\.","/") + ".class");
            // 如果 class 文件存在
            if(classFile.exists()){

                // 将 class 文件读入内存,暂存到一个字节数组中
                FileInputStream in = null;
                ByteArrayOutputStream out = null;
                try{
                    in = new FileInputStream(classFile);
                    out = new ByteArrayOutputStream();
                    byte [] buff = new byte[1024];
                    int len;
                    while ((len = in.read(buff)) != -1){
                        out.write(buff,0,len);
                    }

                    // 构造类的 Class 对象!!!
                    // 注:defineClass() 是一个 native 方法
                    return defineClass(className, out.toByteArray(), 0, out.size());
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

你可能感兴趣的:(JVM,JAVA学习笔记,java,开发语言,jvm)