类加载子系统


内存结构概述

内存结构图

内存结构详图

假设我们想自己手写一个Java虚拟机的话,必须考虑哪些结构?

  • 类加载器
  • 执行引擎

类加载子系统

类加载子系统作用

类加载子系统

  • 类加载器子系统负责从文件系统或者网络中加载 Class 文件,class 文件在文件开头有特定的文件标识。

  • ClassLoader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。

  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量

    和数字常量(这部分常量信息是 Class 文件中常量池部分的内存映射)

类加载器 ClassLoader 角色

类加载实例

  • class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到 JVM 当中来根据这个文件实例化出n个一模一样的实例。

  • class file 加载到 JVM 中,被称为DNA 元数据模板,放在方法区。

  • 在 .class 文件 –> JVM –> 最终成为元数据模板,此过程就要一个运输工具(类装载器 Class Loader),扮演一个快递员的角色。

类的加载过程

  • 加载 –> 链接(验证 –> 准备 –> 解析) –> 初始化

  • 在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定(也成为动态绑定或晚期绑定)

    类的加载过程

加载

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

加载 .class 文件的方式

  • 从本地系统中直接加载

  • 通过网络获取,典型场景:Web Applet

  • 从 zip 压缩包中读取,成为日后 jar、war 格式的基础

  • 运行时计算生成,使用最多的是:动态代理技术

  • 由其他文件生成,典型场景:JSP 应用从专有数据库中提取.class文件,比较少见

  • 从加密文件中获取,典型的防 Class 文件被反编译的保护措施

链接(Linking)

  • 链接分为三个子阶段:验证 –> 准备 –> 解析

链接阶段

初始化(Initialization)

  • 初始化阶段就是执行类构造器方法**< clinit >()** 的过程

  • 此方法不需定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包

  • 含 static 变量的时候,就会有 < clinit >( ) 方法;如果当前类不存在 static 变量,那么它的字节码文件是不会存在 < clinit >( ) < clinit >() 方法中的指令按语句在源文件中出现的顺序执行

  • < clinit >() 不同于类的构造器。(关联:构造器是虚拟机视角下的 < init >() )

  • 若该类具有父类,JVM 会保证子类的 < clinit >() 执行前,父类的 < clinit >() 已经执行完毕

  • 虚拟机必须保证一个类的 < clinit >() 方法在多线程下被同步加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ClassInitTest {
private static int num = 1;

static {
num = 3;
number = 20;
System.out.println(num);
//System.out.println(number); //报错:非法的前向引用(可以赋值,但不能调用)
}

//linking 之prepare:number = 0 --> initial:20 --> 10
private static int number = 10;

public static void main(String[] args) {
System.out.println(ClassInitTest.num); //3
System.out.println(ClassInitTest.number); //10
}
}
//静态变量 number 的值变化过程如下
//1.准备阶段时:默认初始化 0
//2.执行静态代码块:20
//3.执行静态变量初始化:10

类加载器的分类

  • JVM 支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)

  • 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是 Java 虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

  • 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示

类加载器的类型

ExtClassLoader继承树

AppClassLoader继承树

虚拟机自带的加载器

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用 C/C++ 语言实现的,嵌套在 JVM 内部

  • 它用来加载 Java 的核心库(JAVA_HOME / jre / lib / rt.jar、resources.jar 或 sun.boot.class.path 路径下的内容),用于提供JVM 自身需要的类

  • 并不继承自 java.lang.ClassLoader,没有父加载器

  • 加载扩展类和应用程序类加载器,并作为他们的父类加载器(当他俩的爹)

  • 出于安全考虑,Bootstrap 启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader)

  • Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现

  • 派生于 ClassLoader 类

  • 父类加载器为启动类加载器

  • 从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre / lib / ext子目录(扩展目录)下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载

应用程序类加载器(系统类加载器,AppClassLoader)

  • Java 语言编写,由 sun.misc.LaunchersAppClassLoader 实现
  • 派生于 ClassLoader 类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量 classpath 或 系统属性 java.class.path 指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载的
  • 通过 classLoader.getSystemclassLoader( ) 方法可以获取到该类加载器

用户自定义类加载器

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类

的加载方式。

  • 为什么需要自定义类加载器?

    1. 隔离加载类

      在某个应用中需要使用中间件,这个中间件有自己的依赖的 jar 包,在同一个工程里面,如果引用多个框架的话,有可能会出现某些类的路径一样、类名也相同,这样就会出现类的冲突了,这个时候就需要做一个类的仲裁,像现在主流的容器类的框架一样,它们都会自定义类的加载器,实现不同的中间件隔离,避免类的冲突。

    2. 修改类加载的方式

      在整个类的加载过程中,bootstrap 引导类加载器是一定被使用的,用来加载系统需要的核心 API,除了 bootstrap 引导类加载器之外,其他的类加载器也不是必须的,我们可以根据实际情况中修改类的加载方式,具体要用的时候我们再引用

    3. 扩展加载源

      加载的类除了可以在网络、本地物理磁盘、jar包去加载之外,我们还可以考虑通过数据库、电视机的机顶盒等等来扩展加载源

    4. 防止源码泄露

      当有了字节码文件或者没有反编译的手段,java代码是很容易被编译和篡改,所以,为了防止编译和篡改,我们可以对字节码文件进行加密,当我们需要运行这个字节码文件时候,我们需要解密来还原成内存中的类,而这个解密的操作,就需要自定义类的加载器来实现

  • 如何自定义类加载器?

    • 开发人员可以通过继承抽象类 java.lang.ClassLoader 类的方式,实现自己的类加载器,以满足一些特殊的需求

    • JDK1.2之前,在自定义类加载器时,总会去继承 ClassLoader 类并重写 loadClass( ) 方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖 loadClass( ) 方法,而是建议把自定义的类加载逻辑写在 findclass( ) 方法

    • 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URIClassLoader 类,这样就可以避免自己去编写findclass( ) 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

    • 代码举例

      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
      public class CustomClassLoader extends ClassLoader {
      @Override
      protected Class<?> findClass(String name) throws ClassNotFoundException {
      try {
      byte[] result = getClassFromCustomPath(name);
      if (result == null) {
      throw new FileNotFoundException();
      } else {
      return defineClass(name, result, 0, result.length);
      }
      } catch (FileNotFoundException e) {
      e.printStackTrace();
      }

      throw new ClassNotFoundException(name);
      }

      private byte[] getClassFromCustomPath(String name) {
      //从自定义路径中加载指定类:细节略
      //如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
      return null;
      }

      public static void main(String[] args) {
      CustomClassLoader customClassLoader = new CustomClassLoader();
      try {
      Class<?> clazz = Class.forName("One", true, customClassLoader);
      Object obj = clazz.newInstance();
      System.out.println(obj.getClass().getClassLoader());
      } catch (Exception e) {
      e.printStackTrace();
      }
      }
      }

ClassLoader

  • ClassLoader 类,它是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器
方法名称 描述
getParent( ) 返回该类加载器的超类加载器
loadClass(String name) 加载名称为 name 的类,返回结果为 java.lang.Class 类的实例
findClass(String name) 查找名称为 name 的类,返回结果为 java.lang.Class 类的实例
findLoadedClass(String name) 查找名称为 name 的已经被加载过的类,返回结果为 java.lang.Class 类的实例
defineClass(String name,byte[ ] b,int len) 把字节数组 b 中的内容转换为一个 Java 类,返回结果为 java.lang.Class 类的实例
resolveClass(Class<?> c) 连接指定的一个 Java 类
  • sun.misc.Launcher 它是一个java虚拟机的入口应用

获取 ClassLoader 的途径

  • 方式一:获取当前类的 ClassLoader

    clazz.getClassLoader()

  • 方式二:获取当前线程上下文的ClassLoader

    Thread.currentThread().getContextClassLoader()

  • 方式三:获取系统的 ClassLoader

    ClassLoader.getSystemClassLoader()

  • 方式四:获取调用者的 ClassLoader

    DriverManager.getCallerClassLoader()

双亲委派机制

原理

  • Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的 class 文件加载到内存中生成 class 对象。而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式

  • 工作原理

    1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行

    2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器

    3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

    4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常

双亲委派机制工作原理

  • SPI 接口是由引导类加载器加载的,接口具体的实现类是由线程上下文类加载器加载的,而线程上下文类加载器就是系统类加载器,所以我们在加载的时候,会先进行双亲委派,在引导类加载器加载 SPI 核心类,然后加载 SPI 接口,最后再反向委托,通过系统类加载器进行实现类 jdbc.jar 的加载

双亲委派实例

优势

  • 避免类的重复加载

  • 保护程序安全,防止核心 API 被随意篡改

    • 自定义类:java.lang.String 没有调用

    • 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)

      异常:java.lang.SecurityException: Prohibited package name: java.lang

沙箱安全机制

  • 自定义 String 类时:在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 jdk 自带的文件(rt.jar包中java.lang.String.class),报错信息说没有 main 方法,就是因为加载的是 rt.jar 包中的 String 类。这样可以保证对 java 核心源代码的保护,这就是沙箱安全机制。

其他

  • 如何判断两个 class 对象是否相同?

    • 在 JVM 中表示两个 class 对象是否为同一个类存在两个必要条件:
      1. 类的完整类名必须一致,包括包名
      2. 加载这个类的 ClassLoader(指ClassLoader实例对象)必须相同
  • 对类加载器的引用

    • JVM 必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的
    • 如果一个类型是由用户类加载器加载的,那么 JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中
    • 当解析一个类型到另一个类型的引用的时候,JVM 需要保证这两个类型的类加载器是相同的
  • 类的主动使用和被动使用

    • 主动使用,分为七种情况:

      1. 创建类的实例

      2. 访问某个类或接口的静态变量,或者对该静态变量赋值

      3. 调用类的静态方法

      4. 反射( 比如:Class.forName(“cn.sxt.Test”) )

      5. 初始化一个类的子类

      6. Java 虚拟机启动时被标明为启动类的类

      7. JDK7 开始提供的动态语言支持:java.lang.invoke.MethodHandle 实例的解析结果 REF_getStatic、REF putStatic、

        REF_invokeStatic 句柄对应的类没有初始化,则初始化

    • 除了以上七种情况,其他使用 Java 类的方式都被看作是对类的被动使用,都不会导致类的初始化,即不会执行初始化阶段不会

      调用 clinit( ) 方法和 init( ) 方法


文章作者: Yang Shiyu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yang Shiyu !
  目录