Java 虚拟机学习笔记 | 类加载过程和对象的创建流程

前言

创建对象是 Java 语言绕不开的话题,那么对象是如何创建出来的呢?我们今天就来聊一聊。对象创建第一步就是检查类是否加载,而类的加载又牵扯到类的加载过程。如果单说对象的创建而绕开类的加载过程,感觉有点牵强。干脆就将这2块内容结合起来一起讲。希望通过我的讲解带你搞懂类的加载过程和对象的创建。

在进行阅读前你需要先了解 Java 运行时数据区域 的相关概念,如果你已经了解可以直接向下阅读,如果对Java 运行时数据区域一无所知,请先一步我的另一篇博客 Java 虚拟机学习笔记 | 运行时数据区总结 一文进行了解。

对象的创建和类的加载执行流程

对象的创建

首先介绍一下对象的创建需要大致流程如下:

  1. 类加载检查
  2. 为新对象分配内存
  3. 为对象值初始化为零值
  4. 设置对象头
  5. 执行init方法

类加载检查: 当虚拟机遇到 new 指令的时候会去常量池中检查者个类的符号引用是否存在,进行类加载检查操作,如果没有执行就进行类加载过程。这里所说的 new 执行不是我们写代码时候的 new。这个new指令可以通过 javap进行反汇编进行查看。

如下所示的Demo.java的代码:
在这里插入图片描述
编译 Demo.java
在这里插入图片描述
将 Demo.class 字节码文件进行反汇编操作。
在这里插入图片描述
生成的反汇编内容如下:
0:new 就是虚拟机遇到的 new 指令。
在这里插入图片描述
为新对象分配内存: 如果该类已经进行了类加载,会在堆内存中为新对象分配一块内存空间。内存空间的大小在类加载的时候就已经确定了。分配内存空间方式有2种:1. 指针碰撞、2. 空闲列表。
采用那种方式进行,取决于垃圾收集器采用那种收集算法。如果是标记整理那么会采用指针碰撞的方式,如果是标记清除则会采用空闲列表的方式。

分配内存是一个高并发的操作,虚拟机在分配内存的时候通过2中方式来保证线程安全的问题:1. TLAB、 2. CAS+重试。
默认情况下会为每个线程预先在堆上分配一个内存空间,我们称之为 TLAB(Thread Local Allocation Buffer)线程本地分配缓存。当对象内存需要内存空间大于 TLAB 或者 TLAB用完是采用 CAS+重试的方式进行。

为新对象值初始化为零值:
分配完内存后,接下来的操作就是将对象成员属性值设置为零值。这样可以保证这些示例属性在不赋值的情况下就可以使用。需要注意的是这个操作不包含对象头。

设置对象头: 分配完内存后需要对对象头进行设置,类型指针(找到类的元信息)、对象GC分代年龄、线程持有的锁、锁的状态标志(否启用偏向锁) 对象头也被称之为 Mark Word。

对象在内存中的布局包含了3部分:对象头、实例数据、对齐填充。每一块包含的内容如下图所示:
在这里插入图片描述

执行init方法: 在执行完上述的操作后对象基本算是在我们的堆内存中创建完毕,但是对象真正的创建才刚刚开始。还需要执行 init方法,完成成员属性字段真实的赋值后才开始按照我们编写的代码的逻辑进行执行。

这里所说的 init 方法就是我们在上面进行反汇编代码中的 invokespecial 指令。如下图所示:
在这里插入图片描述

类加载过程

创建对象过程中第一步就是执行类加载检查,如果已经进行类的加载就继续执行对象创建过程,如果没有进行类的加载就会执行类的加载过程。具体执行流程如下:

  • 加载
  • 链接
  • 初始化

链接过程又可以拆分为:

  • 验证
  • 准备
  • 解析

加载: 这里说的加载和类加载不是一回事,加载时类加载的一个执行过程。加载这个动作是读取字节码的信息并将为该类创建一个Class对象到方法区中 也可以理解为 将.class文件加载到方法区中。另外还有一点就是加载和连接是同交叉进行的。

需要注意的是类的加载时通过类加载器执行的。在 Java 虚拟机中类加载器有一下几种:

  • BootstrapClassLoader(启动类加载器)
  • ExtensionClassLoader(扩展类加载器)
  • AppClassLoader(应用程序类加载器)
  • 自定义类加载器

在这里插入图片描述
类在通过类加载器进行加载的时候才用的是双亲委派模型,具体的定义就是:
当了进行加载的时候先判断该类是否被加载过,如果已经被加载的类直接返回。如果没有被加载过开始进行加载,加载的过程中会将加载的请求委派给它的父类加载器进行加载。如果父类加载器无法加载时,才由自己的加载器进行加载。最总的效果是我们的加载操作基本都由BootstrapClassLoader(启动类加载器) 执行。简单点理解就是:有啥事各自找各自的爸去做,老爸不做在由自己去做。我个人理解就是啃老模式

每个类都有与他对应的类记加载器,当父类加载器为null时,BootstrapClassLoader(启动类加载器)会作为该类的父类加载器。

这样做的好处是:保证了Java程序的稳定运行,可以避免类的重复加载,保证了 Java 的核心 API 不被篡改。例如:如果不使用双亲委派模型,我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

验证: 验证会对 文件格式、元数据、字节码、符号引用进行验证。确保我们的代码的正确性。
准备: 为类变量分配内存,并将类变量的值初始化为零值。这步和创建对象过程中 为新对象值初始化为零值 的操作类似。
解析: 将常量池内的符号引用替换为直接引用的过程
初始化: 初始化操作会执行类构造器,并且会对类变量进行真实的赋值操作。这步和创建对象中执行init方法类似。执行完之后就可以开始创建对象了。

类加载过程和对象的创建 具体执行流程图如下:
在这里插入图片描述
类加载过程中各阶段在在Java 运行时数据区域处理流程图如下:
在这里插入图片描述
类加载过程中各阶段在在Java 运行时数据区域处理流程具体介绍图如下:在这里插入图片描述
对象的创建各阶段在在Java 运行时数据区域处理流程图如下:
在这里插入图片描述

小结

关于类加载过程和对象的创建,不仅仅是它们如何创建这么简单。还有很多细节需要我们了解,例如:对象的创建在分配内存的时候需要通过 指针碰撞或空闲列表的方式进行分配;同时关于分配的线程安全问题采用 TLAB 和 CAS+重试的方式来保证;对象内存布局包含:对象头、示例数据、对其填充;类的加载过程中还涉及到类加载器的双亲委派模型来保证程序稳定运行。

参考文献

深入理解Java虚拟机 JVM 高级特性与最佳实践

发布了136 篇原创文章 · 获赞 502 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/ljk126wy/article/details/99204071
今日推荐