Class的生命周期

2022-12-02 10:45:51 浏览数 (1)

文章目录

  • 1加载
  • 2链接
    • 2.1 验证
      • 2.1.1元数据验证
      • 2.1.2字节码验证
      • 2.1.3 符号引用的验证
    • 2.2 准备
    • 2.3准备
  • 3初始化
  • 4使用
  • 5卸载回收

我们编写的Java代码是如何运行在我们的操作系统之上呢?

java 文件通过javac编译成class文件,这种代码我们称之为字节码(中间码), 由JVM去加载字节码这个过程。

官方说法:(这部分不需要去记,简单看看,千万别记)

程序运行时,执行引擎中的解析器将字节码解析为二进制的机器码来执行,在程序运行的期间,执行引擎中的即时编译器会针对热点代码,将该部分的字节码编译成机器码以获得更高的执行效率。 整个程序运行时,解析器和即时编译器的相互配合,使得Java程序几乎能够达到编译型语言一样的执行速度

像编译器就交给专业的人去做,大部分普通程序能够接触到的是JVM加载字节码这个过程。官方把这个过程成为类加载。

在进入Class File Loading之前,我们明确一下类加载流程的目的: 就是把javac 编译过的class字节码文件,通过加载,生成某种形式的class数据结构进入内存,程序可以调用这个数据结构来构造出object,这个过程是在运行时进行的,这个也是Java动态拓展性的基础。 类的整个生命周期如下图:

这个图片表现了一个类的生命周期,完整一点的话,我们可以在最开始加上javac编译阶段。而“类加载”只包括加载、连接、初始化三个过程。

需要区分“类加载”与“加载”,加载只是类加载的第一个环节。

解析部分是灵活的,它可以在初始化环节之后再进行,实现所谓的“后期绑定”,这点在讲到解析环节时候会详细讲。除解析过程外的其他加载过程必须按照如图顺序开始。

有兴趣的还可以顺便看看我的纸质笔记: 链接: JVM学习笔记-虚拟机类加载机制——(纸质笔记)

话不多说进入主题:

1加载

加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段, 在加载阶段,Java虚拟机需要完成以下三件事情: 1)通过一个类的全限定名来获取定义此类的二进制字节流。 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 (类的文件信息交给JVM) 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口 (类文件所对应的对象Class ---->JVM) 总结:加载是读取一个Class File二进制字符流,将其所代表的的静态存储结构转化为方法区的运行时数据结构,并在内存(堆)中生成一个便于用户调用的java.lang.Class类型的对象的一个过程。

在互联网环境,我们最重要的是保证我们系统的安全性,虽然JVM内存中已经存在这个对象, 但是这个时候对象需要经过一点小小的考验,如果通过考验,那么才能顺序加载。

代码语言:javascript复制
	加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,
	加载阶段尚未完成,连接阶段可能已经开始了,
	但是这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序。

2链接

2.1 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证文件格式、元数据、字节码等等 文件格式验证:此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。

(1)是否以魔数 0xCAFEBABE开头 (2)主、次版本号是否在当前虚拟机处理范围之内 。 (3)常量池的常量中是否有不被支持的常量类型(检査常量tag 标志)。 (4)指向常量的各种索引值中是否有指向不存在的常量或不符合装型的常量 。 (5)CONSTANT_Utf8_info型的常量中是否有不符合 UTF8编码的数据 (6)Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息

2.1.1元数据验证

保证不存在不符合 Java 语言规范的元数据信息。

(1)这个类是否有父类(除了 java.lang.0bject之外,所有的类都应当有父类) (2)这个类的父类是否继承了不允许被继承的类(被finaI修饰的类) (3)如果这个类不是抽象类, 是否实現了其父类或接口之中要求实现的所有方法 (4)类中的字段、 方法是否与父类产生了矛盾(例如覆盖了父类的final字段, 或者出現不符合规则的方法重载, 例如方法参数都一致, 但返回类型却不同等)

不需要去记:

为什么还需要校验元数据,有可能你在互联网里接受的类不是通过javac编译出来的,是黑客自己写的,所以还需要校验一次。保证类是能符合面向对象,封装继承多态的思想的。 第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息

2.1.2字节码验证

通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。

总结:简单概括就是对class静态结构进行语法语义上的分析,保证其不会产生危害虚拟机的行为,更深层次的理解会非常复杂,暂时不用去理解。

2.1.3 符号引用的验证

在解析阶段中发生,保证可以将符号引用转化为直接引用。解析阶段我们之前也提到过,它可以在初始化阶段之前或者之后进行,所以验证其实包含了很多步骤,分散在不同的阶段内。这点是在图中画得不太准确的,网上很多博客也没有提到,我们可以在官网里找到对应的说明。此外验证的内容也是会不断发展的,除了这里提到的文件格式验证,元数据验证,字节码验证,符号引用验证四个环节,从低版本的虚拟机到现在,验证的步骤其实已经不断加入了各种机制,那在元数据字节码验证通过后呢。

2.2 准备

为我们类中定义的变量赋予零值(初始值)。

准备阶段是正式为类中定义的变量(即静态[ˈstætɪk]变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这 种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述。

虚拟机内存规范中定义了方法区这种抽象概念,HotSpot虚拟机在JDK8之前使用了永久代这种具体的方法来实现方法区,JDK8及以后,弃用“永久代”这种实现方式,采用元空间这种直接内存来存储类的原信息等等,所以说,我们常常看到有人说JDK8及其以后采用“元空间”来替代”方法区“,这种说法是完全错误的,因为方法区是抽象概念,永久代和方法区是实现方式。 我们怎么去理解呢,JDK8之前,类的元信息、常量池、静态变量等都存放在永久代具体实现中,JDK8之后常量池、静态变量被移除“方法区” 转移了堆中,原信息依旧保存在方法区内,但是具体的存储方式改成了元空间。

理解难点:抽象概念与具体实现之间的区别。

2.3准备

符号引用验证通过之后:将常量池中的符号引用转为直接引用

解析的动作主要是针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行转换为直接引用,符号引用可以理解为上门讲那些常量的名字,直接引用就是内存中的真实地址,我们可以通过这个地址调用具体的方法等等。

当解析步骤完成之后,意味着整个连接部分已经完成,也就说加载外部Java文件已经成功引入到我们虚拟机当中。

当一个Java类被编译成Class之后呢,假如这个类成为A,并且A中引用了B,那么在编译阶段呢A是不知道B有没有被编译的,而且此时B一定也没有被加载,所以A肯定也不知道B中的实际地址,这个时候A怎么找到B呢,此时在A的class文件中,将使用一个字符串S来代表B的地址,S就被称为符号引用了。在运行时如果A 发生了类加载,到解析阶段会发现B还未被加载,那么将会触发B的类加载,将B加载到虚拟机中,此时A中的B的符号引用将会被替换成B的实际地址这被称为直接引用,这样A也就能真正的调用B了。

了解过多态的同学应该知道java通过后期绑定的方式来实现多态,那么后期绑定的这个概念是如何实现的呢?其实就是这里的动态解析,接着上面所说,如果A调用B是一个具体的实现类,那么就称为静态解析,因为解析的目标类很明确,而假如上层Java代码使用了多态,这里的A是一个抽象类或者是接口,他有两个具体的实现类C和D ,此时A的具体实现并不明确,当然也就不知道使用哪个具体类的直接引用来进行替换了,直到运行过程中发生了调用,此时虚拟机调用栈中将会得到具体的类型信息,这时候再进行解析,就能用明确的直接引用替代符号引用了。这是也是解析阶段有时候发生在初始化阶段之后,这就是动态解析,用它来实现了后期绑定和多态,底层对应了用的是invokevirtual或invokeinterface这条字节码指令, 如果没有听懂,可以先理解一下多态,后期绑定这些概念

总结: 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。

符号引用可以理解成上面讲的那14种常量的名字等等。

直接引用就是内存中的真实地址。

当解析步骤完成意味着整个连接部分的完成,这也就是说加载外部的java类已经成功的引入到我们的java虚拟机中了。

3初始化

为静态变量赋真正的值。

初始化阶段比较易于理解,就是会判断代码中的是否存在主动的资源初始化操作,如果有就执行。

这里所说的主动的资源初始化动作,不是指构造函数,而是class层面的,比如说成员变量的赋值动作,静态变量的赋值动作,以及静态代码块的逻辑,而只有显式的调用new指令,才会调用构造函数,进行对象的实例化,这是对象层面的,二者不要混淆。

只有你显式调用new 指令,才会调用构造函数,进行对象实例化,对象层面。

有几种类必须初始化的情况:

  1. new、putstatic、getstatic、invokestatic字节吗指令的时候,如果类尚未初始化,则需要触发初始化。
  2. 对类进行反射调用的,如果类没有初始化,则需要初始化。
  3. 虚拟机启动时候,用于指定一个包含main()的主类,虚拟机会先初始化这个类。
  4. 动态语言支持时, java.lang.invoke.MethodHandle 实例最后结果为REF_getStatic

4使用

这里不用说了吧,

代码语言:javascript复制
new Object()

5卸载回收

Class 回收要满足以下三个条件:

  1. No instance 该类所有的实例都已经被GC;
  2. No ClassLoader 加载该类的ClassLoader实例已经被GC;
  3. No Reference 该类的java.lang.Class对象没有被引用。(XXX.class, 静态变量/方法)

class 的生命周期总结:

只有加载步骤中的读取二进制流与初始化部分,能被上层开发者,也就大部分的Java程序员控制,而剩下的步骤,都是由JVM去掌控,其中的细节是由JVM开发人员处理,对于上层开发者者是一个黑盒。我们程序员实现了,热部署自定义加载的等等的功能

0 人点赞