1、JVM的类加载机制

1、java里面加载类的流程图。

1、JVM的类加载机制_第1张图片

主要步骤:

  • 加载:在硬盘上查找并通过IO读取字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  • 验证:校验字节码文件的准确性。
  • 准备:给这个类的静态变量分配内存,并赋予默认值。
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。
  • 初始化:对类的静态变量初始化成指定值,执行静态代码块。

注意:主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

2、Java中对应的类加载器

  • 引导类加载器:负责加载支撑JVM运行的,位于JRElib目录下的核心类库,比如rt.jar、charsets.jar等。
  • 扩展类加载器:负责加载支撑JVM运行的位于JRElib目录下的ext扩展目录中的JAR类包。
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
  • 自定义类加载器:负责加载用户自定义路径下的类包。

类加载器被创建的流程图:

1、JVM的类加载机制_第2张图片

Launch 类的相关源码如下:

    // 调用 getLauncher 返回一个Launcher类的实例对象,该实例对象会由JVM进行创建
    public static Launcher getLauncher() {
        return launcher;
    }    
    
    // 在创建 Launcher实例对象的时候就会调用到该 构造方法
    /**
    * 1、
    */
    public Launcher() {
        Launcher.ExtClassLoader var1;
        try { 
            /**
            *  创建了一个 ExtClassLoader (扩展类加载器),这个方法里面就会对 ExtClassLoader 
              *  parent 属性 进行赋值操作,这里直接会赋值成 null
            */
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
           /**
            *  创建了一个 AppClassLoader (扩展类加载器),这个方法里面就会对 AppClassLoader
              *  parent 属性 进行赋值操作,这里直接会赋值成 上面创建的 ExtClassLoader,并给该类的                *  loader 属性 赋值为 AppClassLoader的实例对象
            */
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        //设置当前线程的类加载器 为 AppClassLoader
        Thread.currentThread().setContextClassLoader(this.loader);
        // 。。。 。。。 省略一些不需关注代码
    }

上述代码主要的逻辑就是去 创建了两个类加载器ExtClassLoaderAppClassLoader 并分别赋值对应的parent的属性 为 null 和 ExtClassLoader的实例对象。

3、双亲委派机制

1、JVM的类加载机制_第3张图片

Java里面进行类的加载主要使用的类是 java.lang.ClassLoader#loadClass(java.lang.String),所以要查看双亲委派机制是如何进行的,我们需要看下loadClass方法对应的逻辑。loadClass方法主要的源码如下:

    public Class loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

     protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 这里首先会去判断当前类是否已经被加载过了,如果已经被加载过了,这里就不会返回null
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 首先会判断当前类的父类加载器是否为空,第一次进来时我们的类型加载器为 AppClassLoader ,调用父类 ExtClassLoader.loadClass 而 ExtClassLoader 调用时 parent == null
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 当前为 ExtClassLoader 对应的 parent 为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();
                    // 如果前面对应的父类没有加载到时,就会调用 该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;
        }
    }

双亲委派机制说简单点就是先找父类加载,不行再由儿子自己加载。

我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoaderloadClass方法最终会调用其父类ClassLoaderloadClass方法,该方法的大体逻辑如下:

  1. 首先,检查一下指定名称的类是否已经加载过了,如果加载过了,就不需要再加载,直接返回。
  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name,false);)或者是调用bootstrap类加载器来加载。
  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的fIndClass方法来完成类加载。

从上面的代码中就可以看出,当调用 AppClassLoaderloadClass方法的时候先会调用父类的,然后父类调用父类的 直到调用到 parent == null,如果对应的父类没有加载到,则会由自身加载,这就是对应双亲委派的逻辑。

4、为什么要设计双亲委派机制

  • 沙箱安全机制:防止JDK核心API类库可以不被外部所篡改,如我们自己写的java.lang.String.class类不会被加载。
  • 避免类的重复加载:当父类已经加载过一次,没有必要子类再进行加载一次,保证加载类的唯一性

示例:

//自定义 String 类,包名跟 自带的String保持一致,则会报下面的错。
package java.lang;

public class String {

    public static void main(String[] args) {
        System.out.println("**************My String Class**************");
    }

}

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

5、自定义类加载器

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

示例:

// 待补充

5.1打破双亲委派机制

再来一个沙箱安全机制示例,尝试打破双亲委派机制,用自定义类加载器加载我们自己实现的java.lang.String.class

// 待补充

6、tomcat中加载 jar 或者 war包 是否可以使用双亲委派机制? 原因是什么?

从tomcat 的实现来看,tomcat 中 负责加载对应 部署项目的jar 或者 war 包中对应的类的自定义类加载器,是打破了双亲委派机制,且每一部署在同一个tomcat的项目 对应的 tomcat的自定义类加载器都是独立的,即每个项目都会有一个,原因如下:

​ 先假设有两个项目,一个项目中用的是 spring-expression 的版本为 4.3 , 另外一个项目使用的 spring-expression 的版本为 5.5。

  • 如果tomcat中使用了双亲委派机制,则当我们需要加载 spring-expression所使用到的类时,会将两个项目中先使用到的那个先加载 (用到再加载),这样就会导致后面那个项目在加载时发现父类以及加载了,就不会去加载了,所以这里就会有问题了,就会导致后面这个项目所要使用到的 spring-expression 中的版本不是它本身项目所指定的。
  • 如果tomcat 中部署的所有项目都公用同一个类加载器,同样也会出现上面那个问题。

7、本文主要围绕下面5个环节开展:

  • java 里面是如何加载一个类的,主要的步骤有哪些,以及每一步的作用是什么?
  • java里面的类加载器有哪些,以及这些类加载器是如何被创建的流程?
  • java里面是如何实现双亲委派机制的?为什么要设计双亲委派机制?
  • 如何自定义一个类加载,并且实现自定义的类加载器打破对应的双亲委派机制。
  • 思考,tomcat中加载 jar 或者 war包 是否可以使用双亲委派机制? 原因是什么?

你可能感兴趣的:(javajvm类加载机制)