java类加载机制

文章目录

  • java类加载机制
      • 准备测试类
      • 类的加载运行流程
        • 其中loadClass有如下步骤
          • 测试类加载发生时间代码
      • 类加载器和双亲委派机制
        • **双亲委派机制**
          • 双亲委派机制代码逻辑图
          • 双亲委派机制示意图
          • **为什么设计双亲委派机制?**
        • 全盘负责委托机制
      • **自定义类加载器示例**
      • **打破双亲委派机制**
      • **Tomcat打破双亲委派机制**
          • 模拟实现

java类加载机制

准备测试类

package com.blog.jvm.classloader;

/**
 * @Auth : cloudinwinter 2021/2/7 19:23
 * @description : 测试Java类加载机制
 */
public class ClassLoaderTest {
    private static final int i = 999;
    private static Book book = new Book();

    public int compute(){
        int i = 1;
        int j = 2;
        int k = (i + j) * 10;
        return k;
    }
    public static void main(String[] args) {
        ClassLoaderTest loaderTest = new ClassLoaderTest();
        loaderTest.compute();
    }
}

类的加载运行流程

java类加载机制_第1张图片

其中loadClass有如下步骤

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

加载:通过流读取类的字节码文件,一般只有在使用时触发,使用类的静态元素,和new一个类的对象,只是进行类的定义入User user1 =null;不会触发类的加载。

验证:验证字节码文件的正确性。

准备:给类的静态变量分配内存,并且赋值默认值,final修饰直接进行赋值。

解析:将符号引用(类加载前类中定义的方法和引用的类所分配的内存地址的位置无法获取,用符号进行代替)转换为直接引用(将符号转化为内存地址),其中静态链接是指在类加载过程中完成上述过程,动态链接是指在程序运行期间完成上述过程。

初始化: 对静态属性赋值,执行静态代码块。

java类加载机制_第2张图片

测试类加载发生时间代码
package com.blog.jvm.classloader;

/**
 * @Auth : cloudinwinter 2021/2/7 21:50
 * @description : 测试类只有被使用(静态方法被调用,创建类的实例)时,才会触发类加载器
 */
public class TestDanamicLoad {
    static {
        System.out.println("TestDanamicLoad已经被加载");
    }
    
    public static void main(String[] args) {
        new A();
        //只是进行定义,不会触发类的加载
        B b = null;
    }

}

class A{
    // 类加载时静态代码块会执行
    static {
        System.out.println("a的静态代码块执行");
    }

    A(){
        System.out.println("a的构造方法被执行");
    }
}

class B{
    static {
        System.out.println("b的静态代码块执行");
    }

    B(){
        System.out.println("b的构造方法被执行");
    }
}

类加载器和双亲委派机制

​ 类加载过程主要是通过类加载器实现

引导类加载器:负责加载支持jvm运行的jre的lib目录下核心类库,如rt.jar

扩展类加载器: 负责加载jvm运行的jre的lib下的ext目录下jar包

应用程序类加载器:加载classpath目录下的类,主要是有我们编写的程序生成的class文件

自定义类加载器: 加载自定义目录下的类包

package com.blog.jvm.classloader;

import com.sun.crypto.provider.DESedeKeyFactory;
import sun.misc.Launcher;

import java.net.URL;

/**
 * @Auth : cloudinwinter 2021/2/7 22:22
 * @description : TODO
 */
public class TestJdkLoader {

    public static void main(String[] args) {
        // 输出jdk核心jar包的类加载器,c++实现,所以返回为null
        System.out.println(String.class.getClassLoader());
        // 输出jdk扩展jar包的类加载器名称,
        System.out.println(DESedeKeyFactory.class.getClassLoader().getClass().getName());
        // 输出运行程序的类加载器名称
        System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getName());

        //这里的getParent()方法拿到的是类的parent属性,并非继承关系中父类的意思。
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassLoader = appClassLoader.getParent();
        ClassLoader bootStrapClassLoader = extClassLoader.getParent();
        System.out.println("应用程序类加载器:"+ appClassLoader);
        System.out.println("扩展类加载器:"+ extClassLoader);
        System.out.println("自定义类加载器:"+ bootStrapClassLoader);

        System.out.println();
        System.out.println("bootStrapClassLoader加载下列文件:");
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urLs.length; i++) {
            System.out.println(urLs[i]);
        }

        System.out.println();
        System.out.println("扩展类加载器加载下列文件");
        System.out.println(System.getProperty("java.ext.dirs"));

        // 会输出包含扩展类加载器和引用程序类加载器的所有文件
        System.out.println();
        System.out.println("应用程序类加载器加载下列文件");
        System.out.println(System.getProperty("java.class.path"));

    }
}

运行结果:
    null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
应用程序类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
扩展类加载器:sun.misc.Launcher$ExtClassLoader@4b67cf4d
自定义类加载器:null

bootStrapClassLoader加载下列文件:
应用程序类加载器加载下列文件
C:\Program Files\Java\jdk1.8.0_211\jre\lib\charsets.jar;
C:\ProgramFiles\Java\jdk1.8.0_211\jre\lib\deploy.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\access-bridge-64.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\dnsns.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\jfxrt.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\nashorn.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\sunjce_provider.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\sunpkcs11.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\zipfs.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\javaws.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jfxswt.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\management-agent.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\plugin.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_211\jre\lib\rt.jar;
C:\Users\jeol\IntelliJ IDEA 2020.1.2\lib\idea_rt.jar
D:\jeol\Desktop\jvmTest\target\classes;// --这有这一个才是应用程序类加载器加载的

双亲委派机制

​ 当前的类加载器加载类时,会委托其父加载器进行加载(非继承关系发,只是当前加载器的parent属性设置为上一级的类加载器),如果父加载器还有父加载器,继续向上委托;到了最上级会先判断当前加载器的加载路径下是否包含该类,包含则直接直接加载,不包含会委托下一级的类加载器进行加载,以此类推。

双亲委派机制代码逻辑图

​ 介绍了应用程序类的类加载流程,涉及到应用程序类加载器,扩展类类加载器,引导类类加载器

java类加载机制_第3张图片

appclassLoader的继承关系

java类加载机制_第4张图片

1、加载应用程序类时会调用到appclassLoader的loadClass(“类名”)方法,这里会直接调用父类的loadClass方法,由于URLClassLoader没有重写ClassLoader的loadClass(“类名”)方法,所以这里会直接调用到ClassLoader的loadClass方法。

2、ClassLoader的loadClass方法有如下操作:请查看注释

// ClassLoader的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,判断类是否被加载,会调用本地方法
            Class<?> c = findLoadedClass(name);
            
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    /*
                    没有找到,会判断当前类加载器的parent属性是否为空,为空会委托上一级的加载器进行加载,
                    parent属性设置位置下面会有说明.比如appClassLoader的parent属性是
                    ExtClassLoader,然后ExtClassLoader也继承了这个方法,走到这个位置继续判断其parent
                    属性是否为空。
                    */
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 引导类加载器是由本地方法实现的,到了ExtClassLoader,其父属性就为null,这里					     //调用引导类
                        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();
                    /*
                    通过上面的步骤,此时这里应该是调用ExtClassLoader的findClass方法,java的继承特性,
                    会调用 URLClassLoader的findClass方法,
                    */
                    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;
        }
    }

​ 3、URLClassLoader的findClass方法

// URLClassLoader的findClass方法   
protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        // 类名转化为相对路劲
                        String path = name.replace('.', '/').concat(".class");
                        /*首先第一次调用到此处,是ExtClassLoader类,ucp是其的类的静态属性,定义了其加
                        载那些路径下的包,这里由于是应用类,所以res首次返回为null, 又回到loadClass方法
                        继续往下走。
                        */
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                // 通过路径获取class文件的二进制数据,加载到jvm中
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

4、Launcher类在其构造方法被调用时会创建扩展类加载器对象和应用程序类加载器对象,并且将应用程序类加载器对象设置为扩展类加载器对象。

双亲委派机制示意图

java类加载机制_第5张图片

为什么设计双亲委派机制?
  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心
    API库被随意篡改。
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一
    次,保证被加载类的唯一性
package java.lang;
/**
 * @Auth : cloudinwinter 2021/2/14 19:28
 * @description : 测试Java核心类被重写的情况
 */
public class String {

    public static void main(String[] args) {
        System.out.println("测试沙箱安全机制");
    }
}

运行结果:
    错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

结论:加载的还是核心包的String类,所以找不到main方法。
全盘负责委托机制

​ “全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类
所依赖及引用的类也由这个ClassLoder载入。

自定义类加载器示例

​ 自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是
loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空
方法,所以我们自定义类加载器主要是重写findClass方法。

package com.blog.jvm.classloader;

import java.io.FileInputStream;
import java.io.IOException;

/**
 * @Auth : cloudinwinter 2021/2/14 19:35
 * @description : 实现自定义的类加载器
 */
public class MyClassLoader extends ClassLoader {
    private String classPath;

    public MyClassLoader(String classPath) {
        // 这里会设置自定义的加载器的parent属性为AppClassLoader
        super();
        this.classPath = classPath + "/";
    }

    public MyClassLoader(ClassLoader parent, String name) {
        super(parent);
        this.classPath = classPath + "/";
    }

    private byte[] loadClassByte(String name) throws IOException {
        String path = name.replace('.', '/');
        FileInputStream fis = new FileInputStream(classPath + path + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = new byte[0];
        try {
            data = loadClassByte(name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return defineClass(name, data, 0, data.length);
    }

}

测试代码
        try {
            MyClassLoader myClassLoader = new MyClassLoader("D:/classloadertest");
            Class aClass = myClassLoader.loadClass("com.blog.jvm.classloader.Book");
            Object object = aClass.newInstance();
            Method method = aClass.getDeclaredMethod("introduce", null);
            method.invoke(object,null);
            System.out.println(object.getClass().getClassLoader().getClass().getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    

打破双亲委派机制

​ 再来一个沙箱安全机制示例,尝试打破双亲委派机制,用自定义类加载器加载全限定类名称相同的类,位于不同位置。双亲委派机制通过ClassLoader的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);
            long t0 = System.nanoTime();
            if (c == null) {

                long t1 = System.nanoTime();
                // 非项目的包还是交给jdk自带的类加载器进行加载,这里自定义的类加载器委托给应用程序类加载器				//进行加载
                if (!name.startsWith("com.blog.jvm")) {
                    c = this.getParent().loadClass(name);
                } else {
                    c = findClass(name);
                }
                // this is the defining class loader; record the stats
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();

            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

Tomcat打破双亲委派机制

以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?

我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  • 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  • 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
  • web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  • web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。

再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行?

答案是不行的。为什么?

第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性

第三个问题和第一个问题一样。

我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

Tomcat自定义加载器详解

java类加载机制_第6张图片

tomcat的几个主要类加载器:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;

从图中的委派关系中可以看出:

CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。

tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。

很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制

模拟实现

参考上方打破双亲委派机制代码,创建自定义加载器时,加载位置不同即可。

你可能感兴趣的:(java基础,java,ClassLoader,类加载机制)