天天看点

jvm的类加载机制其实也没有想象中那么难以琢磨jvm的类加载机制

jvm的类加载机制

1、类加载过程

多个java类通过编译打包成可运行的jar包,最终由java命令运行某个主类的main函数启动程序。

首先需要通过类加载器把主类加载到jvm。

主类在运行过程中使用其它类的时候,才逐步加载这些类。

注意,jar包里面的类不是一次性加载的,是使用到的时候才会加载到jvm中。

类加载到使用的整个过程有如下几步:

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

  • 加载:在磁盘上查找并通过IO读入字节码文件,使用到类时才会加载,比如new对象的时候,调用静态方法的时候等等。
  • 验证:检验语法,校验字节码文件的正确性。
  • 准备:给类的静态变量分配内存,并赋予默认值(比如:static int i = 1,此时给i变量分配内存,赋值为0)
  • 解析:静态链接(把一些静态方法替换为指向所存内存的指针或句柄)和动态链接(在程序运行期间完成的,将符号引用替换为直接引用)过程
  • 初始化:对类的静态变量初始化为指定的值将i赋值为1,执行静态代码块
    jvm的类加载机制其实也没有想象中那么难以琢磨jvm的类加载机制

2、类加载器

类加载过程主要是通过类加载器来实现的,java里面的类加载器主要有:

  • 启动类加载器:负责加载支撑jvm运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
  • 扩展类加载器:负责加载支撑jvm运行的位于JRE的lib目录下的ext扩展目录中的jar类包
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载自己在项目中写的类
  • 自定义类加载器:负责加载用户自定义路径下的类包

查看各个类别的类加载器:

public class TestJDKClassLoader {

	public static void main(String[] args) {
    	System.out.println(String.class.getClassLoader()); // 启动类加载器
    	System.out.println(com.sun.crypto.provider.AESKeyGenerator.class.getClassLoader().getClass().getName());// 扩展类加载器
    	System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());// 应用程序类加载器
    	System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());// 应用程序类加载器
	}
}

运行结果:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader
           

自定义类加载器

自定义类加载器需要继承java.lang.ClassLoader类,该类定义了两个核心方法:

1、loadClass(String name, boolean resolve)

loadClass()方法实现了双亲委派机制,大体逻辑如下:

  1. 检查指定名称的类是否已经被加载,如果已经加载过了,则不需要再加载,直接返回
  2. 如果此类没有被加载,则判断是否有父级类加载器,如果有父级类加载器,则由父级类加载器加载(parent.loadClass(name, false)),或者是调用bootStrap类加载器来加载
  3. 如果父加载器和bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass(String name)方法来完成类加载

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);// 判断指定名称的类是否已经被加载
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) { // 判断是否有父级类加载器
                      c = parent.loadClass(name, false); // 如果有,则由父级类加载器来加载此类
                  } else {
                      c = findBootstrapClassOrNull(name);// 父级加载器为空,由bootstrap类加载器加载
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) { // 如果父级类加载器和bootstrap类加载器都没有找到这个类,则由当前类加载器调用findClass方法加载此类
                  // 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;
      }
  }
           

2、findClass(String name)

findClass()默认是抛出异常,自定义类加载器主要是重写findClass方法,实现过程如下:

(1)先定义一个User类

public class User
{
  public void userLoad()
  {
    System.out.println("user类被加载 : " + User.class.getClassLoader().getClass().getName());
  }
}
           

(2)将User编译过后的class文件放在自定义的目录

jvm的类加载机制其实也没有想象中那么难以琢磨jvm的类加载机制

(3)自定义classLoader加载自定义目录下的User类

public class MyClassLoader extends ClassLoader {

    private String classPath;
    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 重写ClassLoader的findClass方法
     * @param name 类名(全限定名)
     * @return
     * @throws ClassNotFoundException
     */
    protected Class<?> findClass(String name)
            throws ClassNotFoundException
    {
        try {
            byte[] data = loadByte(name); // 将指定的class文件读取为字节数组
            return defineClass(name,data,0,data.length); // defineClass 是ClassLoader封装好的,将字节数组转为class对象,直接调用即可
        }catch (Exception e){
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    /**
     *  将指定的class文件转为byte数组
     * @param name
     * @return
     * @throws IOException
     */
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.","/"); // 全类名中的 . 转为 /  解析为文件路径
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }


    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("F:/classTest");
        Class clazz = classLoader.loadClass("com.std.jvm.classLoader.User"); // 加载自定义位置的User类
        Object obj = clazz.newInstance(); // 创建对象实例
        Method method = clazz.getDeclaredMethod("userLoad",null);
        method.invoke(obj,null); // 执行User中定义的方法
    }
}
           

(4)执行结果:

user类被加载 : com.std.jvm.classLoader.MyClassLoader

Process finished with exit code 0
           

3、双亲委派机制

jvm类加载器是有亲子层级结构的,如下:

jvm的类加载机制其实也没有想象中那么难以琢磨jvm的类加载机制

类加载遵循双亲委派机制,加载某个类时会先委托父加载器寻找目标,如果所有的父加载器在自己加载的类路径下都找不到目标类,则在当前类加载器的加载路径中查找并载入目标类。

比如上面自定义加载类加载的User类:

1、最先会找到自定义类加载器(MyClassLoader)加载,自定义类加载器会委托应用程序类加载器(AppClassLoader)加载,应用程序类加载器会委托扩展类加载器(ExtClassLoader)加载,扩展类加载器委托启动类加载器加载(c语言写的所以看不到)。

2、顶层的启动类加载器在自己的类加载路径下寻找User类,并没有找到,则向下退回加载的请求,扩展类加载器收到回复则自己加载,寻找自己的类加载路径下,也没有找到User类,继续向下退回请求,应用程序类加载器收到回复在自己的类加载路径下依然没有找到User类,继续向下退回加载请求,最终,自定义类加载器收到回复,在自己的类加载路径下(F:/classTest)找到了User类(com.std.jvm.classLoader.User),则会加载User类到jvm

3、简单说就是:类加载的时候,先由父级类加载器加载,父级类加载器没有加载到再由子级类加载器加载

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

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样可以防止java核心类库被随意修改
  • 避免类的重复:当父级类加载器加载了该类时,就没有必要子级类加载器再加载一次(要不然用到该类时,应该用哪一个?),保证了被加载类的唯一性

沙箱安全机制示例,比如自定义一个java.lang.String:

package java.lang;
	
public class String {

    public static void main(String[] args) {
        System.out.println("我是java.lang.String,我的类加载器是: "+String.class.getClassLoader().getClass().getName());
    }

}

执行结果为:

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

出现这个情况的原因就是应用程序类加载器加载自定义的java.lang.String的时候,遵循双亲委派机制,会先向上推送加载请求,当顶级启动类加载器收到此请求时,在自己的加载路径下找到了java.lang.String,并将其成功加载到了jvm中。最终请求再返回到应用程序类加载器的时候,loadClass()中判断java.lang.String已经被加载到了jvm,则不会再继续加载此类,所以jvm中加载的类并不是当前自定义的String类

双亲委派机制是由loadClass()方法实现的,那么自定义的类加载器可否重写loadClass()方法,尝试打破双亲委派机制,java.lang.String不想上传送,直接由当前自定义类加载器加载呢?

1、依然采用以上的自定义类加载器,先将自定义的java.lang.String.class存放在自定义的目录

jvm的类加载机制其实也没有想象中那么难以琢磨jvm的类加载机制

2、在自定义的类加载器中重写loadClass()方法尝试打破双亲委派

public class MyClassLoader extends ClassLoader {

    private String classPath;
    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 重写ClassLoader的findClass方法
     * @param name 类名(全限定名)
     * @return
     * @throws ClassNotFoundException
     */
    protected Class<?> findClass(String name)
            throws ClassNotFoundException
    {
        try {
            byte[] data = loadByte(name); // 将指定的class文件读取为字节数组
            return defineClass(name,data,0,data.length); // defineClass 是ClassLoader封装好的,将字节数组转为class对象,直接调用即可
        }catch (Exception e){
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    /**
     *  将指定的class文件转为byte数组
     * @param name
     * @return
     * @throws IOException
     */
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.","/"); // 全类名中的 . 转为 /  解析为文件路径
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }


    /**
     * 重写类加载方法,当前类加载器加载指定路径下面的类,不向上委派,直接自己加载
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    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();

                // 如果目标类未被加载,不判断父级类加载器,直接自己来加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name); // 直接调用自己的findclass方法

                    // 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;
        }
    }

    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("F:/classTest");
        Class clazz = classLoader.loadClass("java.lang.String"); // 加载自定义位置的String类
        Object obj = clazz.newInstance(); // 创建对象实例
    }
}

执行结果:

	java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
           

抛异常说非法的命名,这个是沙箱安全机制的非法命名验证,可以说明重写loadClass方法以后,确实没有向上委派,加载的是当前的自定义的java.lang.String类。

现实应用中打破双亲委派的实例:tomcat

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

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

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

再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行? 答案是不行的。为什么?

  • 第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的, 默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
  • 第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
  • 第三个问题和第一个问题一样。
  • 我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class 文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后 的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以 你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载 这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

Tomcat自定义加载器详解

jvm的类加载机制其实也没有想象中那么难以琢磨jvm的类加载机制

tomcat的几个主要类加载器:

  • commonLoader:Tomcat基本的类加载器,加载路径中的class可以被 Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于 Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有 Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对 当前Webapp可见;

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

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

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

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

tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。 我们前面说过,双亲委派机制要求除了顶层的启动类加载器之外,其余的类加载器都应当由 自己的父类加载器加载。

自己能加载的类则与对方相互隔离。

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

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

tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。 我们前面说过,双亲委派机制要求除了顶层的启动类加载器之外,其余的类加载器都应当由 自己的父类加载器加载。

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