文章作用

  • 理解类从加载,连接,初始化到卸载的生命周期
  • 理解类加载,类加载器;理解双亲委派模型
  • 理解并掌握各种主动使用类的初始化实际

类加载生命周期(概述)

类从被加载到JVM开始,到卸载出内存,整个生命周期

类加载,连接和初始化 - 图1

  • 加载:查找并加载类文件的二进制数据
  • 连接:就是将已经读入内存的类的二进制数据合并到JVM运行时环境中去
    • 验证:确保被加载类的正确性
    • 准备:为类的静态变量分配内存,并初始化他们
    • 解析:把常量池中的符号引用转换成直接引用
  • 初始化:为类的静态变量赋初始值

类加载

  1. 通过类的全限定名来获取该类的二进制字节流
  2. 把二进制字节流转化为方法区的运行时数据结构
  3. 在堆上创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,并向外提供了访问方法区内数据结构的接口

加载类的方式

  • 最常见的方式:本地文件系统中加载,从jar等归档文件中加载
  • 动态方式:将java源文件动态编译为class文件
  • 其他方式:网络下载,从专有数据库中加载

类加载器

  • java程序不能直接引用启动类加载器,直接设置classLoader为null,默认就使用启动类的加载器
  • 类加载器并不需要等到某个类”首次主动使用”的时候才加载它,Jvm规范允许类加载器在预料到某个类将要被使用的时候就预先家在它
  • 如果在加载的时候.class文件缺失,会在该类首次主动使用时报告LinkageError错误,如果一直没有被使用,就不会报错

加载器类型

  • Java虚拟机自带的加载器
    • 启动类加载器 BootstrapClassLoader
      • 说明:用于加载启动的基础模块类,比如java.base,java.management,java.xml等等
    • 平台类加载器(jdk8以上) PlatformClassLoader
      • 说明:用于加载一些平台相关的模块
    • 扩展类加载器(jdk8及以下)ExtensionClassLoader
      • 说明:8以后就改为PlatformClassLoader,因为ExtensionClassLoader加载器会加载ext文件夹下的定义的加载器,这样做并不安全
    • 启动类加载器 AppClassLoader
      • 说明: 用于加载应用级别的模块
  • 用户自定义的加载器:是java.lang.ClassLoader的子类,用户可以定制类的加载方式;只不过自定义类加载器其加载的顺序是在所有系统类加载器的最后

类加载器的关系

类加载,连接和初始化 - 图2

双亲委派模型

  • 双亲委派模型对于保证Java程序的稳定运作很重要
  • 实现双亲委派的代码在java.lang.ClassLoader的loadClass()方法中,如果自定义类加载器的话,推荐覆盖实现findClass()方法

Jvm中的ClassLoader通常采用双亲委派模型,要求除了启动类加载器外,其余的类加载器都应该有自己的父级加载器.这里的父子关系是组合而不是继承,工作过程如下图

类加载,连接和初始化 - 图3

破坏双亲委派模型

  • 双亲模型有个问题:父加载器无法向下识别子加载器加载的资源
  • 为了解决这个问题,引入了线程上线文类加载器,可以通过Thread的setContextClassLoader()进行设置
  • 另外一种典型情况就是实现了热替换,比如OSGI的模块化热部署,它的类加载器就不再是严格按照双亲委派模型,很多可能就在平级的类加载器中执行了

类连接

验证

  • 类文件结构检查:按照JVM规范规定的类文件结构进行
  • 元数据验证:对字节码描述的信息进行语义分析,保证其符合Java语言规范要求
  • 字节码验证:通过对数据流和控制流进行分析,确保程序语义是合法和符合逻辑的.这里主要对方法体进行校验
  • 符号引用验证:对类自身以外的信息,也就是常量池中的各种符号引用,进行匹配校验

准备

为类的静态变量分配内存,并初始化他们

解析

所谓解析就是把常量池中的符号引用转换成直接引用的过程,包括:符号引用:以一组无歧义的符号来描述所引用的目标,与虚拟机的实现无关

下面关于符号引用和直接引用的说明来自《深入理解 Java 虚拟机》7.3.4 节

符号引用(Symbolic References)

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
例如下图(javap 反编译后的文件)中的红框就是符号引入
类加载,连接和初始化 - 图4

直接引用(Direct References)

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

解析的动作主要针对:

  • 类和接口接口
  • 字段( CONSTANT_Fieldref_info )
  • 类方法( CONSTANT_Methodref_info )
  • 接口方法( CONSTANT_InterfaceMethodref_info )
  • 方法类型( CONSTANT_Method_info )
  • 方法句柄( CONSTANT_MethodHandle_info )
  • 调用点限定符( CONSTANT_InvokeDynamic_info )

类的初始化

类的初始化就是为类的静态变量赋初始值,或者说是执行类构造器方法的过程

  1. 如果类还没有加载和连接,就先加载和连接
  2. 如果类存在父类,且父类没有初始化,就先初始化父类
  3. 如果类中存在初始化语句(例如static 块),就依次执行这些初始化语句
  4. 如果是接口
    • 初始化一个类的时候,并不会先初始化它实现的接口
    • 初始化一个接口时,并不会初始化它的父接口
    • 只有当程序首次使用接口里面的变量或者是调用接口方法的时候,才会导致接口初始化
  5. 调用Classloader类的loadClass方法来装载一个类,并不会初始化这个类,不是对类的主动使用

初始化时机

Java程序对类的使用方式分成:主动使用和被动使用,JVM必须在每个类或者接口首次主动使用时才初始化它们;被动使用类不会导致类的初始化,主动使用的情况有如下:

  1. 创建类实例
  2. 访问某个类或接口的静态变量
  3. 调用类的静态方法
  4. 反射类
  5. 初始化类的子类而父类还没有初始化
  6. JVM启动的时候运行的主类
  7. 定义了default方法的接口,当接口实现类初始化时

不会初始化的情况如下:

  1. 子类引用父类的静态字段,不会初始化子类
  2. 通过数组定义来引用类,不会触发初始化
  3. 访问静态常量

类的使用

就是字面意思使用,略过

类的卸载

JVM会自动判断并完成类卸载工作,了解即可

当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载

JVM自带的类加载器装载的类,是不会卸载的,有用户自定义的类加载器加载的类是可以卸载的