JVM系列 | 对象的创建与存储
JVM系列 | 对象的生命周期1 对象的创建与存储
文章目录
- 前言
- 对象的创建过程
- 内存空间的分配方式
- 方式1 | 指针碰撞
- 方式2 | 空闲列表
- 线程安全问题 | 避免空间冲突的方式
- 方式1 | 同步处理(加锁)
- 方式2 | 本地线程分配缓存
- 对象的内存布局
- Part1 | 对象头
- Mark Word
- 类型指针
- Part2 | 实例数据
- Part3 | 对齐填充
- * 示例 | 代码与图
- 对象的访问定位
- 方式1 | 句柄
- 方式2 | 直接指针
- 差异对比
- 方法区 与 Java堆 的比较
- 资料来源
- 下一篇预览 | 对象的消亡-垃圾回收机制
前言
之前在《Java虚拟机运行时数据分区介绍》一文中介绍过Java对象一般都存储在堆中,本文章将在上篇文章的基础上,详细介绍一下对象的创建过程与堆内存模型。
对象的创建过程
Java程序运行过程中无时无刻不在创建对象。从语言层面上,创建对象通常仅仅是一个new关键字,但是从JVM的角度来说,当遇到一个new指令时,会做以下几件事情。
- 检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并且这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有则必须执行类的加载过程。
- 给对象分配内存空间,所需内存大小在类加载完成后便可以完全确定(内存空间的分配方式在下一个小结做详细介绍)
- JVM还要对对象进行必要的初始化,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等,这些信息存放在对象头之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
- 在此时所有的字段都为默认的0值,此时要执行构造函数,即Class文件中的
<init>()方法。
内存空间的分配方式
一般来说,内存分配有两种方式,分别如下:
方式1 | 指针碰撞
假设所有的对象是在内存中并排排列的,那么我们仅需要一个“分界指针”指出当前最后一个对象所在位置即可(也就是空闲空间的起始位置),这样一来,当创建新的对象的时候,只需要将该指针向后移动一个对象大小的空间即可。
为了方便理解,整张图:
- 当没有新对象进来的时候,分界指针指向空闲空间的起始位置
- 当进来新对象之后,假设新对象大小为1kb,那么指针就向后移动1kb的空间,这1kb就用来存储新的对象
该方式适用于标记整理算法、标记复制算法等垃圾回收算法。
方式2 | 空闲列表
如果对象不是有序的存储在内存中的,而是如下图一样散乱的存储(程序的最开始可能是有序的,随着对象被清理而不整理,则可能会出现这种情况),那么JVM会为空闲空间维护一个列表,该列表记录着所有空闲空间的起始位置与大小,当需要new一个新的对象的时候,会在列表中寻找到一个可以放得下该对象的空间,将对象放在该空间中,并更新空闲列表。
- 维护空闲空间列表
- 新的对象进来,在空闲空间列表中查找可以放得下该对象的空间
- 放入对象并更新空闲空间列表
该方式适用于标记清除算法。
线程安全问题 | 避免空间冲突的方式
线程安全问题:由于在对象的创建是一件非常频繁的事情,假设有100个线程同时需要创建对象,他们进入到堆内存后(以上述方式1为例),100个线程同时拿到了当前分界指针的位置,并在该位置开始写入内容,那么就会造成内存混乱,100个线程创建的对象互相重写前一个线程写入的内容,最终必定会造成该位置的内容不可用。
最终的结果可能比我画的图要复杂的多,各个对象的字节码穿插出现。
如何才能避免这种问题呢?JVM采用了两种方案:
方式1 | 同步处理(加锁)
事实上虚拟机采用的是CAS配上失败重试的方式保证更新操作的原子性,当一个线程抢占到锁之后,其它线程只能等待,当该线程执行结束后,该内存空间已经有对象了,且分界指针已经更新,此时其余线程再次抢占锁,如此循环往复,就没有了线程安全问题。
方式2 | 本地线程分配缓存
本地线程分配的方式如下图所示,JVM会为每一个线程分配一个新的空间用来存储接下来该线程中new出来的对象,这样就不会存在空间抢占问题,从而避免了线程安全问题。
- 为每一条线程分配空闲空间
- 将线程创建的对象写入其对应的缓存空间中去
- 若是缓存空间不足则重新分配空间
对象的内存布局
一个对象由三个部分组成,分别为对象头、实例数据、对齐填充,如下图所示:
这张图每个内容都有一定的含义,下面会详细介绍。
Part1 | 对象头
对象头一般存储两类信息:
Mark Word
Mark Word用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标识、线程持有锁、偏向线程ID、偏向时间戳等。这类数据在长度32位的虚拟机中长度为32比特,在未开启指针压缩技术的64位虚拟机中长度为64比特。官方称此为"Mark Word"。
对象需要存储的运行时数据很多,其实已经超过了64位Bit Map结构所能记录的最大限度,但是对象头中的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽可能多的数据,根据对象的状态复用自己的存储空间。
例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)。
类型指针
类型指针用于指向对象的类元数据。类元数据包含了类的结构信息,如类名、父类、接口、方法、字段等。这部分信息是静态的,不会在运行时发生变化。类型指针的主要作用是使JVM能够快速地找到对象对应的类信息,以便执行方法调用和字段访问等操作。
类型指针通常指向方法区中的类对象。方法区中包含了每个类的类元数据结构(Class Metadata),包括类的常量池、字段和方法的描述符、方法的字节码等。
此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
Part2 | 实例数据
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(OrdinaryObject Pointers,OOPs),相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。
Part3 | 对齐填充
对齐填充并不一定是必须存在的,仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍(也就是对象大小必须是8的倍数),对象头已经被精心设计成8的倍数,因此如果对象实例数据不满足8的倍数的话,会用对齐填充来对齐。
* 示例 | 代码与图
我们在Java中定义一个类,内容如下:
public class Example {int num1;byte byte1;boolean bool1;
}
假设对象头占用16字节,字段num1占用4字节,字段byte1占用1字节,字段bool1占用1字节,则对象的布局如下:
-
16字节的对象头
-
6字节的实例数据
-
由于实例数据不足8位,用2字节的对齐填充进行补齐
对象的访问定位
这一段写的比较多,看起来下划线、加粗、高亮交替出现也比较乱,但是真的很重要也很有趣,我尽量用较少的语言描述清楚。
关于下文会用到的栈、栈帧等内容可以在《【JVM】Java虚拟机运行时数据分区介绍》找到对应介绍。
我们在代码中定位对象的情况无非只有几种(代码如下):
- 作为成员变量
- 作为成员方法
- 作为方法参数
- 作为方法中的实例对象
不过,需要注意的是,无论作为哪种方式出现在代码中,最终对他们的操作都一定是在方法中的,而方法是以栈帧的形式出现在栈中的,所以对于对象的任何操作都可以理解成是从栈帧出发寻找到这个对象的存储地址。
OK,深入理解一下上面一段话,会想到什么呢?
有没有什么东西有操作内容却不是方法?
答案就是:代码块与静态代码块
先来简单复习下内容:代码块会在每次创建一个新的对象的时候执行一次,静态代码块会在类加载的过程中(初始化的时候)执行且仅执行一次。
代码块与静态代码块并不会创建栈帧。当类被加载的时候,JVM会为该类生成一个名为
<clinit>的方法,静态代码块会加入到该方法中一起执行。当使用构造方法创建一个对象的时候,JVM会为该构造函数创建一个新的栈帧,普通代码块作为构造方法的一部分一起执行,并且在这个栈帧前面运行。
public class JimExample {private JimExample e1; // 实例变量引用,存储在堆中private static JimExample e2; // 类变量引用,存储在方法区的静态区中// 'param' 是一个方法参数引用,存储在栈帧中public void m1(JimExample param) {// 'obj' 是一个局部变量引用,存储在栈帧中JimExample obj = new JimExample();}}
至此我们已经了解到了对于对象的使用(定位寻址)一定是从栈帧中出发的,而对对象的访问主要是通过句柄和直接指针来实现的。
方式1 | 句柄
使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference(在Java栈的本地变量表中)存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如下图所示:
注意:一个是实例数据,一个是类型数据,句柄池中两个指针为一个句柄,两个指针一个指向实例数据,一个指向类型数据。
方式2 | 直接指针
使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图下图所示:
注意:堆中存储的数据不再是句柄池,而是一个实实在在的对象,只不过该对象有一个指针指向了该对象的对象类型数据上,所以访问完整的对象要先通过reference找到该对象的实例数据,再通过实例数据中的指针找到对象的类型数据。
差异对比
使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发,具体可参见第3章),但从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。
方法区 与 Java堆 的比较
| 特性 | 方法区(Method Area) | 实例池/堆(Instance Pool/Heap) |
|---|---|---|
| 存储内容 | 类的结构信息、常量池、静态变量、字节码、类的元数据等。 | 通过new关键字创建的对象实例和数组。 |
| 生命周期 | 类加载时开始存在,类卸载时移除。 | 对象被创建时开始存在,GC回收时移除。 |
| 内存管理 | 使用不同于堆的管理机制,HotSpot JVM中, JDK 8前为永久代(PermGen),JDK 8后为元空间(Metaspace)。 | 分为新生代和老年代,新生代包括Eden区、 Survivor区(S0和S1)。 |
| 特点 | 存储静态信息,不随程序运行变化,垃圾回收较少。 | 存储动态数据,随程序运行变化,GC频繁执行。 |
资料来源
周志明:《深入理解Java虚拟机》
下一篇预览 | 对象的消亡-垃圾回收机制
JVM虚拟机每时每刻都在创建对象,但是如果创建出来的对象一直存放在内存中,那么程序的内存早晚有不够用的一天,在这里简单介绍一下C++的对象管理,可能不少资料中都提到过C++的程序员在代码中是神一样的存在,他们掌握着每一个对象的生死,这是由于C++需要使用delete/free关键字(与new关键字对应)手动删除创建出来的对象,代码如下所示:
int* p = new int; // 动态分配一个整数
*p = 10; // 使用这个整数
delete p; // 释放分配的内存
但是Java中并没有这种操作,这是由于JVM有垃圾回收机制,简单来说JVM会定期扫描实例对象是否还“有用”,如果没有用的话就给它清理掉。
相关文章:
JVM系列 | 对象的创建与存储
JVM系列 | 对象的生命周期1 对象的创建与存储 文章目录 前言对象的创建过程内存空间的分配方式方式1 | 指针碰撞方式2 | 空闲列表 线程安全问题 | 避免空间冲突的方式方式1 | 同步处理(加锁)方式2 | 本地线程分配缓存 对象的内存布局Part1 | 对象头Mark Word类型指针…...
【JavaScript 算法】快速排序:高效的排序算法
🔥 个人主页:空白诗 文章目录 一、算法原理二、算法实现三、应用场景四、优化与扩展五、总结 快速排序(Quick Sort)是一种高效的排序算法,通过分治法将数组分为较小的子数组,递归地排序子数组。快速排序通常…...
Excel如何才能忽略隐藏行进行复制粘贴?
你有没有遇到这样的情况:数据很多,将一些数据隐藏后,进行复制粘贴,结果发现粘贴后的内容仍然将整个数据都显示出来了!那么,Excel如何才能忽略隐藏行进行复制粘贴? 打开你的Excel表格 Excel如何…...
行人越界检测 越线 越界区域 多边形IOU越界判断
行人越界判断 越界判断方式:(1)bbox中心点越界(或自定义)(2)交并比IoU判断 越界类型:(1)越线 (2)越界区域 1.越线判断 bbox中心点xc、…...
「UCD」浅谈蓝湖Figma交互设计对齐
在现代数字产品的设计和开发过程中,选择合适的工具对于提高团队效率和保证产品质量至关重要。本文将从开发和设计两个不同的角度,探讨蓝湖和Figma两款流行工具的优势与不足,并提出结论和建议。 开发研发视角:蓝湖 优点: 清晰的设计规范:蓝湖为开发工程师提供了清晰的设计…...
VUE3 播放RTSP实时、回放(NVR录像机)视频流(使用WebRTC)
1、下载webrtc-streamer,下载的最新window版本 Releases mpromonet/webrtc-streamer GitHub 2、解压下载包 3、webrtc-streamer.exe启动服务 (注意:这里可以通过当前文件夹下用cmd命令webrtc-streamer.exe -o这样占用cpu会很少,…...
[PaddlePaddle飞桨] PaddleOCR-光学字符识别-小模型部署
PaddleOCR的GitHub项目地址 推荐环境: PaddlePaddle > 2.1.2 Python > 3.7 CUDA > 10.1 CUDNN > 7.6pip下载指令: python -m pip install paddlepaddle-gpu2.5.1 -i https://pypi.tuna.tsinghua.edu.cn/simple pip install paddleocr2.7…...
Python应用开发——30天学习Streamlit Python包进行APP的构建(15):优化性能并为应用程序添加状态
Caching and state 优化性能并为应用程序添加状态! Caching 缓存 Streamlit 为数据和全局资源提供了强大的缓存原语。即使从网络加载数据、处理大型数据集或执行昂贵的计算,它们也能让您的应用程序保持高性能。 本页仅包含有关 st.cache_data API 的信息。如需深入了解缓…...
python实现openssl的EVP_BytesToKey及AES_256_CBC加解密算法
python实现openssl EVP_BytesToKey(EVP_aes_256_cbc(), EVP_md5(), NULL, pass, passlen, 1, key, iv); 并实现AES 256 CBC加解密. # encoding:utf-8import base64 from Crypto.Cipher import AES from Crypto import Random from hashlib import md5def EVP_BytesToKey(passw…...
基于SpringBoot+VueJS+微信小程序技术的图书森林共享小程序设计与实现
注:每个学校每个老师对论文的格式要求不一样,故本论文只供参考,本论文页数达到60页以上,字数在6000及以上。 基于SpringBootVueJS微信小程序技术的图书森林共享小程序设计与实现 目录 基于SpringBootVueJS微信小程序技术的图书森…...
【css】image 使用 transform:scale 放大后显示不全的问题
css 可以用 transform: scale(1.2) 实现图片放大 1.2 倍显示的功能,在此基础上可以修改 transform-origin 为用户点击的坐标值优化体验。问题在于 origin 位于图片下方时,图片放大后出现滚动条,而滚动条的高度会忽略放大显示的图片的上半部分…...
损失函数简介
损失函数(Loss Function)是机器学习中用来衡量模型预测值与真实值之间差异的函数。在训练过程中,通过最小化损失函数来优化模型的参数,以提高模型的预测准确性。 以下是损失函数的主要用途和一些常用的损失函数类型: 损失函数的用途: 评估模型性能:损失函数提供了一个…...
2023睿抗CAIP-编程技能赛-本科组省赛(c++)
RC-u1 亚运奖牌榜 模拟 AC: #include<iostream> using namespace std; struct nation{int j,y,t; }a[2]; int main(){int n;cin>>n;for(int i1;i<n;i){int x,y;cin>>x>>y;if(y1) a[x].j;if(y2) a[x].y;if(y3) a[x].t;}cout<<a[0].j<<&…...
现在国内的ddos攻击趋势怎么样?想了解现在ddos的情况该去哪看?
目前,国内的DDoS攻击趋势显示出以下几个特征: 攻击频次显著增加:根据《快快网络2024年DDoS攻击趋势白皮书》,2023年DDoS攻击活动有显著攀升,总攻击次数达到1246.61万次,比前一年增长了18.1%。 攻击强度和规…...
微服务到底是个什么东东?
微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。 每个服务运行在其独立的进程中,服务和服务间采用轻量级的通信机制互相沟通(通常是基于 HTTP 的…...
C++笔试强训5
文章目录 一、选择题1-5题6-10题 二、编程题题目一题目二 一、选择题 1-5题 x1,先x,再x–,while判断永远为真,故死循环 选D。 sizeof会计算\0,strlen不包括\0,并且strlen只计算\0之前的。 所以sizeof是10,strken是4 …...
初学51单片机之UART串口通信
CSDN其他博主的博文(自用)嵌入式学习笔记9-51单片机UART串口通信_51uart串口通讯-CSDN博客 CSDN其他博主的博文写的蛮好,如果你想了解51单片机UART串口可以点进去看看: UART全称Universal Asynchronous Receiver/Transmitter即通…...
数据结构——查找(线性表的查找与树表的查找)
目录 1.查找 1.查找的基本概念 1.在哪里找? 2.什么查找? 3.查找成功与否? 4.查找的目的是什么? 5.查找表怎么分类? 6.如何评价查找算法? 7.查找的过程中我们要研究什么? 2.线性表…...
MySQL入门学习-深入索引.组合索引
在 MySQL 中,组合索引(也称为复合索引)是在多个列上创建的索引。以下是关于组合索引的详细信息: 一、组合索引的概念: - 组合索引是基于多个列创建的索引结构。它可以提高在这些列上进行查询的效率。 二、深入理解组…...
RABBITMQ的本地测试证书生成脚本
由于小程序要求必须访问wss的接口,因此需要将测试环境也切换到https,看了下官方的文档 RabbitMQ Web STOMP Plugin | RabbitMQ里面有这个信息 然后敲打GPT一阵子,把要求输入几个来回,得到这样一个脚本: generate_cer…...
【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
【算法训练营Day07】字符串part1
文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接:344. 反转字符串 双指针法,两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...
C++ 基础特性深度解析
目录 引言 一、命名空间(namespace) C 中的命名空间 与 C 语言的对比 二、缺省参数 C 中的缺省参数 与 C 语言的对比 三、引用(reference) C 中的引用 与 C 语言的对比 四、inline(内联函数…...
QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...
[Java恶补day16] 238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度…...
css3笔记 (1) 自用
outline: none 用于移除元素获得焦点时默认的轮廓线 broder:0 用于移除边框 font-size:0 用于设置字体不显示 list-style: none 消除<li> 标签默认样式 margin: xx auto 版心居中 width:100% 通栏 vertical-align 作用于行内元素 / 表格单元格ÿ…...
