深入JVM了解Java对象实例化过程

news/2024/4/25 10:15:36/文章来源:https://blog.csdn.net/qq_41860497/article/details/130335081

文章目录

  • 一、对象创建方式
  • 二、对象产生步骤
    • 1、判断对象是否已经加载、链接、初始化
    • 2、为对象分配内存空间
    • 3、处理并发问题
      • 3.1 TLAB
    • 4、初始化零值
    • 5、完善对象内存布局的信息
    • 6、调用对象的实例化方法 `<init>`
    • 7、总结
  • 三、对象的内存布局
    • 1、对象头
      • 1.1 运行时元数据(Mark Word)
      • 1.2 类型指针(Klass Word)
    • 2、 实例数据(Instance Data)
    • 3、 填充(padding)
  • 三、对象的访问定位
    • 1、 句柄间接访问
    • 2、直接指针访问
  • 四、对象的生命周期
    • 1、Creation
    • 2、Using
    • 3、Invisible
    • 4、Unreachable
    • 5、 Collected、Finalized、Free
  • 五、对象初始化顺序总结

一、对象创建方式

  • new:最常见的方式、Xxx的静态方法,XxxBuilder/XxxFactory的静态方法
  • Class的newInstance方法:反射的方式,只能调用空参的构造器,权限必须是public
  • Constructor的newInstance(XXX):反射的方式,可以调用空参、带参的构造器,权限没有要求
  • 使用clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口,实现clone()
  • 使用序列化:从文件中、从网络中获取一个对象的二进制流
  • 第三方库 Objenesis

二、对象产生步骤

1、判断对象是否已经加载、链接、初始化

当我们在程序中写下new指令的时候,首先改指令的参数是否在常量池中定位到一个符号引用(Symbolic Reference),并检查这个符号引用代表的类是否已经加载、解析和初始化。其实就是验证是否是第一个使用该类。如果是第一次使用该类,就会执行类的加载过程。

注:符号引用是指,一个类中引入了其他的类,可是 JVM 并不知道引入其他类在什么位置,所以就用唯一的符号来代替,等到类加载器去解析时,就会使用符号引用找到引用类的具体地址,这个地址就是直接引用

类的加载过程在双亲委派模式下,使用当前类加载器按照ClassLoader + 包名 + 类名key进行查找对应的.class文件。

如果找到了,直接进行加载,生成Class对象,如果没有找到。抛出ClassNotFoundException的异常 。

在类加载完成后,JVM 就可以完全确定new出来的对象的内存大小了,接下来,JVM 会执行为该对象分配内存的工作

2、为对象分配内存空间

为对象分配空间的任务等同于把一块确定大小的内存从 JVM 堆中划分出来,目前常用的有两种方式(根据使用的垃圾收集器的不同而使用不同的分配机制):

  1. Bump the Pointer(指针碰撞)
  2. Free List(空闲列表)

指针碰撞
意思是所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针指向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是SerialParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。
在这里插入图片描述
空闲列表
如果 JVM 堆内存并不是规整的,即:已用内存空间与空闲内存相互交错,JVM 会维护一个空闲列表,记录那些内存块是可用的,在为该对象分配空间时,JVM 会从空闲列表中找到一块足够大的空间划分给对象使用
在这里插入图片描述

3、处理并发问题

  • 采用CAS失败重试、区域加锁保证更新的原子性
  • 每个线程预先分配一块TLAB:通过设置 -XX:+UseTLAB参数来设定

对象的内存分配过程中,主要是对象的引用指向这个内存区域,然后进行初始化操作
但是,因为堆是全局共享的,因此在同一时间,可能有多个线程在堆上申请空间,在并发场景中,就会存在两个线程先后把对象引用指向了同一个内存区域。
在这里插入图片描述
为了解决这个并发问题,对象的内存分配过程就必须进行同步控制。但是无论是使用哪种同步方案(实际上虚拟机使用的可能CAS),都会影响内存的分配效率。所以就有了一个HotSpot虚拟机的解决方案,这 种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。TLAB只是HotSpot虚拟机的一个优化方案,不代表所有的虚拟机都有这个特性。

每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。

3.1 TLAB

TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

所以说,因为有了TLAB技术,堆内存是线程共享的这个命题是不准确的,其eden区域中还是有一部分空间是分配给线程独享的。
TLAB分配对象逻辑

4、初始化零值

JVM 会为所有实例数据赋零值 (默认值),即:将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值,例如整型的默认值为 0,引用类型的默认值为null等等。保证对象实例字段在不赋值时可以直接使用。

5、完善对象内存布局的信息

在我们为对象分配好内存空间后,JVM 会设置对象的内存布局的一些信息。

对象在内存中存储的布局(以HotSpot虚拟机为例)分为:对象头,实例数据以及对齐填充

  • 对象头
    对象头包含两个部分:
    • Mark Word:存储对象自身的运行数据,如:Hash Code,GC 分代年龄,锁状态标志等等
    • 类型指针:对象指向它的类的元数据的指针
  • 实例数据
    实例数据是真正存放对象实例的地方
  • 对齐填充
    这部分不一定存在,也没有什么特别含义,仅仅是占位符。因为 HotSpot 要求对象起始地址都是 8 字节的整数倍,如果不是就对齐

并且,JVM 会为对象头进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的 Hash Code, 对象的 GC 分带年龄等等,这些信息都存放在对象的对象头中

6、调用对象的实例化方法 <init>

在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

因此一般来说(由字节码中跟随invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来。

在 JVM 完善好对象内存布局的信息后,会调用对象的 <init> 方法,根据传入的属性值为对象的变量赋值。

我们在上文介绍了类加载的过程(加载 -> 连接 -> 初始化),在初始化这一步骤,JVM 为类的静态变量显示赋值,并且执行了静态代码块。实际上这一步骤是由 JVM 生成的<clinit>方法完成的。

<clinit> 的执行的顺序为:

  • 父类静态变量初始化
  • 父类静态代码块
  • 子类静态变量初始化
  • 子类静态代码块

而我们在创建实例 new 一个对象时,会调用该对象类构造器进行初始化,这里面就会执行<init>方法。

<init> 的执行顺序为:

  • 父类变量初始化
  • 父类普通代码块
  • 父类构造函数
  • 子类变量初始化
  • 子类普通代码块
  • 子类构造函数

关于 <init> 方法:

有多少个构造器就会有多少个 <init> 方法。<init> 具体执行的内容包括非静态变量的赋值操作,非静态代码块的执行,与构造器的代码非静态代码赋值操作与非静态代码块的执行是从上至下顺序执行,构造器在最后执行

关于<clinit><init> 方法的差异:

<clinit> 方法在类加载的初始化步骤执行,<init> 在进行实例初始化时执行<clinit> 执行静态变量的赋值与执行静态代码块,而 <init> 执行非静态变量的赋值与执行非静态代码块以及构造器
<init>构造器和<cinit>以及构造方法的关系

7、总结

对象创建的几个过程:
在这里插入图片描述

  1. 加载类元信息
  2. 为对象分配内存
  3. 处理并发问题
  4. 属性的默认初始化(零值初始化)
  5. 设置对象头信息
  6. 属性的显示初始化、代码块中初始化、构造器中初始化

三、对象的内存布局

这点其实是上面第五点的展开说明。一个对象的内存布局包括三个部分:
1 对象头 2. 实例数据 3. 填充数据
在这里插入图片描述

1、对象头

对象头包含了两部分,分别是运行时元数据(Mark Word)和类型指针。如果是数组,还需要记录数组的长度。

1.1 运行时元数据(Mark Word)

32位的hotspot对象头
在这里插入图片描述
64位:
在这里插入图片描述
对上图64位的进行具象表示如下图所示:
在这里插入图片描述
下面对各个标志位进行解读:

锁标志lock—— 区分锁的状态,参数占用两个字节,可以表示四种状态。但是上面锁的状态有五种,可以看出无锁态和偏向锁都用01表示。那么如何区分无锁态和偏向锁?这时就需要引入偏向锁参数。0表示普通对象,1表示偏向锁。

是否偏向锁(biased_lock)——是否偏向锁,这个参数占用1bit,0表示不是偏向锁,1表示的是偏向锁。

分代年龄——表示Java对象被GC的次数,每次GC的时候,如果对象在Survivor区复制一下,年龄增加1。当对象达到设定的阈值时,就会晋升为老年代。这个参数占4bit,也就是最大是2^4 - 1 = 15次。这是JVM参数XX:MaxTenuringThreshold选项最大为15的原因。默认情况下并行GC的年龄阈值为15,并发GC的年龄阈值为6。
hashcode——对象的hashcode,使用方法System.identityHashCode()进行计算,如果采用延迟计算,计算后会把结果写到该对象头中。当对象被锁定时,该值会移动到Monitor中。
线程ID——在偏向模式中,当某个线程持有该对象,则该对象头的线程ID位置存储的就是这个线程ID。这样在后面的操作中就不需要在进行获取锁的动作。
epoch——偏向锁的时间戳,用于在CAS锁操作过程中,偏向性表示,表示更偏向那个锁。
ptr_to_lock_record——在轻量级锁的状态下,指向栈中纪录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象头中设置指向锁纪录的指针。
ptr_to_heavyweight_monitor——在重量级锁的状态下,指向管程Monitor的指针。如果两个不同的线程同在一个对象上竞争,则必须将轻量级锁定升级到Monitor新管理等待的线程。在重量级锁定的情况下,JVM设置ptr_to_heavyweight_monitor指向Monitor

基本上是以下几种:

  • 哈希值(HashCode)
  • GC分代年龄
  • 锁状态标志
  • 线程持有的锁
  • 偏向线程ID
  • 翩向时间戳

1.2 类型指针(Klass Word)

指向类元数据InstanceKlass,确定该对象所属的类型。
推荐阅读: Class对象存储在堆中

2、 实例数据(Instance Data)

是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)

  • 相同宽度的字段总是被分配在一起
  • 父类中定义的变量会出现在子类之前
  • 如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙

3、 填充(padding)

这部分不一定存在,也没有什么特别含义,仅仅是占位符。因为 HotSpot 要求对象起始地址都是 8 字节的整数倍,如果不是就对齐。

例子说明:

public class Customer{int id = 1001;String name;Account acct;{name = "匿名客户";}public Customer() {acct = new Account();}
}public class CustomerTest{public static void main(string[] args){Customer cust=new Customer();}
}

上述在内存中的关系:
在这里插入图片描述

三、对象的访问定位

在描述完创建一个对象的过程之后,我们再来简单看一下如何去访问这个对象。

JVM 规范中只规定了reference类型是一个指向对象的引用,但没有规定这个引用具体如何去定位,访问堆中对象,因此对象的访问取决于 JVM 的具体实现,目前主流的访问对象的方式有两种:句柄间接访问直接指针访问

1、 句柄间接访问

JVM 堆中会划分一块内存来作为句柄池,reference 中存储句柄的地址,句柄中则存储对象的实例数据何类的元数据的地址:
在这里插入图片描述

2、直接指针访问

直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。Hotspot是采用这种方式的。
在这里插入图片描述

四、对象的生命周期

在 JVM 运行空间中,对象的整个生命周期大致可以分为七个阶段:在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了 JVM中对象的完整的生命周期。下面分别介绍对象在处于这7个阶段时的不同情形。

1、Creation

一个对象想要进入创建阶段,前提是它的类文件必须已经加载到内存中,并且已经创建了 Class 对象,这样才能根据类信息进行创建
在对象的创建阶段,系统通过以下步骤完成对象的创建过程:

  • 为对象在堆内存中分配空间
  • 构造对象。从最顶层的父类开始对局部变量进行赋值
  • 从最顶层的父类开始往下调用构造方法

2、Using

当对象创建阶段结束之后,通常就会进入到对象的应用阶段。这个阶段是对象得以表现自身能力的阶段。也就是说对象的应用阶段是对象整个生命周期中证明自身 “存在价值” 的时期。在对象的应用阶段,对象具备下列特征:

  • 系统至少维护着对象的一个强引用(Strong Reference

  • 所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)弱引用(Weak Reference)虚引用(Phantom Reference)

3、Invisible

不可视阶段中,对象存在且被引用,但是这个引用在接下来的代码中并没有被使用到,这就造成了内存的冗余。

public void process() {try {MyObject obj = new MyObject();obj.doSomething();}catch (Exception e) {e.printStackTrace();}while (true) {// 该代码块对 obj 对象来说已经是不可视的// 因此下面代码在编译时会引发错误obj.doSomething();}
}

如果一个对象已经使用完毕,并且在可视区域内不再使用,那么应该主动将其设置为 null。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。

4、Unreachable

当一个对象没有再被强引用时,就会进入不可达阶段,在这个阶段中,对象随时会被回收,这由 JVM 中的垃圾回收器(GC)来决定。

5、 Collected、Finalized、Free

对象生命周期的最后一个阶段是可收集阶段、终结阶段与释放阶段。当对象处于这个阶段的时候,可能处于下面三种情况:

  • 垃圾回收器发现该对象已经不可到达

  • finalize 方法已经被执行

  • 对象空间已被重用

当对象处于上面三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。虚拟机就可以直接将该对象回收了。

五、对象初始化顺序总结

在没有继承的条件下,实例化一个对象初始化的顺序为:

  • 静态成员的初始化

  • 静态初始化块

  • 成员的初始化

  • 初始化块

  • 构造器

这里面需要注意的是【静态部分只在类加载时初始化一次】

如果有继承关系,那么实例化子类对象的初始化顺序为:

  • 父类静态成员的初始化

  • 父类静态代码块初始化

  • 子类静态成员的初始化

  • 子类静态代码块初始化

  • 父类成员的初始化

  • 父类初始化块

  • 父类构造器

  • 子类成员的初始化

  • 子类初始化块

  • 子类构造器


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

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

相关文章

详解树与二叉树的概念,结构,及实现(上篇)

目录 一&#xff0c; 树 1.2 树的相关概念 1.3 树的表示 1.4 树在实际中的运用&#xff08;表示文件系统的目录树结构&#xff09; 二&#xff0c; 二叉树 2.1二叉树概念 三&#xff0c;特殊的二叉树 1. 满二叉树 2. 完全二叉树 3. 1 二叉树的性质 3. 2 二叉树的存储…

【速卖通】 AliExpress(速卖通)关键词搜索结果采集

采集场景 在AliExpress(速卖通) 首页中 http://www.aliexpress.com 中输入关键词&#xff0c;采集关键词搜索后得到的商品列表信息。 采集字段 关键词、标题、商品id、商品图片地址、商品详情链接、价格、免费退送货、星级、已出售数量、店铺名 采集结果 采集结果可导出为E…

Linux-初学者系列——篇幅7_文本编辑和处理命令

文本编辑和处理命令-目录 一、系统基本编辑命令安装vim软件工具包语法格式&#xff1a; 1、vim编辑命令模式01 普通模式02 编辑模式03 命令模式 2、编辑文件技巧01 批量删除多行指定信息02 批量增加多列指定信息03 编辑常见问题错误1&#xff1a;没有指定编辑信息错误2&#xf…

基于TensorRT的yolov5 实例分割部署

yolov5-7.0 github: https://github.com/ultralytics/yolov5/tree/master 1. 代码的使用 1.1 训练yolov5-seg模型 使用的yolov5-7.0的代码,github下载:https://github.com/ultralytics/yolov5/releases/tag/v7.0 训练指令 python segment/train.py --data coco128-seg.y…

centos7 查看服务器配置信息

1.linux查看版本当前操作系统发行信息 cat /etc/centos-release cat /etc/centos-release 2、查看内核版本uname -a或者cat /proc/version 3、查看CPU参数 1&#xff09;、查看 CPU 物理个数   grep physical id /proc/cpuinfo | sort -u | wc -l 2&#xff09;、查看 CPU …

SpringCloud:ElasticSearch之DSL查询文档

elasticsearch的查询依然是基于JSON风格的DSL来实现的。 1.1.DSL查询分类 Elasticsearch提供了基于JSON的DSL&#xff08;Domain Specific Language&#xff09;来定义查询。常见的查询类型包括&#xff1a; 查询所有&#xff1a;查询出所有数据&#xff0c;一般测试用。例如…

magento webapi 接口返回 json对象

前言 现在主流的项目开发都是前后端分离&#xff0c;数据通过json对象格式进行传输。但是magento框架&#xff0c;和传统PHP框架相比&#xff0c;区别很大。虽然也支持以RestApi的形式传输数据&#xff0c;但是要么格式并非是传统jsonObject要么就是需要大量的get、set方法。本…

关于xilinx使用PCIE实现FPGA的部分重配置实现(MCAP)

平台&#xff1a;vivado21018.3 芯片&#xff1a;xcku115-flva1517-2-i (active) 本文官方文档&#xff1a;Xilinx_Answer_64761_Ultrascale_Devices 本文驱动下载地址&#xff1a;64761 - Bitstream Loading across the PCI Express Link in UltraScale and UltraScale Dev…

JAVA——线程池

目录 一、线程池的概念 二、Java标准库中的线程池 三、ThreadPoolExecutor 类的参数 四、线程池的拒绝策略 五、模拟实现线程池 一、线程池的概念 线程池顾名思义就是集中存储线程的地方——联想一下水池。 线程池是一种多线程处理形式&#xff0c;处理过程中将任务添加到…

Ext4日志优化-iJournaling

背景 这几年随着SSD等高性能介质的普及&#xff0c;及其在大规模分布式存储系统上的应用。基于Append only的日志写入技术也应用得越来越多&#xff0c;这几天刚好有空&#xff0c;重读了Ext4文件系统的日志部分的内容&#xff0c;也正好看到一篇对Ext4日志技术进行优化的论文…

《编码——隐藏在计算机软硬件背后的语言》精炼——第11章(门)

“The only source of knowledge is experience.” - Albert Einstein 引言 编码是一种处理并表达信息的方式&#xff0c;它包括摩斯电码、盲文、二进制语言等等&#xff0c;当然作为计算机类的经典书籍&#xff0c;这本书简述了计算机中以二进制数为基础的编码方式&#xff0…

nginx简单介绍

文章目录 1. 下载并解压2. 80端口被占用&#xff0c;更改nginx默认的监听端口3. 访问nginx4. 在linux上安装nginx5. nginx常用命令6. nginx.conf 1. 下载并解压 官网下载 2. 80端口被占用&#xff0c;更改nginx默认的监听端口 更改conf/nginx.conf文件 3. 访问nginx ht…

【Linux】popen pclose接口介绍

本篇文章简单讲述了c语言接口popen/pclose的用法 1.函数作用 函数定义如下 #include <stdio.h>FILE *popen(const char *command, const char *type); int pclose(FILE *stream);1.1 popen popen函数会创建一个管道&#xff0c;fork后调用shell来打开进程。由于管道的…

射频封装技术:层压基板和无源器件集成

射频和无线产品领域可以使用非常广泛的封装载体技术&#xff0c;它们包括引线框架、层压基板、低温共烧陶瓷&#xff08;LTCC&#xff09;和硅底板载体&#xff08;Si Backplane&#xff09;。由于不断增加的功能对集成度有了更高要求&#xff0c;市场对系统级封装方法&#xf…

Qt 项目Mingw编译器转换为VS编译器时的错误及解决办法

错误 在mingw生成的项目&#xff0c;转换为VS编译器时通常会报些以下错误&#xff08;C4819警告&#xff0c;C2001错误&#xff0c;C2143错误&#xff09; 原因及解决方式 这一般是由于字符编码引起的&#xff0c;在源代码文件中包含了中文字符导致的。Qt Creator 生成的代码文…

iptables防火墙和Firewalld

引言 在 Internet 中&#xff0c;企业通过各种应用系统来为用户提供各种服务&#xff0c;如 Web 网站、电子邮件系统、FTP 服务器、数据库系统等&#xff0c;那么&#xff0c;如何来保护这些服务器&#xff0c;过滤企业不需要的访问甚至是恶意的入侵呢&#xff0c;接下来&#…

【Linux】生产者消费者模型——环形队列RingQueue(信号量)

文章目录 铺垫信号量信号量概念信号量PV操作信号量基本接口 环形队列的生产消费模型引入环形队列访问环形队列代码实现代码改造多生产者多消费者代码 总结 铺垫 之前写的代码是存在不足的地方的&#xff1a; 我们使用线程操作临界资源的时候要先去判断临界资源是否满足条件&am…

最新动态 | 大势智慧参加广东省应急测绘保障与安全生产演练

4月20日&#xff0c;2023年度广东省应急测绘保障与安全生产演练在台山市赤溪镇鱼塘湾举行。本次演练由广东自然资源厅主办&#xff0c;广东省国土资源测绘院、江门市自然资源局和台山市人民政府承办。在省市各指导单位与参演单位的多方协同与指挥下&#xff0c;应急测绘保障与安…

【三十天精通Vue 3】第十四天 Vue 3 的单元测试详解

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: 三十天精通 Vue 3 文章目录 引言一、为什么要进行单元测试1.1 单元测试的概念1.2 单元测试的优…

ctfshow_WEB_web2 wp

前言 写这个是因为。。。我想摆烂&#xff0c;就去从最简单的题开始做了&#xff0c;想着交一道题是一道嘛&#xff0c;总之觉得这样做很适合欺骗安慰自己&#xff08;逃 然后我发现我错了&#xff0c;我第二道题就做了好久还没做出来&#xff0c;甚至最后去点开了hint…… ps…