• JVM—类加载子系统


    JVM—类加载子系统

    JVM的类加载是通过ClassLoader及其子类来完成的。

    有哪些类加载器

    类加载器如下:

    类加载器

    • 启动类加载器(BootStrap ClassLoader):负责加载JAVA_HOME\lib目录或通过-Xbootclasspath参数指定路径中的且被虚拟机认可(rt.jar)的类库;
    • 扩展类加载器(Extension ClassLoader):负责加载JAVA_HOME\lib\ext目录或通过java.ext.dirs系统变量指定路径中的类库;
    • 应用程序类加载器(Application ClassLoader):负责加载用户路径classpath上的类库;
    • 自定义类加载器(Custom ClassLoader):加载应用之外的类文件;

    类加载器执行顺序

    类加载器执行顺序如下图:

    类加载器执行顺序

    1. 自底向上检查类是否已经加载:

      加载过程中会先检查类是否已被加载,从自定义加载器到BootStrap逐层检查,只要某个类加载器已加载某个类,就视为此类已加载,可以保证此类使得所有ClassLoader只加载一次;

    2. 自顶向下尝试加载类:由上层来逐层尝试加载此类。

    类加载时机与过程

    类加载的四个时机:

    1. 遇到new、getStatic、putStatic、invokeStatic四条指令;

      比如有如下类:

      public class MyTest {
          public static int hello;
          
          public static void testMethod(){
              
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

      当使用如下三种代码时,此类会被加载:

      //第一种
      MyTest.age;
      //第二种
      MyTest.testMethod();
      //第一种
      new MyTest();
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    2. 使用java.lang.reflect包方法对类进行反射调用

      比如:

      Class clazz = Class.forName("com.sjdwz.MyTest");
      
      • 1
    3. 初始化一个类,发现其父类还没初始化,要先初始化其父类;

    4. 当虚拟机启动时,用户需要指定一个主类main,需要先将主类加载。

    一个类的一生

    一个类的一生如下:

    一个类的一生

    类加载做了什么

    主要做了三件事:

    1. 根据类全限定名称,定位到class文件,以二进制字节流形式加载到内存中;
    2. 把字节流静态数据加载到方法区(永久代,元空间);
    3. 基于字节流静态数据,创建字节码Class对象。

    类加载途径

    类加载途径如下图:

    类加载途径

    自定义类加载器

    我们可以自定义类加载器,来加载D:\sjdwzTest目录下的lib文件夹下的类。

    步骤如下:

    1. 新建一个类MyTest.java

      package com.sjdwz.myclassloader;
      public class MyTest {
          public void sayHello(){
              System.out.println("hello world!");
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    2. 使用javac MyTest.java命令,将生成的MyTest.class文件放到D:\sjdwzTest\lib\com\sjdwz\myclassloader文件夹下

      注意:包路径不能错。

      编译的位置

    3. 自定义类加载器,继承ClassLoader,重写findClass()方法 ,调用defineClass()方法:

      /**
       * @Description 自定义类加载器
       * @Created by 随机的未知
       */
      public class SjdwzClassLoader extends ClassLoader {
          private String classpath;
      
          public SjdwzClassLoader(String classpath) {
              this.classpath = classpath;
          }
      
          @Override
          protected Class<?> findClass(String name) throws ClassNotFoundException {
              try {
                  //输入流,通过类的全限定名称加载文件到字节数组
                  byte[] classDate = getData(name);
                  if (classDate != null) {
                      //defineClass方法将字节数组数据 转为 字节码对象
                      return defineClass(name, classDate, 0, classDate.length);
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              }
              return super.findClass(name);
          }
      
          /**
           * 加载类的字节码数据
           * @param className
           * @return
           * @throws IOException
           */
          private byte[] getData(String className) throws IOException{
              String path = classpath + File.separatorChar +
                      className.replace('.', File.separatorChar) + ".class";
              try (InputStream in = new FileInputStream(path);
                   ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                  byte[] buffer = new byte[2048];
                  int len = 0;
                  while ((len = in.read(buffer)) != -1) {
                      out.write(buffer, 0, len);
                  }
                  return out.toByteArray();
              } catch (FileNotFoundException e) {
                  e.printStackTrace();
              }
              return null;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
    4. 测试类如下:

      public class SjdwzClassLoaderTest {
          public static void main(String[] args) throws Exception {
              //自定义类加载器的记载路径
              SjdwzClassLoader sjdwzClassLoader = new SjdwzClassLoader("D:\\sjdwzTest\\lib");
              Class<?> testClazz = sjdwzClassLoader.loadClass("com.sjdwz.myclassloader.MyTest");
              if(testClazz != null){
                  Object testObj = testClazz.newInstance();
                  Method sayHelloMethod = testClazz.getMethod("sayHello", null);
                  sayHelloMethod.invoke(testObj,null);
                  System.out.println(testClazz.getClassLoader().toString());
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

      输出如下:

      输出

    双亲委派与打破双亲委派

    什么是双亲委派

    当一个类加载器收到类加载任务,会先交给其父类加载器去完成。 因此,最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,子类才会尝试加载任务。

    为什么需要双亲委派

    主要考虑安全因素,双亲委派可以避免重复加载核心的类,当父类加载器已经加载了该类时,子类加载器不会再去加载。

    为什么还需要破坏双亲委派

    在实际应用中,双亲委派解决了Java基础类统一加载的问题,但是存在着缺陷。JDK中的基础类的方法作为典型的API被用户类用户调用,但是也存在API调用用户代码的情况,比如:SPI代码。这种情况就需要打破双亲委派模式。

    比如:数据库驱动DriverManager。以Driver接口为例,Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,由系统类加载器加载。这个时候就需要启动类加载器来委托子类来加载Driver实现,这就破坏了双亲委派。

    如何破坏双亲委派

    1. 重写ClassLoader的loadClass方法;

      在JDK1.2之后,新加了一个findClass方法让用户重写;

    2. SPI,父类委托子类加载器加载Class;

    3. 热部署和不停机更新用到的OSGI技术。

  • 相关阅读:
    阿里云服务器+Frp+Proxifier工具进行内网穿透
    Autosar代码阅读和调试方法
    Java回顾-网络编程
    Array.from()的作用
    基于STM32单片机的温湿度检测报警器(数码管)(Proteus仿真+程序)
    前端js获取key值方法
    FPGA TestBench编写学习
    【Linux】常见的用于定位网络问题的命令(概述)
    Linux的网络配置
    OpenHarmony 4.0 实战开发——分布式软总线解析:设备发现与传输
  • 原文地址:https://blog.csdn.net/qq_41243472/article/details/137254667