【JVM】类的生命周期

发布时间:2024年01月19日


类的生命周期

image.png
加载-----》连接(验证、准备、解析)------》初始化------》使用-------》卸载

加载

  1. 第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。
    • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础
    • 从网络中获取,最典型的应用是 Applet
    • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类
    • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator. generateProxyClass 生成字节码
  2. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中,生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。

image.png

  • _java_mirror 即 Java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 Java 使用
  • _super 即父类、_fields 即成员变量、_methods 即方法、_constants 即常量池、_class_loader 即类加载器、_vtable虚方法表、_itable 接口方法表
  1. 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象。作为该类在方法区中的各种数据的访问入口
    • image.png
  • 注意

    • 在Java虚拟机中,方法区是用于存储类的结构信息、常量池、静态变量、静态方法等数据的区域。其中包含了类的字节码、字段和方法的描述符、运行时常量池等。

    • 而java.lang.Class对象则表示一个类的结构信息,包含了类的名称、父类、接口、字段和方法等。在运行时可以通过它来获取和操作类的相关信息。例如创建类的实例对象、获取类的方法信息、字段信息。

    • Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互

      image.png
      简单讲解一下,一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding),对象头包括Mark Word和Klass Word。

  • Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等

  • Klass Word:类型指针,指向该对象的 Class 类对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;

所以 Person对象头里存储着Person.class的地址,在运行时可以通过它来获取和操作类的相关信息。例如创建类的实例对象、获取类的方法信息、字段信息。

连接

连接被分为三个阶段验证、准备、解析。

验证

验证内容是否满足《Java虚拟机规范》

  1. 文件格式验证,比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求。
  2. 元信息验证,例如类必须有父类(super不能为空)。
  3. 验证程序执行指令的语义,比如方法内的指令执行到一半强行跳转到其他方法中去。
  4. 符号引用验证,例如是否访问了其他类中private的方法等。

准备

为静态变量分配内存并赋初始值(final修饰的静态变量,会直接赋值**),**使用的是方法区的内存。

解析

主要是将常量池中的符号引用替换为直接引用。

  • 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和方法描述符(因为类还没有加载完,很多方法是找不到的)
  • 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中
    - image.pngimage.png

初始化阶段

  • 初始化阶段会执行静态代码块中的代码,并为静态变量赋值。
  • 初始化阶段会执行字节码文件中clinit部分的字节码指令。

image.png
image.png
还是按代码自上而下的顺序执行的
以下几种方式会导致类的初始化:

  1. 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。
  2. 调用Class.forName(String className)。
  3. new一个该类的对象时。
  4. 执行Main方法的当前类。

clinit指令在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的。

  1. 无静态代码块且无静态变量赋值语句。
  2. 有静态变量的声明,但是没有赋值语句。
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
    • 直接访问父类的静态变量,不会触发子类的初始化。
    • 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。

image.png
image.png
类实例化过程:父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数

卸载阶段

时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java 虚拟机进程终止
卸载类即该类的 Class 对象被 GC,卸载类需要满足3个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成

在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类

面试题

介绍一下类加载的过程


一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载
(Loading)、验证(Veri?cation)、准备(Preparation)、解析(Resolution)、初始化
(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统 称为连接(Linking)。这七个阶段的发生顺序如下图所示。
image.png
一、加载
在上述七个阶段中,包括了类加载的全过程,即加载、验证、准备、解析和初始化这五个阶段。 一、加载
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了,方法区中的数据存储格式完全由虚拟机实现自行定义,《Java虚拟机规范》未规定此区域的具体数据结 构。类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对 象将作为程序访问方法区中的类型数据的外部接口。
二、验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证阶段大致上会 完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

  1. 文件格式验证:

第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

  1. 元数据验证:

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求。

  1. 字节码验证:

第三阶段是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
4. 符号引用验证:
符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验, 通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
三、准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值 的阶段。从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是 一个逻辑上的区域,在JDK7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概 念的。而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。
四、解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现, 那解析阶段中所说的直接引用与符号引用又有什么关联呢?

  • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class 文件格式中。
  • 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实 例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中 存在。

五、初始化
类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户 应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到 初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序 编码制定的主观计划去初始化类变量和其他资源。我们也可以从另外一种更直接的形式来表达:初始化 阶段就是执行类构造器() 方法的过程。() 并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。

介绍一下对象的实例化过程

对象实例化过程,就是执行类构造函数对应在字节码文件中的() 方法(实例构造器),()
方法由非静态变量、非静态代码块以及对应的构造器组成。

  • () 方法可以重载多个,类有几个构造器就有几个() 方法;
  • () 方法中的代码执行顺序为:父类变量初始化、父类代码块、父类构造器、子类变量初始化、子类代码块、子类构造器。

静态变量、静态代码块、普通变量、普通代码块、构造器的执行顺序如下图:
image.png
具有父类的子类的实例化顺序如下:
image.png

文章来源:https://blog.csdn.net/m0_69171095/article/details/135697375
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。