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

这篇具有很好参考价值的文章主要介绍了深入JVM了解Java对象实例化过程。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、对象创建方式

  • 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了解Java对象实例化过程
空闲列表
如果 JVM 堆内存并不是规整的,即:已用内存空间与空闲内存相互交错,JVM 会维护一个空闲列表,记录那些内存块是可用的,在为该对象分配空间时,JVM 会从空闲列表中找到一块足够大的空间划分给对象使用
深入JVM了解Java对象实例化过程

3、处理并发问题

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

对象的内存分配过程中,主要是对象的引用指向这个内存区域,然后进行初始化操作
但是,因为堆是全局共享的,因此在同一时间,可能有多个线程在堆上申请空间,在并发场景中,就会存在两个线程先后把对象引用指向了同一个内存区域。
深入JVM了解Java对象实例化过程
为了解决这个并发问题,对象的内存分配过程就必须进行同步控制。但是无论是使用哪种同步方案(实际上虚拟机使用的可能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、总结

对象创建的几个过程:
深入JVM了解Java对象实例化过程

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

三、对象的内存布局

这点其实是上面第五点的展开说明。一个对象的内存布局包括三个部分:
1 对象头 2. 实例数据 3. 填充数据
深入JVM了解Java对象实例化过程

1、对象头

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

1.1 运行时元数据(Mark Word)

32位的hotspot对象头
深入JVM了解Java对象实例化过程
64位:
深入JVM了解Java对象实例化过程
对上图64位的进行具象表示如下图所示:
深入JVM了解Java对象实例化过程
下面对各个标志位进行解读:

锁标志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了解Java对象实例化过程

三、对象的访问定位

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

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

1、 句柄间接访问

JVM 堆中会划分一块内存来作为句柄池,reference 中存储句柄的地址,句柄中则存储对象的实例数据何类的元数据的地址:
深入JVM了解Java对象实例化过程

2、直接指针访问

直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。Hotspot是采用这种方式的。
深入JVM了解Java对象实例化过程

四、对象的生命周期

在 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 方法已经被执行

  • 对象空间已被重用

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

五、对象初始化顺序总结

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

  • 静态成员的初始化

  • 静态初始化块

  • 成员的初始化

  • 初始化块

  • 构造器

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

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

  • 父类静态成员的初始化

  • 父类静态代码块初始化

  • 子类静态成员的初始化

  • 子类静态代码块初始化

  • 父类成员的初始化

  • 父类初始化块

  • 父类构造器

  • 子类成员的初始化

  • 子类初始化块

  • 子类构造器文章来源地址https://www.toymoban.com/news/detail-423617.html


到了这里,关于深入JVM了解Java对象实例化过程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java进阶(1)——JVM的内存分配 & 反射Class类的类对象 & 创建对象的几种方式 & 类加载(何时进入内存JVM)& 注解 & 反射+注解的案例

    1.java运行时的内存分配,创建对象时内存分配; 2.类加载的顺序,创建一个唯一的类的类对象; 3.创建对象的方式,new,Class.forName,clone; 4.什么时候加载.class文件进入JVM内存中,看到new,Class.forName; 5.如何加载?双亲委托(委派)机制:安全;AppClassLoader; 6.反射实质:能

    2024年02月14日
    浏览(34)
  • JVM中对象创建过程

    在JVM中对象的创建,我们从一个new指令开始: 这个过程大概图示如下: 虚拟机收到new指令触发。 类加载检查:如果类没有被类加载器加载,则执行类加载流程(将class信息加载到JVM的运行时数据区的过程),对象所需内存大小在类加载完后可以完全确定。 对象分配内存:从

    2024年03月22日
    浏览(39)
  • 【jvm系列-06】深入理解对象的实例化、内存布局和访问定位

    JVM系列整体栏目 内容 链接地址 【一】初识虚拟机与java虚拟机 https://blog.csdn.net/zhenghuishengq/article/details/129544460 【二】jvm的类加载子系统以及jclasslib的基本使用 https://blog.csdn.net/zhenghuishengq/article/details/129610963 【三】运行时私有区域之虚拟机栈、程序计数器、本地方法栈 https

    2023年04月16日
    浏览(43)
  • JVM运行时区域——对象创建内存分配过程

            新创建的对象 , 都存放在伊甸园区域 ,当垃圾回收时,将伊甸园区域的垃圾数据销毁,然后将存活的对象转移到幸存者0区域,之后创建的新的对象还是存放在伊甸园区域,等到再次垃圾回收后,将伊甸园区域和幸存者0区域中存活的对象一起转移到幸存者1区域中

    2024年02月15日
    浏览(36)
  • JVM面试题-JVM对象的创建过程、内存分配、内存布局、访问定位等问题详解

    内存分配的两种方式 指针碰撞 适用场合:堆内存 规整 (即没有内存碎片)的情况下。 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。 使用该分配方式的GC收集器:

    2024年02月08日
    浏览(40)
  • “深入剖析JVM内部机制:了解Java虚拟机的工作原理“

    标题:深入剖析JVM内部机制:了解Java虚拟机的工作原理 摘要:本文将深入剖析JVM内部机制,详细介绍Java虚拟机的工作原理。我们将探讨JVM的组成部分、类加载过程、内存管理、垃圾回收以及即时编译等关键概念。此外,还将提供示例代码来帮助读者更好地理解JVM的内部机制

    2024年02月11日
    浏览(32)
  • 深入理解Java虚拟机jvm-对象的内存布局

    在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例 数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈 希码(HashCode)、GC分代年龄、锁状态标志、

    2024年02月09日
    浏览(43)
  • 【JVM】Java堆 :深入理解内存中的对象世界

    人不走空                                                                            目录         🌈个人主页:人不走空       💖系列专栏:算法专题 ⏰诗词歌赋:斯是陋室,惟吾德馨 ​编辑 什么是Java堆? 作用和特点 1. 存储对象实例 2. 垃圾收集 3. 对象

    2024年01月19日
    浏览(44)
  • 【Java 基础篇】Java反射:深入了解Class对象

    Java是一门强类型语言,它要求在编译时知道所有的类和对象类型。然而,有时候我们需要在运行时动态地获取和操作类的信息,这就是Java反射的用武之地。本文将深入探讨Java反射,特别是与Class对象相关的内容。 反射是一种机制,允许我们在运行时检查、获取和操作类的信

    2024年02月06日
    浏览(35)
  • 【JVM】详细解析java创建对象的具体流程

    目录 一、java创建对象的几种方式 1.1、使用new  1.2、反射创建对象 1.2.1、Class.newInstance创建对象 1.2.2、调用构造器再去创建对象Constructor.newInstance  1.3、clone实现 1.4、反序列化 二、创建对象的过程 2.1、分配空间的方式 1、指针碰撞 2、空闲列表 3、怎么选择分配方式 三、

    2024年02月15日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包