【Java JVM】实例对象的创建

当我们涉及 Java 编程时, 对象的创建是一个基础而关键的概念。在 Java 中, 一切皆为对象, 而对象的创建方式直接影响代码的结构和性能。
本博客将探讨一下 Java 实例对象的创建过程。

1 创建对象的方法有哪些

在 Java 中如果要创建一个对象, 有哪些方式呢?

  1. 运用 new 关键字创建实例, 这是最常用的创建对象方法
  2. 通过反射, 调用 java.lang.Class 的 newInstance 方法, 相当于调用一个类的无参的构造函数创建对象
  3. 通过反射, 调用 java.lang.reflect.Constructor 类的 newInstance 方法, 支持无参/有参/私有的构造函数
  4. 通过对象的 clone 方法, 对象需要实现 java.lang.Cloneable 接口
  5. 通过反序列化, 对象需要实现 java.io.Serializable
  6. 通过 sun.misc.Unsafe 的 allocateInstance 方法

其中方法 1, 2, 3 本质都是通过类的构造函数创建对象, 就是 Java 的 new 机制。
而方法 4, 5, 6 不会调用构造函数。我们这里只讨论正常的构造函数创建对象的方式。

2 创建的过程

public class Demo {public static void main(String[] args) {Demo main = new Demo();}
}

上面是一个逻辑很简单, 就是通过 new 创建出了一个 Demo 的实例。
从 Java 层面这个对象的创建就完成了, 如何还需要进行深入分析的话, 我们需要进入到字节码的层面了。

对应如何将类文件转为字节码, 可以看一下后面的附录 1。

上面的 Demo 例子转为字节码后如下

Classfile /Users/lcn29/Projects/Demo/src/main/java/io/github/lcn29/Demo.classLast modified xxxx年xx月xx日; size 286 bytesSHA-256 checksum de9e200e3a5848520480df67e259986c46dca7342bbaf8f1b84b094815e04ee5Compiled from "Demo.java"
public class io.github.lcn29.Demominor version: 0major version: 65flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #7                          // io/github/lcn29/Demosuper_class: #2                         // java/lang/Objectinterfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:#1 = Methodref          #2.#3          // java/lang/Object."<init>":()V#2 = Class              #4             // java/lang/Object#3 = NameAndType        #5:#6          // "<init>":()V#4 = Utf8               java/lang/Object#5 = Utf8               <init>#6 = Utf8               ()V#7 = Class              #8             // io/github/lcn29/Demo#8 = Utf8               io/github/lcn29/Demo#9 = Methodref          #7.#3          // io/github/lcn29/Demo."<init>":()V#10 = Utf8               Code#11 = Utf8               LineNumberTable#12 = Utf8               main#13 = Utf8               ([Ljava/lang/String;)V#14 = Utf8               SourceFile#15 = Utf8               Demo.java
{public io.github.lcn29.Demo();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=2, args_size=10: new           #7                  // class io/github/lcn29/Demo3: dup4: invokespecial #9                  // Method "<init>":()V7: astore_18: returnLineNumberTable:line 6: 0line 7: 8
}
SourceFile: "Demo.java"

备注:
从上面的字节码内容中可以看到很多注释的内容 (// 后面的内容), 在实际的字节码中是不会有后面的注释内容的, 只有一个简单的 指令 #参数 (也可能没这个参数)

后面注释的内容是 javap 为了方便我们阅读, 提前帮我们把 #参数 的内容转换了, 即后面注释的内容就是 #参数 的真正内容。

#参数 的真正内容如何查找的?
这里的 #参数 可以看作是一个坐标, 通过这个指标可以到字节码文件的常量池中获取对应的内容, 即字节码文件中的 Constant pool 项。

比如: #7 在我们的字节码文件的 Constant pool 从中找到的内容是 #8, 同理通过 #8Constant pool 中最终获取到了内容 io/github/lcn29/Demo, 也就是 #7 的内容就是 io/github/lcn29/Demo

OK, 转为字节码后, 我们可以看到 JVM 创建对象的更多步骤。
下面我们就围绕这个字节码过程, 简单梳理一下 JVM 层面创建对象的过程。

2.1 检查类的加载

从 main 方法入手, 我们遇到的第一个字节码

new           #7                  // class io/github/lcn29/Demo

JVM 虚拟机遇到一条 new 指令时, 首先会去检查这个指令的后面参数是否能在运行时常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已被加载, 解析和初始化过。
如果没有, 那必须先执行相应的类加载过程。

类加载 的过程就不在这里展开了。

所以 new 的是 io/github/lcn29/Demo 这个类, 所以首先需要确保在内存中有这个类存在。

2.2 分配内存

类加载检查通过后, 接下来虚拟机将为新生对象分配内存。

一个对象需要分配内存的就 3 个部分

  1. 对象头 (Object Header) 的大小固定的
  2. 实例数据 (Instance Data) 的大小可以通过类的各个属性的大小计算出来
  3. 对齐填充 (Padding) 只需要在得到前 2 个的大小后, 保证整个对象为 8 个字节的倍数即可

所以一个对象所需内存的大小在类加载完成后便可完全确定, 这时就可以给这个对象分配内存空间。
这个过程实际就是把一块确定大小的内存从 Java 堆中划分出来。

2.2.1 内存分配方式

方式一
如果 Java 堆中内存是绝对规整的, 所有用过的内存都放在一边, 空闲的内存放在另一边, 中间放着一个指针作为分界点的指示器, 那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离, 这种分配方式称为 “指针碰撞 (Bump the Pointer)”

方式二
如果 Java 堆中的内存并不是规整的, 已使用的内存和空闲的内存相互交错, 那就没有办法简单地进行指针碰撞了, 虚拟机就必须维护一个列表, 记录哪些内存块是可用的, 在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录, 这种分配方式称为 “空闲列表 (Free List)”

至于选择哪种分配方式由 Java 堆是否规整决定。  
而 Java 堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理 (Compact) 功能决定。  
Serial, ParNew 等带压缩整理过程的收集器就使用指针碰撞, 基于 CMS 这种清除算法就使用空闲列表
2.2.2 内存分配的安全问题

对象的创建需要申请内存, 这个过程不是线程安全的。
如果现在正在给对象 A 分配内存, 临界指针/空闲列表的值还未改变, 这时候又要一个对象 B 进行
内存的申请, 那么就导致线程不安全。

为了解决这个问题, 有 2 种方式

  1. 对分配内存空间的动作进行同步处理, 虚拟机是可以通过 CAS 加上失败重试的方式保证更新操作的原子性
  2. 把内存分配的动作按照线程划分在不同的空间之中进行, 每个线程在 Java 堆中预先分配一小块内存, 称为本地线程分配缓冲 (Thread Local Allocation Buffer, TLAB), 哪个线程要分配内存, 就在哪个线程的本地缓冲区中分配, 只有本地缓冲区用完了, 分配新的缓存区时才需要同步锁定。

2.3 空间初始化

内存分配完成后, 虚拟机需要将分配到的内存空间都初始化为默认值 (不包括对象头), 如果使用了 TLAB 的话, 这一项工作也可以提前至 TLAB 分配时顺便进行。
这步操作保证了对象的实例字段在 Java 代码中可以不赋自定义值就可以直接使用, 使程序能访问这些字段的数据类型所对应的初始值。

各种数据类型的初始值:

类型默认值
booleanfalse(0)
char\u0000(unicode 编码, 转为十进制就是 0)
byte0
short0
int0
float0.0f
double0.0d
long0L
reference(引用类型)null

2.4 其他必要的设置

JVM 会对这个对象的对象头等相关的属性进行设置, 比如确定是哪个类的实例, 将 klass Pointer 指向对应的 Class, 设置对象的哈希码, 对象的 GC 分代年龄, 偏向锁标识等。

到了这一步, 字节码 new 引起的对象创建就完成。
但是这时创建出来的的对象所有的属性都是默认值, 还是一个未完整的对象的。

2.5 执行 <init> 方法

顺着字节码, 下一个 dup, 这个只是单纯的为了更方便地为后面的赋值操作而执行的,
本身不会改变到对象的任何属性, 所以跳过。

下一个 invokespecial #9 (Method “”😦)V) 字节码。

invokespecial 字节码, 可以先简单看作是调用后面参数指定的方法。

是 JVM 在编译时间, 根据我们的类生成的一个统一的属性初始化方法 (对应了上面的例子的public io.github.lcn29.Demo() 方法)。

举个例子:

public class A {private int num = 10;private int num2;  private int num3;{this.num2 = 20;}public Demo() {this.num3 = 100;} 
}

上面 A 有 3 个属性 num, num2, num3, 它们分别在 3 个地方被赋值了

  1. 声明赋值
  2. 代码块赋值
  3. 构造函数赋值

而编译为字节码后, 编译器会把所有的赋值操作都统一在自己生成的 方法中, 就像下面

public class A {private int num;private int num2;  private int num3;public <init>() {// 先调用父类的 <init> 方法, 确保父类的属性设置完成super.<init>();// 自己的属性赋值this.num = 10;this.num2 = 20;this.num3 = 100;}public Demo() {}
}

了解完 方法, 我们可以了解到 invokespecial #9 这条字节码指令的效果: 对自己的父类和属性进行真正的赋值。

到了这里, 一个真正完整的实例对象就创建完成。

后面的 astore_1 和 return 都不涉及到对象的情况的处理, 跳过。

至此一个 实例对象的创建就完成。

3 <init> 方法和 <clinit> 方法

方法是编译器生成的, 生成的字节码中一般都会有, 但是不一定就会执行。

一般来说 方法是否执行, 由 new 指令后面是否跟随 invokespecial 指令决定。
Java 编译器会在遇到 new 关键字的地方同时生成这 2 条指令, 如果不是通过 new 方式创建的, 则不会有。

在 Java 类的定义中, 除了正常的属性外, 我们还可以再类中定义静态属性, 同理编译器会将静态属性和静态代码块中的属性赋值, 统一到一个 方法中。

方法在我们创建类实例时调用, 而 则是在类加载时执行。

3.1 举个例子加深理解

public class Parent {private static int pNum1 = 10;private int pNum2 = 10;static {pNum1 = 11;}{pNum2 = 12;}public Parent() {this.pNum2 = 13;}
}public class Son extends Parent {private static int sNum1 = 20;private int sNum2 = 20;static {sNum1 = 21;}{sNum2 = 22;}public Son() {this.sNum2 = 23;}
}

当我们创建 Son 的实例的时候, 上面的构造函数, 代码块, 静态代码块的执行顺序是怎么样的?

上面的执行顺序差不多是这样的

  1. Parent 的静态变量赋值
  2. Parent 的静态代码块执行
  3. Son 的静态变量赋值
  4. Son 的静态代码块执行
  5. Parent 的实例变量赋值
  6. Parent 的代码块执行
  7. Parent 的构造函数执行
  8. Son 的实例变量赋值
  9. Son 的代码块执行
  10. Son 的构造函数执行

出现上面的执行顺序, 主要是由 <init><clinit> 造成的。

  1. <clinit> 主要针对我们当前类的初始化, 而 <init> 主要针对我们当前类的实例的初始化, 而且他的初始会先调用父级的无参 <init> 方法。
  2. 这里的初始化指的是类中的属性直接赋值执行, 代码块执行, 构造函数执行, 这三个执行最终会整合到<init>(实例相关的), 或者<clinit>(静态相关的), 并按照的执行顺序执行
  3. 类加载机制中, 会先加载父类, 再加载子类。

从 new Son() 时, 先加载 Parent 对应的类, 然后调用 Parent 的<clinit> 方法

  1. 在 Parent 的 <clinit> 方法

1.1 执行 Parent 的属性直接赋值, 给 pNum1 赋值为 10
1.2 执行 Parent 的静态代码块, 给 pNum2 赋值为 11
1.3 静态代码块和直接赋值没有层级关系, 谁在前谁先, 如果这时静态代码块在直接赋值前, 那么先给 pNum2 赋值 (代码块和实例属性也是遵循这个规则)

  1. 调用 Son 的 <clinit> 方法

2.1 执行 Son 的属性直接赋值, 给 sNum1 赋值为 20
2.2 执行 Son 的静态代码块, 给 sNum1 赋值为 21

  1. 执行 Son 的 <init> 方法

3.1 <init> 第一步会直接调用他的直接父级的 <init> 方法, 也就是 Parent 的 <init> 方法, 然后调用自身的代码块执行, 再构造函数执行 (Parent 的 <init> 还会调用父类的, 这里省略)
3.2 Parent 的 <init> 方法, 先执行属性直接赋值, pNum2 赋值为 10
3.3 Parent 的 <init> 方法, 执行 Parent 的代码块, pNum2 赋值为 12
3.3 Parent 的 <init> 方法, 执行 Parent 的构造函数, pNum2 赋值为 13
3.4 Parent 的 <init> 执行完成, 执行 Son 自己的 <init> 方法, 直接属性赋值, sNum2 赋值为 20
3.5 Son 的代码块, sNum2 赋值为 22
3.6 Son 的构造函数, sNum3 赋值为 23

从上面的例子, 应该可以区分出 <clinit><init> 的作用和区别了吧
2 个方法都是编译器, 对我们编写的初始的整合, static 属性赋值和静态代码块整合为 <clinit>, 实例属性赋值, 代码块和构造方法整合为 <init>, 而且 <init> 方法会先调用直接父级的 init 的方法。

到此, 所有的内容就整理完成了。

4 参考

Java对象的创建过程详解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/230000.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

国产or进口?台阶仪为何要选择国产

在微观轮廓测量领域&#xff0c;选择一款合适的台阶仪对于获得精准的测量结果至关重要。随着科技的不断发展&#xff0c;台阶仪市场上涌现了许多国产和进口产品&#xff0c;消费者在选择时可能会面临一些疑虑。 什么是台阶仪 台阶仪是一种超精密接触式微观轮廓测量仪&#xf…

JavaWeb文件上传与下载

一.文件上传 1. 引入两个jar包 jar包可以在maven库进行下载&#xff0c;地址&#xff1a;https://mvnrepository.com,一般点击下载量最多的jar进行下载就可以了。 apache:commons-fileupload.jarcommons-fileupload.jar这个jar包是依赖commons-io.jar的 2. 代码 前端代码&…

桌面概率长按键盘无法连续输入问题

问题描述&#xff1a;概率性长按键盘无法连续输入文本 问题定位&#xff1a; 系统按键流程分析 图一 系统按键流程 按键是由X Server接收的&#xff0c;这一点只要明白了X Window的工作机制就不难理解了。X Server在接收到按键后&#xff0c;会转发到相应程序的窗口中。在窗…

重启docker容器后,ssh无法访问且浏览器无法访问

今天把云服务器中的docker容器重启一遍后&#xff0c;发现里面的项目访问不到了&#xff0c;而且也无法ssh访问。 改了一天&#xff0c;终于还是改好了。 一 .首先是无法ssh访问&#xff0c; 我无法ssh连接的原因是因为我容器的重启之后sshd没有了&#xff0c;然后重新下载了…

助力智能人群检测计数,基于YOLOv5全系列模型【n/s/m/l/x】开发构建通用场景下人群检测计数识别系统

在一些人流量比较大的场合&#xff0c;或者是一些特殊时刻、时段、节假日等特殊时期下&#xff0c;密切关注当前系统所承载的人流量是十分必要的&#xff0c;对于超出系统负荷容量的情况做到及时预警对于管理团队来说是保障人员安全的重要手段&#xff0c;本文的主要目的是想要…

SpringIOC之作用域Scope

博主介绍:✌全网粉丝5W+,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验✌ 博主作品:《Java项目案例》主要基于SpringBoot+MyBatis/MyBatis-plus+…

设计模式之创建型设计模式(一):单例模式 原型模式

单例模式 Singleton 1、什么是单例模式 在软件设计中&#xff0c;单例模式是一种创建型设计模式&#xff0c;其主要目的是确保一个类只有一个实例&#xff0c;并提供一个全局访问点。 这意味着无论何时需要该类的实例&#xff0c;都可以获得相同的实例&#xff0c;而不会创建…

使用VBA快速统计词组词频(多单词组合)(2/2)

实例需求&#xff1a;产品清单如A列所示&#xff0c;现在如下统计多单词组合词组词频。 在上一篇博客中《使用VBA快速统计词组词频(多单词组合)&#xff08;1/2&#xff09;》讲解了如何实现双词的词频统计。 本文将讲解如何实现3词的词频统计&#xff0c;掌握实现方法之后&a…

前端Vue必问面试题

1,Vue3.0 为什么要使用 proxy 在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶ 不需用使用 Vue. s e t 或 V u e . s…

硬件编程语言

硬件画板说白了就是电气的连接&#xff0c;相较于PCB连接在2.5D中完成&#xff08;有些大佬们是直接3D设计&#xff09;考虑的东西会更多&#xff0c;原理图的抽象使得硬件思路更加简单。 就算是这样&#xff0c; 增加到上千门器件后的大工程是非常难以进行的编辑和检查的&…

Mac如何安装stable diffusion

今天跟大家一起在Mac电脑上安装下stable diffusion&#xff0c;在midjourney等模型收费的情况下如何用自己的电脑算力用上免费的画图大模型呢&#xff1f;来吧一起实操起来 一、安装homebrew 官网地址&#xff1a;Homebrew — The Missing Package Manager for macOS (or Lin…

认识Trino

认识Trino 一、Trino二、结构三、集群四、coordinator五、Worker六、数据源七、连接器八、目录九、架构十、表十一、查询执行模型十二、陈述十三、查询十四、阶段十五、任务十六、分隔十七、Driver十八、Operator十九、Exchange 一、Trino Trino&#xff08;前身为PrestoSQL&a…

conda和pip配置国内镜像源

1、conda配置镜像源&#xff1a; 使用conda进行安装时&#xff0c;访问的是国外的网络&#xff0c;所以下载和安装包时会特别慢。我们需要更换到国内镜像源地址&#xff0c;这里我更换到国内的清华大学地址。&#xff08;永久添加镜像&#xff09; Windows和Linux 对于conda修…

SQL进阶理论篇(十):数据库中的锁

文章目录 简介按照锁的粒度进行划分从数据库管理的角度进行划分从程序员的角度进行划分为什么共享锁会发生死锁&#xff1f;参考文献 简介 索引和锁&#xff0c;是数据库中的两个核心知识点。 索引的相关知识点&#xff0c;在之前的几章里我们已经介绍的差不多了。接下来我们…

[pasecactf_2019]flask_ssti proc ssti config

其实这个很简单 Linux的/proc/self/学习-CSDN博客 首先ssti 直接fenjing一把锁了 这里被加密后 存储在 config中了 然后我们去config中查看即可 {{config}} 可以获取到flag的值 -M7\x10wd94\x02!-\x0eL\x0c;\x07(DKO\r\x17!2R4\x02\rO\x0bsT#-\x1cZ\x1dG然后就可以写代码解…

MNIST内置手写数字数据集的实现

torchvision库 torchivision库是PyTorch中用来处理图像和视频的一个辅助库&#xff0c;接下来我们就会使用torchvision库加载内置的数据集进行分类模型的演示 为了统一数据加载和处理代码&#xff0c;PyTorch提供了两个类用于处理数据加载&#xff0c;他们分别是torch.utils.…

leetcode:641. 设计循环双端队列

设计循环双端队列 实现 MyCircularDeque 类: MyCircularDeque(int k) &#xff1a;构造函数,双端队列最大为 k 。 boolean insertFront()&#xff1a;将一个元素添加到双端队列头部。 如果操作成功返回 true &#xff0c;否则返回 false 。 boolean insertLast() &#xff1a;…

机器视觉技术与应用实战(开运算、闭运算、细化)

开运算和闭运算的基础是膨胀和腐蚀&#xff0c;可以在看本文章前先阅读这篇文章机器视觉技术与应用实战&#xff08;Chapter Two-04&#xff09;-CSDN博客 开运算&#xff1a;先腐蚀后膨胀。开运算可以使图像的轮廓变得光滑&#xff0c;具有断开狭窄的间断和消除细小突出物的作…

饥荒Mod 开发(十五):小地图显示物品

饥荒Mod 开发(十四)&#xff1a;制作屏幕弹窗 本篇源码 饥荒中按下Tab键可以显示地图&#xff0c;刚开始进入游戏的时候地图是未探索状态&#xff0c;所以我们并不知道地图上面的物品分布情况。并且地图上只会显示很少一部分的物品&#xff0c;比如树枝&#xff0c;草&#xf…

C++二维数组(4)

蛇形遍历 题目描述&#xff1a;用数字1,2,3,4,...,n*n这n2个数蛇形填充规模为n*n的方阵。 蛇形填充方法为&#xff1a; 对于每一条左下-右上的斜线&#xff0c;从左上到右下依次编号1,2,...,2n-1&#xff1b;按编号从小到大的顺序&#xff0c;将数字从小到大填入各 条斜线&…