JVM学习笔记——类加载器与类加载过程
阅读原文时间:2023年07月08日阅读:1

类加载器与类加载过程

  类加载器 ClassLoader 用于把 class 文件装载进内存。

  • 启动类加载器(Bootstrap ClassLoader)

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

    • 用来加载 java 的核心类库,(rt.jar,resource.jar 或 sun.boot.class.path)路径下面的内容,提供JVM自身需要的类

    • 并不继承于 java.lang.ClassLoader,启动类加载器没有父类加载器

    • 加载 扩展类加载器 和 应用程序加载器 并指定它们的父类加载器

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

  • 扩展类加载器 (Extension ClassLoader)

    • java 语言编写,有 Launcher 的内部类

    • 派生于 ClassLoader 类

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

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

  • 应用程序加载器(Application ClassLoader)

    • java 语言编写,由 Launcher 的内部类

    • 派生于 ClassLoader 类

    • 父类加载器为扩展类加载器

    • 负责加载环境变量 classpath 或系统属性,java.class.path 指定路径下的类库

    • 该类加载是程序中默认的类加载器,一般来说,Java应用类都是由它来完成

  • 用户自定义类加载器(User Defined ClassLoader)

  • 自定义类加载器通常需要继承 ClassLoader

  • 哪些情况下需要自定义类加载器:

  • 隔离加载类,在某些框架内进行中间件与应用的模块隔离,把类加载到不同环境

  • 修改类加载的方式

  • 扩展加载源

  • 防止源码泄漏,Java 代码容易编译和篡改,可以进行编译加密

      JVM 的类加载机制有三种,目前 HotSpot 默认使用双亲委派机制:

  • 全盘负责机制:当一个类加载器负责加载某个类时,若没有显示使用另外一个类加载器来载入,则默认该类所依赖和引用的其他类也由该类加载器负责载入

  • 双亲委派机制:先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类

  • 缓存机制:缓存机制会缓存所有加载过的类,当程序需要使用某个类时,类加载器先从缓存区中搜寻该类,只有缓存区中不存在该类时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中

双亲委派机制

  当类加载器收到了类加载请求时,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归。

  本质:规定了类加载的顺序,引导类加载器先加载,若加载不到,由扩展类加载器加载,再加载不到,由应用程序加载器加载。

优点:

  • 可以避免重复加载,当父类加载器已经加载了该类的时候,就没有必要再加载一次
  • 安全性更高,采用双亲委派机制 Java 核心 api 库不会被随意篡改,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java api 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class

缺点:

  顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类,系统类访问应用类就出现了问题。比如,在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在这个启动类加载器,这时,就出现了该工厂无法创建由应用类加载器的应用实例问题

  Java 虚拟机规范并没有明确要求类加载器的加载机制一定使用双亲委派模型,只是建议采用这种方式。

沙箱安全机制

  Java 安全模型的核心就是 Java 沙箱,沙箱是一个限制程序运行的环境,沙箱机制就是将Java 代码限定在虚拟机特定的运行范围中, 并且严格限制代码对本地系统资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问。所有的 Java 程序都可以指定沙箱,可以定制安全策略。

  在 JDK 1.6 的最新安全模型中,引入了域(Domain) 的概念,虚拟机会把所有代码加载到不同的系统域和应用域中,系统域负责与关键资源交互,应用域通过系统域的代理实现对资源的访问。不同的受保护域,对应不同的权限。本质是把程序划分到不同的权限组中执行

优点:

  • 保证程序安全

  • 保护 Java 原生 JDK 代码不被篡改

组成沙箱的基本组件:

  • 字节码校验器:在类加载的过程中进行字节码校验,确保 Java 类文件的语言规范,核心类不进行字节码校验

  • 类装载器:类装载器对沙箱的作用

  • 防止恶意代码干涉其他代码(双亲委派机制)

  • 保护被信任类库的边界(双亲委派机制)

  • 将代码归入保护域,确定代码可以执行那些操作(沙箱安全机制)

    1. 加载

      将类的 class 文件读入内存,并将这些静态数据转换成方法区的运行时数据结构,然后创建一个代表这个类的 java.lang.Class 对象

类加载器加载 Class 流程如下:

2. 链接

  将类的二进制数据合并到 JRE 中

  1. * 验证:确保加载信息符合 JVM 规范,保证虚拟机安全

    • 准备:为类的静态变量在方法区分配内存,并设置类变量默认初始值

    • 解析:将虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)

    3. 初始化

      初始化完全由虚拟机主导和控制。到了初始化阶段才真正执行 Java 代码。类初始化的主要工作是为静态变量赋程序设定的初值。也就是执行类构造器 <clinit>()方法的过程

  2. * 当初始化一个类时,如其父类还未初始化,则需先触发父类初始化

    • 虚拟机负责<clinit>()方法在多线程中的安全性和同步问题
  • 类的主动引用一定会发生类的初始化

    • 虚拟机启动时,启动类被初始化,如 main 方法所在的类

    • 创建类的实例,new 一个对象

    • 调用类的静态成员(除 final 常量)和静态方法

    • 反射,使用 java.lang.reflect 包的方法对类进行反射调用

    • 初始化一个父类未被初始化的子类,先初始化父类

  • 类的被动引用不会发生类的初始化

    • 访问静态域时,只有当声明这个域的类时才会发生初始化。eg:当子类引用父类的静态变量时不会发生初始化

    • 通过数组定义类引用

    • 引用常量不会发生该类的初始化,常量在链接阶段就存入常量池中