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

news2024/12/24 10:25:46

文章目录

  • 一、对象创建方式
  • 二、对象产生步骤
    • 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.coloradmin.cn/o/456048.html

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

相关文章

大学生创业项目-校园外卖的创业优势在哪里?

在当今的外卖行业中&#xff0c;校园外卖已成为外卖行业的垂直分类领域。 与我们通常使用的美团、饿了么平台不同&#xff0c;校园外卖平台需要招聘校园学生和校园内外人员兼职作为校园骑手&#xff0c;完成“最后一公里”的外卖送餐方式。 对于平台运营商来说&#xff0c;配…

生成树协议三姐妹:STP、RSTP 和 MSTP,附思科和华为双厂商命令示例

在计算机网络中&#xff0c;为了保证网络拓扑结构的稳定性和可靠性&#xff0c;需要采用一些协议进行网络的管理和控制。其中&#xff0c;STP、RSTP 和 MSTP 是三种常用的网络管理协议。本文将分别介绍这三种协议&#xff0c;并且使用华为、思科两家厂商作为案例给出相应的命令…

( “树” 之 Trie) 677. 键值映射 ——【Leetcode每日一题】

知识点回顾 &#xff1a; Trie&#xff0c;又称前缀树或字典树&#xff0c;用于判断字符串是否存在或者是否具有某种字符串前缀。 ❓677. 键值映射 难度&#xff1a;中等 设计一个 map &#xff0c;满足以下几点: 字符串表示键&#xff0c;整数表示值返回具有前缀等于给定字…

Scrapy框架 -- 深度爬取并持久化保存图片

一、新建一个Scrapy项目daimg scrapy startproject daimg 二、进入该项目并创建爬虫文件daimgpc cd daimg scrapy genspider daimgpc www.xxx.com 三、修改配置文件settings.py ROBOTSTXT_OBEY False LOG_LEVEL ERROR USER_AGENT "Mozilla/5.0 (Windows NT 10.0; …

Git快速入门

Git快速入门 版本控制什么是版本控制常见的版本控制工具版本控制分类Git与SVN的主要区别 聊聊Git的历史Git环境配置软件下载启动GitGit配置 Git基本理论&#xff08;重要&#xff09;三个区域工作流程 Git项目搭建创建工作目录与常用指令本地仓库搭建克隆远程仓库 Git文件操作文…

Springsecurity课程笔记06-13章基于数据库的方法授权

动力节点Springsecurity视频课程 6 密码处理 6.1 为什么要加密&#xff1f; csdn 密码泄露事件 泄露事件经过&#xff1a;https://www.williamlong.info/archives/2933.html 泄露数据分析&#xff1a;https://blog.csdn.net/crazyhacking/article/details/10443849 6.2加密…

平均薪资28K,测试开发的涨薪史,给我看哭了...

金三银四的涨薪季要来了&#xff0c;看着身边的同事有的晋升&#xff0c;有的收获30%的涨薪&#xff0c;他们都拥有哪些影响涨薪的硬核技能呢&#xff1f;互联网行业的高薪是众所周知的&#xff0c;而测试作为互联网公司越来越重视的技术开发模块&#xff0c;薪资收入同样一路走…

Tomcat部署与优化

前言 Tomcat是一款免费、开放源代码的Web应用服务器&#xff0c;是Apache软件基金会的一个核心开源项目&#xff0c;属于轻量级应用服务器&#xff0c;通常意义上的 Web 服务器接受请求后&#xff0c;只是单纯地响应静态资源&#xff0c;如 HTML 文件&#xff0c;图片文件等&a…

深入探究C++中的仿函数和迭代器——提升你的STL技能

&#x1f4d6;作者介绍&#xff1a;22级树莓人&#xff08;计算机专业&#xff09;&#xff0c;热爱编程&#xff1c;目前在c&#xff0b;&#xff0b;阶段>——目标Windows&#xff0c;MySQL&#xff0c;Qt&#xff0c;数据结构与算法&#xff0c;Linux&#xff0c;多线程&…

若依/RuoYi-Vue,若依管理系统-启动步骤

若依RuoYi-Vue前后端项目启动流程_若依前端怎么启动_primary taste_mm的博客-CSDN博客若依官网&#xff1a;RuoYi 若依官方网站 |后台管理系统|权限管理系统|快速开发框架|企业管理系统|开源框架|微服务框架|前后端分离框架|开源后台系统|RuoYi|RuoYi-Vue|RuoYi-Cloud|RuoYi框架…

进驻Lidl利多超市利器—— EDI

Lidl利多超市是源自德国的跨国零售企业&#xff0c;成立于1973年&#xff0c;发展迅速&#xff0c;目前在欧洲拥有10,800多家门店&#xff0c;覆盖29个国家。Lidl的业务范围包括食品、饮料、家庭用品、家具、电器等多个品类。Lidl一直致力于提供高性价比的商品&#xff0c;以满…

FIT2CLOUD飞致云发布开源轻量级云管平台CloudExplorer Lite

2023年4月21日&#xff0c;中国领先的开源软件公司FIT2CLOUD飞致云正式发布开源轻量级云管平台项目CloudExplorer Lite。CloudExplorer Lite&#xff08;https://github.com/CloudExplorer-Dev&#xff09;脱胎于飞致云创始软件产品CloudExplorer多云管理平台&#xff0c;支持对…

图表示学习算法学习

struc2vec: Learning Node Representations from Structural Identity learning latent representations for the structural identity of nodes. &#xff1a; 从结构特征中学习节点潜在表示 node representation : 节点表示 structural identity : 结构特征 struct2Vec是一个…

《UVM实战》学习笔记——第七章 UVM中的寄存器模型1——寄存器模型介绍、前门/后门访问

文章目录 前言一、寄存器模型简介1.1 带寄存器配置总线的DUT1.2 参考模型如何读取寄存器的值1.3 寄存器模型的基本概念 二、简单的寄存器模型2.1 只有一个寄存器的寄存器模型2.2 将寄存器模型集成到验证平台2.3 在验证平台中使用寄存器模型 三、前门访问和后门访问3.1 前门访问…

2023年淮阴工学院五年一贯制专转本应用文写作考试大纲

2023年淮阴工学院五年一贯制专转本应用文写作考试大纲 一、考核对象 本课程的考核对象是五年一贯制高职专转本秘书学专业普通在校生考生。 二、考核目的 通过课堂教学&#xff0c;学生应当能够识记、理解和应用有关应用文写作的基本理论和基本技能。其中&#xff0c;识记指…

TortoiseSVN使用-TortoiseSVN更换或重置登录用户

文章目录 3.4.9 TortoiseSVN更换或重置登录用户 本人其他相关文章链接 3.4.9 TortoiseSVN更换或重置登录用户 1&#xff0c;打开SVN的settings 2&#xff0c;找到Saved Data栏&#xff0c;右侧Authentication data项点击清除按钮clear 3&#xff0c;再次打开SVN&#xff0c;会要…

AgentGPT已成气候

AgentGPT之前也有介绍过&#xff0c;它最主要的功能是在ChatGPT的功能基础上&#xff0c;允许你自己自定义配置部署&#xff0c;根据你给出的命令&#xff0c;它将尝试通过思考&#xff0c;和执行&#xff0c;不用重复的给它发送指令&#xff0c;直接给你汇总好结果。 安装步骤…

牛客网刷题总结

1.利用%符号获取特定位数的数字。 2.强制类型转换 &#xff08;将float转换为int &#xff09; 3.计算有关浮点型数据时&#xff0c;要注意你计算过程中所有的数据都是浮点型 4.0/3.0 ! 4/3 4.通过位操作符实现输出2的倍数&#xff08;对于位操作符不熟悉的小伙伴可以看看我…

StringBuffer类详解

StringBuffer 定义 1.java.lang.StringBuffer代表可变的字符序列&#xff0c;可以对字符串内容进行增删 2.很多方法与String相同&#xff0c;但StringBuffer是可变长度的。 3.StringBuffer是一个容器。 String和StringBuffer的不同 1.String保存的是字符串常量&#xff0c…

机器学习——用KNN解决非线性回归问题

问&#xff1a;k最近邻分类模型是非线性模型。 答&#xff1a;正确。k最近邻分类模型是非线性模型&#xff0c;因为它的决策边界是由最近邻居点的类别决定的&#xff0c;而最近邻居点的分布通常是不规则的&#xff0c;因此决策边界也就不是线性的。因此&#xff0c;k最近邻分类…