【JVM】类的加载

类的生命周期

主要有七步:
16456917973407

类加载过程

系统加载 Class 类型文件主要分3步:加载->连接->初始化。连接过程又可分为:验证->准备->解析。总体共5步。

加载

加载需要完成的操作

在加载类时,Java 虚拟机必须完成以下3件事情:

  1. 通过类的全名,获取类的二进制字节流
  2. 将二进制字节流所代表的静态存储结构转化为方法区内的运行时数据结构(Java 类模型)
  3. 创建 java.lang.Class 类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

二进制流的获取

虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:“通过全类名获取定义此类的二进制字节流” 并没有指明具体从哪里获取、怎样获取。比如:除了最常见的直接加载文件系统中的 .class 文件,比较常见的就是:

  • 从 ZIP 包中读取(日后出现的 JAR、EAR、WAR 格式的基础)
  • 其他文件生成(典型应用就是 JSP)

等等。

类型数据与class实例

1. 类型数据

加载阶段结束后,所加载的二进制流就会按照虚拟机所设定的数据存储格式存储在方法区(JDK 1.8之前:永久代;JDK 1.8之后:元空间)

2. Class 对象

类将 .class 文件加载至元空间后,会在堆内存中创建一个 java.lang.Class 类的对象,用来封装类位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象。

这个 Class 对象是程序访问方法区中的类型数据(方法、字段等)的外部接口,也是实现反射的关键入口。

数组类的加载

数组类本身并不是由类加载器负责创建,它是由 JVM 直接在内存中动态构造出来的。但数组的元素类型仍然需要依靠类加载器去创建。

数组的元素类型与组件类型
  • 元素类型指的是数组去掉所有维度的类型,它最终还是要靠类加载器来完成加载。
  • 组件类型指的是数组去掉一个维度

如果数组的组件类型是引用类型,那么递归采用本部分定义的加载过程去加载这个组件类型。该数组将被标识在加载该组件类型的类加载器的类名称空间上。

如果数组的组件类型不是引用类型,该数组会与引导类加载器关联。

如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public

验证

验证的目的是保证加载的字节码是合法合理且符合规范的。

验证的内容则涵盖了类数据信息的格式验证、元数据验证、字节码验证,以及符号引用验证等。

准备

准备阶段的作用是为类的静态变量分配内存,并将其初始化为默认值。

类变量使用的内存都应该在方法区中分配,但是方法区只是一个逻辑区域,

  • 在JDK 7及之前,HotSpot使用永久代来实现方法区的时候,实现上完全符合这种逻辑概念;
  • 从JDK 7开始,将原本放在永久代的字符串常量池、静态变量移出至堆内存,因此类变量则会随着Class对象一起存放于Java。而在JDK 8 后方法区实现由永久代替换为元空间(本地内存),类变量不在实际的方法区了。

方法区、永久代、元空间的变化史:
https://www.zhihu.com/question/50258991

实例变量初始值随着Class对象存放在堆中,“通常情况”的初始值如下图:

基本数据类型的零值

那么特殊情况时,比如定义了 public static int value=111 这种final类型的类变量,那么value变量在准备阶段的初始值就是111了。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

  • 符号引用:一组符号来描述目标,可以是任何字面量
  • 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

初始化

初始化阶段是执行初始化方法 <clinit> () 方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

<clinit> () 方法注意:

  • 该方法仅能由 Java 编译器生成并由 JVM 调用。
  • 该方法是由编译器自动收集类中所有类变量的赋值语句静态语句块(static{}块)中的语句合并产生的。
  • 父类的该方法一定先于子类执行。换句话说,父类的 <clinit> () 优先级高于子类
  • 该方法不是一定会生成。如果一个类没有静态语句块或类变量赋值,那么编译器就不会为这个类生成 <clinit> () 方法
  • 执行接口的<clinit> ()方法不需要先执行父接口的该方法;此外,接口的实现类在初始化时一样不会执行接口的<clinit> ()方法。

类的卸载

卸载类即该类的 Class 对象被 GC。

卸载类需要满足 3 个要求:

  • 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象
  • 该类没有在其他任何地方被引用
  • 该类的类加载器的实例已被 GC

所以,在 JVM 生命周期内,由 JVM 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

jdk 自带的 BootstrapClassLoader(启动类加载器), ExtClassLoader(扩展类加载器), AppClassLoader(应用程序类加载器/系统类加载器)负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。

而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值