1 java类的生命周期(总体流程)

大致分为 类加载 使用(实例化) 卸载三个阶段

如果只说创建的话更偏向于下面两个阶段

  • 类构造器完成类初始化(赋予静态变量默认值),也即类加载阶段
  • 类实例化(分配内存、赋予默认值、执行定制化赋值操作),我们今天重点讲类实例化的步骤

image-20230402201850087

上图 类加载种 验证——准备——解析 称为 连接阶段 (除了解析外,其他阶段是顺序发生的,而解析可以与这些阶段交叉进行,因为Java支持动态绑定(晚期绑定),需要运行时才能确定具体类型;在使用阶段实例化对象)

上图主要注重 类加载的过程

实例化阶段可以被细分为如下过程(下面我们就要开始以这个图为核心讲解 类实例对象创建的过程

在这里插入图片描述

整个创建对量的流程可以这样表述

在这里插入图片描述

类的初始化和实例化

初始化(类加载)和实例化(使用时)是两个不同的阶段

类的初始化 <cinit>

是完成程序执行前的准备工作。在这个阶段,静态的(变量,方法,代码块)会被执行。同时在会开辟一块存储空间用来存放静态的数据。初始化只在类加载的时候执行一次

主要职责:

类的构造器调用(<cinit>),初始化相关静态代码块以及静态变量的赋值。
对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。

image-20230402203825107

类的实例化(实例化对象) <init>

是指创建一个对象的过程这个过程中会在堆中开辟内存,将一些非静态的方法,变量存放在里面。在程序执行的过程中,可以创建多个对象,既多次实例化。每次实例化都会开辟一块新的内存。(就是调用构造函数)

主要职责: 实例的构造器调用(init)、分配内存、属性值得定制化赋值机制。
类的实例化本身意义就是对象的概念,其实就是实例化对应的对象的过程。
实例对象内存的分配、实例对象参数的默认初始化+实例对象参数的实例化(就是按开发要求的实现调用,例如调用构造器等)。
此时一般处于在装载阶段的初始化完成之后,使用之前的阶段,接下来就要进行类的实例化操作。

image-20230402204432318

image-20230402211550454

在这里插入图片描述

2 类初始化检查

这里我们使用new 关键字创建对象,

Java中创建对象的方式还有好多种,比如反射,克隆,序列化与反序列化等等。

这些方式不一而同,但是经过编译器编译之后,对应到Java虚拟机中其实就是一条new(这里的new指令与前面提到的new关键字不同,这是虚拟机级别的指令)指令。

当Java虚拟机碰到一条new指令时,会首先根据这条指令所对应的参数去常量池中查找是否有该类所对应的符号引用,并判断该类是否已经被加载、解析、初始化过如果没有,首先要进行类加载与初始化。如果类已经加载和初始化,那么继续后续的操作。

这里假设DemoClass类还没有被加载与初始化,也就是方法区中还没有DemoClass的类型信息,这时需要进行DemoClass类的加载与初始化。

3 类的加载过程(非必须)

类加载从大的层面上可以分类三个阶段:加载、链接(链接和建在可能交替运行)、初始化

先总体概括一下

加载:通过类名获取类的二进制字节流是通过类加载器来完成的。其加载过程使用“双亲委派模型”

验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。

准备:为类变量(静态变量)在方法区分配内存,并设置零值。注意:这里是类变量,不是实例变量,实例变量是对象分配到堆内存时根据运行时动态生成的。

解析:把常量池中的符号引用解析为直接引用:根据符号引用所作的描述,在内存中找到符合描述的目标并把目标指针指针返回。

初始化:类的初始化过程是这样的:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。执行()方法(clinit是class initialize的简写)

java变量的初始化顺序?

https://zhuanlan.zhihu.com/p/29969102

  • 父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。
  • 可以发现static块和static变量的初始化顺序和它们声明的位置有关,先声明的先执行,普通块和普通变量的初始化顺序也是如此。

PS:实例化:在堆区分配内存空间,执行实例对象初始化,设置引用变量a指向刚分配的内存地址

3.1 加载

加载阶段主要干了三件事:

  • 根据类的全限定名获取类的二进制字节流。

  • 将二进制字节流所代表的静态存储结构转化为方法区中运行时数据结构。

  • 在内存中创建一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

具体到这里就是首先根据package.DemoClass全限定名定位DemoClass.class二进制文件,然后将该.class文件加载到内存进行解析,将解析之后的结果存储在方法区中,最后在堆内存中创建一个Java.lang.Class的对象,用来访问方法区中加载的这些类信息。

  • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
    • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
    • _super 即父类
    • _fifields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

注意

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror(java类的镜像 对 Person类来说就是 Person.class ,同时 堆内存中的Person.class 也指向 _java_mirror_)是存储在堆中
  • 可以通过 HSDB 工具查看

image-20230325144911203

3.2 链接

链接阶段也可以分为三个阶段: 验证、准备、解析

3.2.1 验证

验证类是否符合 JVM规范,安全性检查

​ 如 类的格式,魔数类型是不是 cofe babe

验证阶段完成的任务主要是确保class文件中字节流中包含的信息符合Java虚拟机的规范,虽然说得很简单,但是Java虚拟机进行了很多复杂的验证工作,总的来说可分为四个方面:

  • 文件格式验证

  • 元数据验证

  • 字节码验证

  • 符号引用验证

  • 具体到这里就是对于加载进内存的DemoClass.class中存储的信息进行虚拟机级别的校验,以确保DemoClass.class中存储的信息不会危害到Java虚拟机的运行。

3.2.2 准备

为 static 变量分配空间,设置默认值 (如int类型的 给他准备四个字节,默认为0)

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾(即存在方法去),从 JDK 7 开始,存储于 _java_mirror 末尾(即存在堆内存)
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成(<cint>v())
    • 静态变量赋值的动作发生在类的构造方法中,而类的构造发生在初始化阶段调用的
  • 如果 static 变量是 final 的基本类型,以及final字符串常量,那么编译阶段值就确定了赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

3.2.3 解析

将常量池中的符号引用解析为直接引用(能够确定内存中的精确地址以及内部状态)

3.3 初始化 (执行<cinit>()V 初始化)

所谓初始化就是执行类的构造方法也即 执行 <cinit>()V 方法的阶段

虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

有且只有五种情况必须对类进行初始化,这五种情况被称为“主动引用”,除了这五种情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”。

  • 第一种:遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或设置一个类的静态字段(static)时(被static修饰又被final修饰的,已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法时。
  • 第二种:使用Java.lang.refect包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发其初始化。
  • 第三种:当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  • 第四种:当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类。
  • 第五种:当使用JDK1.5支持时,如果一个java.langl.incoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
  • 其他:当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)【因为他们在初始化之就已准备完成了】不会触发初始化
  • 类对象.class 不会触发初始化【类加载时 产生的 _mirror 镜像】
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时

常见的不会触发初始化的引用方式(被动引用)

  • 通过子类引用父类的静态变量 只会初始化父类 不会初始化子类
  • 创建类的数组
  • 引用常量池内的变量

实验

class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}
class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

验证(实验时请先全部注释,每次只执行其中一个)

public class Load3 {
    static {
        System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        // 1. 静态常量(基本类型和字符串)不会触发初始化
        System.out.println(B.b);
        // 2. 类对象.class 不会触发初始化
        System.out.println(B.class);
        // 3. 创建该类的数组不会触发初始化
        System.out.println(new B[0]);
        // 4. 不会初始化类 B,但会加载 B、A
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        cl.loadClass("cn.itcast.jvm.t3.B");
        // 5. 不会初始化类 B,但会加载 B、A
        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
        Class.forName("cn.itcast.jvm.t3.B", false, c2);
        // 1. 首次访问这个类的静态变量或静态方法时
        System.out.println(A.a);
        // 2. 子类初始化,如果父类还没初始化,会引发
        System.out.println(B.c);
        // 3. 子类访问父类静态变量,只触发父类初始化
        System.out.println(B.a);
        // 4. 会初始化类 B,并先初始化类 A
        Class.forName("cn.itcast.jvm.t3.B");
    }
}

练习

从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化

public class Load4 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);
    }
}
class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;
}

a和b不会导致E初始化

而c会,因为 c时包装类型 底层会调用 包装操作 Integer.valueOf()会推迟到初试化阶段才执行

典型应用 - 完成懒惰初始化单例模式

public final class Singleton {
    private Singleton() { }
    // 内部类中保存单例
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的(类加载器保障线程安全)

4 分配内存

当类加载过程完成后,或者类本身之前已经被加载过,下一步就是虚拟机要为新生对象分配内存。

对象所需要的内存空间在类加载过程完成后就可以完全确定下来,为对象分配内存空间就相当于从堆内存中划分出一块合适的内存来,分配内存的主要方式有两种:指针碰撞空闲列表

选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

内存分配的两种方式

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是”标记-清除”,还是”标记-整理”(也称作”标记-压缩”),值得注意的是,复制算法内存也是规整的

image-20230403003028545

指针碰撞:这种方式将堆内存分为空闲空间与已分配空间,使用一个指针来作为二者之间的分界线,当要为新生对象分配内存空间的时候,相当于将指针向着空闲空间的方向移动一段与对象大小相等的距离,可见这种分配方式Java堆内存必须是规整的,所有空闲空间在一边,已分配空间在另外一边。

在这里插入图片描述

空闲列表:在虚拟机中维护一个列表,用来记录堆中哪一块内存是空闲可用的,在为新生对象分配内存时,从列表中寻找一块合适大小的可用内存块,分配完成后更新空闲列表,这种方式下堆内存的空闲空间与分配空间可以交错存在。

在这里插入图片描述

内存分配并发问题

由于创建对象的动作是十分频繁的,多线程可能存在多个线程同时申请为对象分配内存空间,这个时候如果不采取一定的同步机制,就有可能导致一个线程还未来得及修改指针,另一个线程就使用了原来的指针分配内存空间,因此衍生出来了两种解决方案:CAS配上失败重试、TLAB方式

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配, 这样就降低了同步锁的申请次数。

PS:TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区)是 Java 中内存分配的一个概念,它是在 Java 堆中划分出来的针对每个线程的内存区域,专门在该区域为该线程创建的对象分配内存。它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。TLAB 本质上还是在 Java 堆中的,因此在 TLAB 区域的对象,也可以被其他线程访问。

5 初始化零值

在为对象分配内存完成之后,虚拟机会将分配到的这块内存初始化为零值(不包括对象头),这样也就使得Java中的对象的实例变量可以在不赋初值的情况下使用,因为代码所访问当的就是虚拟机为这块内存分配的零值。

6 设置对象头

对象头就像我们人的身份证一样,存放了一些标识对象的数据,也就是对象的一些元数据,我们首先看一下对象的构成。

在这里插入图片描述

在初始化了零值之后,怎么知道对象是哪个类的实例,,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息,这些信息存放在对象头中。 另就需要设置指向方法区中类型信息的指针,对象Mark Word中相关信息的设置,就在这个阶段完成。

对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头实例数据对齐填充

Hotspot 虚拟机的对象头包括两部分信息第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

7 实例对象初始化(<init>)

这一步虚拟机将调用实例构造器方法(), 根据我们程序员的意愿初始化对象,在这一步会调用构造函数,完成实例对象的初始化。

从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来

看,对象创建才刚开始,<init>方法还没有执行,所有的字段都还为零。所以一般来说,执行 new

指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才

算完全产生出来

8 创建引用,入栈

执行到这一步,堆内存中已经存在被完成创建完成的对象,但是我们知道,在Java中使用对象是通过虚拟机栈中的引用来获取对象属性,调用对象的方法,因此这一步将创建对象的引用,并压如虚拟机栈中,最终返回引用供我们使用。

在这里就是讲对象的引入入栈,并返回赋值给dc,至此,一个对象被创建完成。

再次回顾一下整体流程吧

在这里插入图片描述

卸载

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

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

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

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

只要想通一点就好了,jdk自带的BootstrapClassLoader,PlatformClassLoader,AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

方法的执行顺序:父类的cinit()、子类的cinit()、父类的init()、子类的init()

内部细致的执行顺序划分:

父类的类变量的赋值动作、父类的静态代码块、

子类的类变量的赋值动作、子类的静态代码块、

父类的成员变量的赋值动作、父类的非静态代码块、父类的默认构造方法、

子类的成员变量的赋值动作、子类的非静态代码块、子类的默认构造方法