【JVM】第一篇 从JDK源码级别彻底剖析JVM类加载机制

5 篇文章 0 订阅

一. Java类加载运行全过程

以Math类为例子,代码如下:

public class Math {
    public static int compute(){
        int a = 1;
        int b = 3;
        return (a + b) * 10;
    }

    public static void main(String[] args) {
        System.out.println(Math.compute());
    }
}

当运行Math类中的main方法时,程序是如何运行的,流程大致如下:
在这里插入图片描述

  1. 参见类运行加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher
    Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个Launcher实例。
    Launcher构造方法内部,其创建了两个类加载器,分别是ExtClassLoader(扩展类加载器)AppClassLoader(应用程序类加载器)
    JVM默认通过使用Launcher的getAppClassLoader()方法返回的AppClassLoader类加载器来加载我们的应用程序。
package sun.misc;

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    //C++ 调用Java代码创建JVM启动器,并且实例化Launcher
    public static Launcher getLauncher() {
        return launcher;
    }

    //Launcher 类的构造方法
    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //构建扩展类加载器ExtClassLoader,在构建的过程中将其父类加载器(引导类加载器)设置为null
            //所以输出引导类加载器为null,因为引导类加载器由C++实现
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //构建应用程序类加载器AppClassLoader,在构造的过程中,将其父类加载器设置为扩展类加载器ExtClassLoader
            //Launcher的loader变量值为AppClassLoader实例,一般都是用这个类加载器来加载自己编写的应用程序
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        ............此处源码省略
    }
  1. 类加载器全部创建完成,并且各自已经加载完各自的目录文件,此时JVM的运行环境才算初步建立完成,获取对应的类加载器(AppClassLoader)去加载Math.class字节码文件,大致流程如下:
    加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
    在这里插入图片描述
    加载环节分析:
  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

查询字节码文件命令: javap -v User1.class 一定要进入class文件的目录下
在这里插入图片描述
类被加载到方法区中后主要包含运行时常量池类型信息字段信息方法信息类加载器的引用对应class实例的引用等信息。

名词说明
运行时常量池是Java虚拟机在运行时创建的一种用于存放字面量和符号引用的表。它是各种指令操作数、类和接口中常量池的运行时表示形式。常量池中存储了各种类型的常量,如字符串、数字、类和接口的符号引用等
类型信息是类中声明的变量的类型信息,包括变量名、访问修饰符、类型等,用于在编译期和运行时确定变量的属性和行为
字段信息是类中声明的变量的信息,包括变量名、访问修饰符、类型等,以及对应的字段值、字段注解等信息,用于在编译期和运行时确定变量的属性和行为
方法信息是类中声明的方法的信息,包括方法名、返回值类型、参数类型等,以及对应的方法体、方法注解等信息,用于在编译期和运行时确定方法的属性和行为
类加载器的引用是加载该类的类加载器对象的引用。每个类都有一个对应的类加载器,它负责将类的字节码文件加载到内存中,并生成对应的Class实例
对应class实例的引用Class实例是Java中表示类的对象,它包含了类的所有信息,包括类型信息、字段信息、方法信息等。对应Class实例的引用指的是程序中持有该Class实例的引用。可以通过该引用来获取类的信息和操作类的属性和行为

二. 从JDK源码级别剖析JVM核心类加载器

JVM的类加载器类型有哪些?
在这里插入图片描述

  1. 引导类加载器 (BootstrapLoader)
    负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
    在这里插入图片描述
  2. 扩展类加载器(ExtClassLoader)
    负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
    在这里插入图片描述
  3. 应用类程序加载器(AppClassLoader)
    负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类

通过一段代码验证下,三个加载器加载的路径:

public class JDKClassLoaderTest {

    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());  //JDK核心类,是由引导类加载器加载,由于此加载器是由底层C++实现的,所以此处无法输出,显示为null
        System.out.println(DESKeyFactory.class.getClassLoader());  //JDK的ext目录下的类,由扩展类加载器加载
        System.out.println(JDKClassLoaderTest.class.getClassLoader()); //是本地生成的classpath路径下的class文件,由应用程序类加载器加载
        System.out.println();

        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassLoader = appClassLoader.getParent();
        ClassLoader bootstrapLoader = extClassLoader.getParent();
        System.out.println("应用程序类加载器=" + appClassLoader);
        System.out.println("应用程序类加载器的父级类加载器是=" + extClassLoader);
        System.out.println("扩展类加载器的父级类加载器是=" + bootstrapLoader);
        System.out.println();

        System.out.println("bootstrapLoader加载一下文件:");
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); //引导类加载器加载加载的 /jdk1.8.0_261/jre/lib 目录下的jar包
        for (URL urL : urLs) {
            System.out.println(urL);
        }
        System.out.println();

        System.out.println("extClassLoader加载一下文件:");
        System.out.println(System.getProperty("java.ext.dirs")); //扩展类加载器加载的是 /jdk1.8.0_261/jre/lib/ext 目录下的jar包

        System.out.println("appClassLoader加载一下文件:");
        System.out.println(System.getProperty("java.class.path"));
    }
}

运行结果
在这里插入图片描述
在这里插入图片描述
从运行的结果可以看出,应用程序类加载器的加载目录包含了引导类加载器扩展类加载器的加载目录
虽然打印出了目录,但是应用程序类加载器会将target/class 目录下的文件加载到指定内存中,其他目录的文件是不会加载到指定内存中的。
为什么会这样? 双亲委派机制

三. 从JDK源码级别剖析类加载双亲委派机制

双亲委派机制是指在一个层次结构中的类加载器,在加载类时,会优先把请求交给父类加载器处理,直到最顶层的类加载器,如果父类加载器无法加载该类,再由子类加载器来尝试加载。

  1. 双亲委派机制的流程
    在这里插入图片描述

源码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);  //检查当前类加载器是否已经加载了该类 name = com.maker.jvm.Math
         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();
                 //所有的类加载器都未加载过该类,会调用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;
     }
 }

大致过程如下:

  • 加载Math.class时,应用程序类加载器(AppClassLoader)最先接收到类加载的请求,然后在自己的专属内存区域中检查是否已经加载过了,如果加载了,直接返回使用,如果没有加载,向上委托父级扩展类加载器(ExtClassLoader)去加载。
  • 扩展类加载器先查看自己专属内存区域中是否已经加载过该类,如果加载了,直接返回,没有加载,扩展类加载器会向上委托父加载器,也就是引导类加载器去处理。
  • 引导类加载器查看自己专属内存区域中是否已经加载过该类,如果加载了,直接返回,没有加载,会尝试自己加载。
  • 由于Math.class在classpath路径下,并不在引导类加载器的加载范围,引导类加载器会加载失败,向下委托子加载器扩展类加载器去加载,扩展类加载器加载失败,会向下委托子加载器应用程序类加载器加载。
  • 应用程序类加载器先查看自己专属内存区域中是否已经加载过该类,如果加载了,直接返回,没有加载,则加载类,并且缓存在自己的专属区域中。

总结: 双亲委派 ,双向委托

2. 为什么要设计双亲委派机制?
沙箱安全机制: 自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载: 当父级类加载器已经加载了该类时,就没有必要子类加载器再加载一次,保证被加载类的唯一性

package java.lang;   //模拟JDK核心类String的路径,java.lang.String

public class String {

  public static void main(String[] args) {
      //JDK 核心类库中的String类,包路径是在java.lang.String
      //模拟此路径,验证本地的String是否会被加载
      System.out.println("使用的类加载器类型=" + String.class.getClassLoader());
  }
}

在这里插入图片描述
由于双亲委派,String类在引导类加载器中被找到,而引导类加载器中加载的String类无main方法,所以报以上错误。
引导类加载器和扩展类加载器中已经加载了类,就不需要重复加载了

3. 全盘负责委托机制
当一个类加载器加载一个类时,除非指定其他类加载器,那么该类中依赖或者引用的其他类也都由当前这个类加载器加载

public static void main(String[] args) {
    System.out.println("加载Math的类加载器:" + Math.class.getClassLoader());
    System.out.println("加载User的类加载器:" + User.class.getClassLoader());
    System.out.println("加载Integer的类加载器:" + Integer.class.getClassLoader());
}

在这里插入图片描述

四. 自定义类加载器打破双亲委派机制

类加载器采用双亲委派机制,即优先将类加载请求传递给父类加载器,如果父类加载器无法加载,则由当前类加载器进行加载。这种机制可以保证类的安全性和可靠性,但有时需要通过自定义类加载器打破双亲委派机制,以实现一些特殊的需求。

要实现自定义类加载器打破双亲委派机制,需要创建一个继承自ClassLoader的自定义类加载器,并重写findClass()方法,该方法可以根据自定义的规则进行类的查找加载。在加载过程中,可以使用defineClass()方法将字节数组转换为Class对象,以实现类的加载。

以下是一个简单的例子,演示如何通过自定义类加载器打破双亲委派机制

public class User {

  public void sayHello() {
      System.out.println("Hello, World! User是由" + User.class.getClassLoader() + "加载的");
  }
}
public class MyClassLoader extends ClassLoader {

 /**
  * 定义一个类加载路径
  */
 private String classPath;

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

 @Override
 protected Class<?> findClass(String name) throws ClassNotFoundException {
     //从指定路径加载类
     byte[] data = loadClassData(name);
     if (data != null) {
         // 定义类
         return defineClass(name, data, 0, data.length);
     }
     throw new ClassNotFoundException();
 }

  
 private byte[] loadClassData(String name) {
     // 从指定路径加载类数据
     String fileName = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
     File file = new File(fileName);
     FileInputStream fis = null;
     ByteArrayOutputStream bos = null;
     try {
         fis = new FileInputStream(file);
         bos = new ByteArrayOutputStream();
         int len = 0;
         byte[] buf = new byte[1024];
         while ((len = fis.read(buf)) != -1) {
             bos.write(buf, 0, len);
         }
         return bos.toByteArray();
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
         try {
             if (fis != null) {
                 fis.close();
             }
             if (bos != null) {
                 bos.close();
             }
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
     return null;
 }

public static void main(String[] args) throws Exception {
     MyClassLoader myClassLoader = new MyClassLoader("D:\\test");
     System.out.println("自定义类加载器的父级类加载器=" + myClassLoader.getParent());

     Class<?> clazz = myClassLoader.loadClass("com.maker.jvm.User");
     Object obj = clazz.newInstance();
     Method method = clazz.getMethod("sayHello");
     method.invoke(obj);
  }
}

运行结果:
在这里插入图片描述
在这个示例代码中,我们自定义了一个类加载器MyClassLoader,它继承自ClassLoader类,并实现了findClass()方法。在findClass()方法中,我们从指定路径加载类,并用defineClass()方法定义类。
这样,我们就可以通过自己的类加载器加载指定路径中的类。同时,我们还可以通过重载loadClass()方法打破双亲委派机制,从而实现自己的加载策略。

JVM系列博文:
【JVM】第一篇 从JDK源码级别彻底剖析JVM类加载机制
【JVM】第二篇 JVM内存模型深度剖析与优化
【JVM】第三篇 JVM对象创建与内存分配机制深度剖析
【JVM】第四篇 垃圾收集器ParNew&CMS底层三色标记算法详解
【JVM】第五篇 垃圾收集器G1和ZGC详解

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

搬砖界的小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值