首页
编程随笔
Java笔记
Html/Css/Js
Android
后端笔记
服务器搭建
BUG收集
Java异常
Android异常
在线工具
Json格式化
编码/解码
Epub在线编辑
登录
发布文章
个人文章
退出登录
首页
技术教程
BUG收集
在线工具
资源下载
登录
发布文章
退出登录
搜索
当前位置:
首页
-
博客
- 正文
关闭
JVM之类的生命周期
更新时间:2022-08-14 16:34:41
阅读数:700
发布者:落幕
## 类的生命周期 类的生命周期包括 5 个阶段:加载、链接、初始化、使用和卸载。 其中验证、准备、解析统称为链接(Linking) ![类的生命周期](https://www.speechb.com/blog/jvm/%E7%B1%BB%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.jpg "类的生命周期") ### 加载阶段 从程序使用过程来看: - 通过一个类的全限定(Fully Qualified Name)(一个类的全限定名是将类全名的.全部替换为/例如com/speechb/User.class)获取定义此类的二进制字节流。 - 将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构。 - 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 - 类将.class文件加载至元空间后,会在堆中创建一个java.lang.Class对象,用来封装类位于方法区内的数据结构。该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。 ![类的加载过程](https://www.speechb.com/blog/jvm/类的加载过程.jpg "类的加载过程") ### 二进制流的获取方式 对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。
(只要所读取的字节码符合 JVM 规范即可)
- 虚拟机可能通过文件系统读入一个 class 后缀的文件
(最常见)
- 读入 jar、zip 等归档数据包,提取类文件。 - 事先存放在数据库中的类的二进制数据 - 使用类似于 HTTP 之类的协议通过网络进行加载 - 在运行时生成一段 class 的二进制信息等 - 在获取到类的二进制信息后,Java 虚拟机就会处理这些数据,并最终转为一个 java.lang.Class 的实例。 如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。 ### 2.3. 类模型与 Class 实例的位置 #### 类模型的位置 加载的类在 JVM 中创建相应的类结构,类结构会存储在方法区(JDKl.8 之前:永久代;J0Kl.8 及之后:元空间)。 #### Class 实例的位置 类将.class 文件加载至元空间后,会在堆中创建一个 Java.lang.Class 对象,用来封装类位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象。 ![Class实例](https://www.speechb.com/blog/jvm/Class实例.jpg "Class实例") ```java Class clazz = Class.forName("java.lang.String"); //获取当前运行时类声明的所有方法 Method[] ms = clazz.getDecla#FF0000Methods(); for (Method m : ms) { //获取方法的修饰符 String mod = Modifier.toString(m.getModifiers()); System.out.print(mod + ""); //获取方法的返回值类型 String returnType = (m.getReturnType()).getSimpleName(); System.out.print(returnType + ""); //获取方法名 System.out.print(m.getName() + "("); //获取方法的参数列表 Class>[] ps = m.getParameterTypes(); if (ps.length == 0) { System.out.print(')'); } for (int i = 0; i < ps.length; i++) { char end = (i == ps.length - 1) ? ')' : ','; //获取参教的类型 System.out.print(ps[i].getSimpleName() + end); } } ``` 2.4 数组类的加载 创建数组类的情况稍微有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建数组类(下述简称 A)的过程: - 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组 A 的元素类型; - JVM 使用指定的元素类型和数组维度来创建新的数组类。 如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public。 ### 3. 过程二:Linking(链接)阶段 #### 3.1. 环节 1:链接阶段之 Verification(验证) 当类加载到系统后,就开始链接操作,验证是链接操作的第一步。
* 它的目的是保证加载的字节码是合法、合理并符合规范的。*
验证的步骤比较复杂,实际要验证的项目也很繁多,大体上 Java 虚拟机需要做以下检查,如图所示。 ![验证阶段的检查](https://www.speechb.com/blog/jvm/验证阶段的检查.jpg "验证阶段的检查") ##### 整体说明: 验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证,以及符号引用验证等。 -
*其中格式验证会和加载阶段一起执行*
。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。 -
*格式验证之外的验证操作将会在方法区中进行*
。 链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查。(磨刀不误砍柴工) ##### 具体说明: 1、格式验证:是否以魔数 0XCAFEBABE 开头,主版本和副版本号是否在当前 Java 虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等。 2、语义检查:Java 虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。比如: - 是否所有的类都有父类的存在(在 Java 里,除了 object 外,其他类都应该有父类) - 是否一些被定义为 final 的方法或者类被重写或继承了 - 非抽象类是否实现了所有抽象方法或者接口方法 - 字节码验证:Java 虚拟机还会进行字节码验证,
字节码验证也是验证过程中
3、最为复杂的一个过程}$。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如: - 在字节码的执行过程中,是否会跳转到一条不存在的指令 - 函数的调用是否传递了正确类型的参数 - 变量的赋值是不是给了正确的数据类型等 栈映射帧(StackMapTable)就是在这个阶段,用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。但遗憾的是,100%准确地判断一段字节码是否可以被安全执行是无法实现的,因此,该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的。
*在前面次检查中,已经排除了文件格式错误、语义错误以及字节码的不正确性。但是依然不能确保类是没有问题的。*
4、符号引用的验证:校验器还将进符号引用的验证。Class 文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此,在验证阶段,
虚拟机就会检查这些类或者方法确实是存在的
,并且当前类有权限访问这些数据,如果一个需要使用类无法在系统中找到,则会抛出 NoClassDefFoundError,如果一个方法无法被找到,则会抛出 NoSuchMethodError。此阶段在解析环节才会执行。 ### 3.2. 环节 2:链接阶段之 Preparation(准备) 准备阶段(Preparation),简言之,为类的静态变分配内存,并将其初始化为默认值。 当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。Java 虚拟机为各类型变量默认的初始值如表所示。 | 类型 | 默认初始值 | | ------------ | ------------ | | byte | (byte)0 | | short | short | | int | 0| | long | 0L | | float | 0.0f | | double | 0.0 | |char | \u0000 | | boolean | false | | reference | null | Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是 0,故对应的,boolean 的默认值就是 false。 注意 -
这里不包含基本数据类型的字段用修饰的情况,因为在编译的时候就会分配了,准备阶段会显式赋值。
```java // 一般情况:static final修饰的基本数据类型、字符串类型字面量会在准备阶段赋值 private static final String str = "Hello world"; // 特殊情况:static final修饰的引用类型不会在准备阶段赋值,而是在初始化阶段赋值 private static final String str = new String("Hello world"); 注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。 ``` - 在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。 ### 3.3. 环节 3:链接阶段之 Resolution(解析)
在准备阶段完成后,就进入了解析阶段。解析阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用。
#### 具体描述: 符号引用就是一些字面量的引用,和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在 Class 类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下 println()方法被调用时,系统需要明确知道该方法的位置。 #### 举例: 输出操作 System.out.println()对应的字节码: invokevirtual #24
![输出语句的符号引用](https://www.speechb.com/blog/jvm/输出语句的符号引用.jpg "输出语句的符号引用") 以方法为例,Java 虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。
通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。
### 过程三:Initialization(初始化)阶段 #### 4.1. static 与 final 的搭配问题 **说明:**使用 static+ final 修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值? - 情况 1:在链接阶段的准备环节赋值 - 情况 2:在初始化阶段
()中赋值 **结论:** 在链接阶段的准备环节赋值的情况: - 对于基本数据类型的字段来说,如果使用 static final 修饰,则显式赋值(直接赋值常量,而非调用方法通常是在链接阶段的准备环节进行 - 对于 String 来说,如果使用字面量的方式赋值,使用 static final 修饰的话,则显式赋值通常是在链接阶段的准备环节进行 - 在初始化阶段<clinit>()中赋值的情况: 排除上述的在准备环节赋值的情况之外的情况。 **最终结论:**使用 static+final 修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类到或 String 类型的显式财值,是在链接阶段的准备环节进行。 ```java public static final int INT_CONSTANT = 10; // 在链接阶段的准备环节赋值 public static final int NUM1 = new Random().nextInt(10); // 在初始化阶段clinit>()中赋值 public static int a = 1; // 在初始化阶段
()中赋值 public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100); // 在初始化阶段
()中赋值 public static Integer INTEGER_CONSTANT2 = Integer.valueOf(100); // 在初始化阶段
()中概值 public static final String s0 = "helloworld0"; // 在链接阶段的准备环节赋值 public static final String s1 = new String("helloworld1"); // 在初始化阶段
()中赋值 public static String s2 = "hellowrold2"; // 在初始化阶段
()中赋值 ``` #### 4.2. <clinit>()的线程安全性 对于<clinit>()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。 正是因为
函数()带锁线程安全的
,因此,如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。 如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行<clinit>()方法了。那么,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息。