百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

JVM 从入门到放弃之 Java 对象创建过程

wptr33 2025-03-19 17:23 23 浏览

架构对象的创建

Java 是一门面向对象的编程语言,创建对象通常只是通过 new关键字创建。

对象创建过程

当虚拟机遇到一个字节码 new指令的时候,首先去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用。并且检查这个符号引用代表的类是否被虚拟机类加载器加载。如果没有,必须先执行类加载的流程。

在类的检查通过过后,接下来虚拟机就会为新生成对象分配内存。对象所需要的内存大小在类加载的时候决定。(对象内存分配后面将有独立的一小段讲解)。

内存分配完成后,虚拟机会将这块分配到的内存空间(不包括对象头)都初始化为零值,就是将这块内存空间进清理和初始化。

接下来虚拟机还需要进行对象进行初始化设置,比如元数据(对象是那个类的实例)、对象的哈希编码、对象的 GC 分代年龄、偏向锁状态等信息这些信息都用于存放到对象头(Object Header)中。

完成上述流程,其实已经完成了虚拟机中内存的创建,但是我们在 Java 执行 new创建对象的角度才刚刚开始,我们还需要调用构造方法初始化对象(可能还需要在此前后调用父类的构造方法、初始化块等)。进行 Java 对象的初始化。即在 .class 的角度是调用 ()方法。如果构造方法中还有调用别的方法,那么别的方法也会被执行,当构造方法内的所有关联的方法都执行完毕后,才真正算是完成了 Java 对象的创建。

整体对象创建流程如下:

对象内存分配

对象内存分配过程如下图所示:

为对象分配空间的任务实质上是从 Jvm 的内存区域中,指定一块确定大小的内存块给 Java 对象。(默认是在堆上分配)。

指针碰撞

假设 Java 堆中内存是绝对规整的,所有使用过的内存都被放在一边,没有使用过的内存放在了另外一边。中间放着一个指针用来表示他们的分界点。那所分配的内存仅仅是把那个指针向空闲的方向挪动一段与Java对象大小相等的距离,这种分配方式叫做“指针碰撞”(Dump The Pointer) 。

空闲列表

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

内存分配方式选择

什么时候使用指针碰撞,什么时候才用空闲列表? 选择哪一种分配方式是由 Java 堆是否规整决定的,而 Java 堆是否规整又是由所采用的垃圾回收器是否有空间整理(Compact)的能力决定。

  • 当使用 Serial 、ParNew 等带指针压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单,又高效。
  • 当采用 CMS 基于清除(Sweep)算法的收集器时,理论上只能采用复杂的空闲列表来分配内存。

并发内存分配方案

对象频繁分配的过程中,即使只修改一个指针所指向的位置,但是在并发的情况下也不是线程安全的,可能出现正在给 A 对象分配内存,指针还没有来得及修改,对象 B 又同时使用原来的指针进行内分配的情况。解决这个问题有两种可选的方案:一种是对内存分配空间的动作进行同步处理-实际上虚拟机是采用CAS + 失败重试的方式来保证更新操作的原子性。另外一种就是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thred Local Allocation Buffer, TLAB), 那个线程要分配内存,就在那个线程分配内存,就在那个线程的本地缓冲中分配,只有本地缓冲用完了,分配新的缓冲区时才需要同步锁定,虚拟机是否使用 TLAB,可以通过 -XX:+/-UseTLAB参数设置。

对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构:

对象头结构

Mark Word (64bit)

结合 openjdk 源码 markOop.hpp中我们可以看到:

两个指针变量说明:

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM 使用原子操作而不是 OS 互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM 通过 CAS 操作在对象的 Mark Word 中设置指向锁记录的指针。


ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器 Monitor 的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到 Monitor 以管理等待的线程。在重量级锁定的情况下,JVM 在对象的
ptr_to_heavyweight_monitor 设置指向 Monitor 的指针。

markOop.hpp中我们可以看到 文件的注释如下:

// 部分省略
//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
// 部分省略

klass

klass 对应 Java 的 CLass 类,一个对象 jvm 中就会生成一个 kclass 实例对象存储到 Java 类对象的元数据信息,在 jdk 1.8 中,将这块存储到元空间中。在对象头中存储的就是 klass 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 。

数组长度(只有数组对象有)

如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度 。

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段、方法内容。无论是从父类继承下来的,还是在子类中定义的,都在这里一一记录。

对齐填充

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象大小计算

  • 在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
  • 在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
  • 64位开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。数组长度4字节+数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+数组markword为4字节(64位未开启指针压缩的为8字节))+对齐4=16字节。
  • 静态属性不算在对象大小内。

打印对象状态

JOL(Java Object Layout)一款开源的用于分析 JVM 中对象布局的一个小工具。使用 Unsafe、JVMTI 和 Serviceability Agent (SA) 来解码实际的对象布局、占用空间和引用。这使得 JOL 比其他依赖堆转储、规范假设等的工具更准确。 maven 仓库依赖如下:


  org.openjdk.jol
  jol-core
  0.16

1.查看对象内部信息包括:对象内的字段布局、标题信息、字段值、对齐/填充。 ClassLayout.parseInstance(obj).toPrintable()

2.查看对象外部信息:包括引用的
:GraphLayout.parseInstance(obj).toPrintable()

3.查看对象占用内存空间的大小:GraphLayout.parseInstance(obj).totalSize()

完整代码:

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;

public class ObjectTest2 {

    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        System.out.println();
        System.out.println();
        System.out.println(GraphLayout.parseInstance(obj).toPrintable());
        System.out.println();
        System.out.println();
        System.out.println(GraphLayout.parseInstance(obj).totalSize());
    }
}

对象的访问定位

句柄访问

使用句柄访问方式,Java堆中将可能会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图所示:

直接访问

直接指针访问,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图下图所示:

对象访问方式对比

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发),但从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。

原文链接:
https://developer.51cto.com/article/705031.html?utm_source=tuicool&utm_medium=referral

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...