内存结构概述

简图

类加载子系统 - 图1

详细

类加载器子系统:加载、连接、初始化

类加载子系统 - 图2

类加载子系统 - 图3

类加载器与类的加载过程

  • 类加载机制:Java虚拟机把描述类的数据从字节码文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制
  • 类与类加载器:对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,也就是说:即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。(这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况)
  1. 类加载器子系统的作用

    类加载子系统 - 图4

    • 类加载器子系统负责从文件系统或者网络中加载字节码文件,字节码文件开头有特定的文件标识(是否以魔数0xCAFEBABE开头)。
    • ClassLoader只负责字节码文件的加载,至于它是否可以运行,则由Execution Engine决定。
    • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
  1. 类的加载过程
    1. Loading加载
    2. Linking连接
    3. Initialization初始化

加载

  • 在加载阶段,Java虚拟机需要完成的三件事情
    1. 通过一个类的全限定名来获取定义此类的二进制字节流。
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3. 内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  • 加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了
  • 第一步中的二进制字节流可以来自于哪里?

    类加载子系统 - 图5

连接

  1. 验证
    • 验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
    • 验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证
      • 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
      • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
      • 字节码验证:主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
      • 符号引用验证:符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError的子类异常,典型的如:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
  2. 准备

    • 准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

      public static int value = 123;

      在准备阶段过后 value的值是0;在后面的初始化完成后才会变为123

      类加载子系统 - 图6

      public static final int value = 123;

      但是被final修饰的类变量,在准备阶段就会初始化为123

    • 首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中

    • 在JDK 7及之前,HotSpot使用永久代来实现方法区时,这些变量所使用的内存都应当在方法区中进行分配;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆
  3. 解析

    • 解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程

      类加载子系统 - 图7

      符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容

      直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在

    • 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。

    • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANTClass_info、CONSTANT Fieldref info、CONSTANT Methodref info等

初始化

  • 初始化阶段就是执行类构造器**<clinit>()**方法的过程<clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物

    <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的

    num2声明在赋值之后,因为在连接的准备阶段,类变量已经赋值为0 了

    所以num2已经存在于内存中并完成了默认初始化

    所以在这里的初始化的时候 可以看到<clinit>()先赋值为20,再赋值为10

    类加载子系统 - 图8

    静态语句块中只能访问到定义在静态语句块之前的变量定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

    类加载子系统 - 图9

    <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成**<clinit>()**方法。

    类加载子系统 - 图10

  • 注意:<clinit>()不是类的构造方法,每个类都必然有一个构造方法,不写的话,就是有一个默认的无参构造器

    类的构造方法对应到<init>()

    类加载子系统 - 图11

  • Java虚拟机会保证在子类的**<clinit>()**方法执行前,父类的**<clinit>()**方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object。

    由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作

    类加载子系统 - 图12

    类加载子系统 - 图13

  • Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步

    如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的**<clinit>()**方法,其他线程都需要阻塞等待,直到活动线程执行完毕**<clinit>()**方法

    如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的

    因为在加载阶段,一个类就已经被加载进内存中了,后面可以直接使用,如果多线程都想要加载,也只需要加载一次

    类加载子系统 - 图14

类加载器分类

站在Java虚拟机的角度来看,只存在两种不同的类加载器:

  1. 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
  2. 另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader

站在Java开发人员的角度,自JDK 1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构,尽管这套架构在Java模块化系统出现后有了一些调整变动,但依然未改变其主体结构。

三层类加载器

类加载子系统 - 图15

类加载子系统 - 图16

  1. 启动类加载器

    • 负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中
    • 并不继承自java.lang.ClassLoader,没有父加载器
    • 加载扩展类和应用程序类加载器(在上面的代码中可以看到也是一个对象),并指定为他们的父类加载器
    • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
    • 启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可

      类加载子系统 - 图17

  2. 扩展类加载器

    • 这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的
    • 负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库
    • 用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,那么也会由扩展类加载器加载
    • 由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。

      类加载子系统 - 图18

  3. 应用程序类加载器(也称为系统类加载器)

    • 这个类加载器由sun.misc.Launcher$AppClassLoader来实现
    • 由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”
    • 负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
    • 如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

用户自定义类加载器

  1. 为什么需要自定义类加载器
    • 隔离加载类:如果引用其他组件,假如存在路径和类名相同的类,就会导致冲突
    • 修改类加载的方式:比如有些类不需要一开始就加载,可以由我们自己动态设置加载
    • 扩展加载源
    • 防止源码泄露
  2. 实现步骤

    类加载子系统 - 图19

ClassLoader的使用说明

  1. 常用方法介绍

    类加载子系统 - 图20

  2. 关于ClassLoader

    类加载子系统 - 图21

  3. 获取ClassLoader的方式

    类加载子系统 - 图22

双亲委派机制

  1. 双亲委派模型

    图中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”

    类加载子系统 - 图23

  2. 工作过程

    • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载
  3. 好处

    • Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系
    • 避免类的重复加载
    • 保护程序安全,防止核心API被篡改(沙箱安全机制)
    • 例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类
    • 反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱

      演示Object类不是应用程序类加载

      因为双亲委派机制的存在,会一直往上委托父类加载器,最终java.lang下的类会被启动类加载器加载进内存

      类加载子系统 - 图24

  4. 源码

    ClassLoager类中的loadClass方法

    类加载子系统 - 图25

  5. 被”破坏”的双亲委派模型

    类加载子系统 - 图26

    Rt.jar由启动类加载器进行加载,其中有一个核心类SPI(服务提供者接口(Service Provider Interface))

    如果我们需要加载JDBC,就需要SPI加载JDBC实现类的代码

    这种情况,Java为我们提供了线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器

    以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类。

    使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则

其他

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

    类加载子系统 - 图27