【JVM】对象的生命周期一 | 对象的创建与存储

news2024/9/27 17:25:28

Java | 对象的生命周期1-对象的创建与存储

文章目录

  • 前言
  • 对象的创建过程
    • 内存空间的分配方式
      • 方式1 | 指针碰撞
      • 方式2 | 空闲列表
    • 线程安全问题 | 避免空间冲突的方式
      • 方式1 | 同步处理(加锁)
      • 方式2 | 本地线程分配缓存
  • 对象的内存布局
    • Part1 | 对象头
      • Mark Word
      • 类型指针
    • Part2 | 实例数据
    • Part3 | 对齐填充
    • * 示例 | 代码与图
  • 对象的访问定位
    • 方式1 | 句柄
    • 方式2 | 直接指针
    • 差异对比
    • 方法区 与 Java堆 的比较
  • 资料来源
  • 下一篇预览 | 对象的消亡-垃圾回收机制

前言

之前在《Java虚拟机运行时数据分区介绍》一文中介绍过Java对象一般都存储在堆中,本文章将在上篇文章的基础上,详细介绍一下对象的创建过程与堆内存模型。

对象的创建过程

Java程序运行过程中无时无刻不在创建对象。从语言层面上,创建对象通常仅仅是一个new关键字,但是从JVM的角度来说,当遇到一个new指令时,会做以下几件事情。

  1. 检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并且这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有则必须执行类的加载过程。
  2. 给对象分配内存空间,所需内存大小在类加载完成后便可以完全确定(内存空间的分配方式在下一个小结做详细介绍)
  3. JVM还要对对象进行必要的初始化,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等,这些信息存放在对象头之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  4. 在此时所有的字段都为默认的0值,此时要执行构造函数,即Class文件中的<init>()方法。

内存空间的分配方式

一般来说,内存分配有两种方式,分别如下:


方式1 | 指针碰撞

假设所有的对象是在内存中并排排列的,那么我们仅需要一个“分界指针”指出当前最后一个对象所在位置即可(也就是空闲空间的起始位置),这样一来,当创建新的对象的时候,只需要将该指针向后移动一个对象大小的空间即可。

为了方便理解,整张图:

在这里插入图片描述

  1. 当没有新对象进来的时候,分界指针指向空闲空间的起始位置
  2. 当进来新对象之后,假设新对象大小为1kb,那么指针就向后移动1kb的空间,这1kb就用来存储新的对象

该方式适用于标记整理算法、标记复制算法等垃圾回收算法。


方式2 | 空闲列表

如果对象不是有序的存储在内存中的,而是如下图一样散乱的存储(程序的最开始可能是有序的,随着对象被清理而不整理,则可能会出现这种情况),那么JVM会为空闲空间维护一个列表,该列表记录着所有空闲空间的起始位置与大小,当需要new一个新的对象的时候,会在列表中寻找到一个可以放得下该对象的空间,将对象放在该空间中,并更新空闲列表

在这里插入图片描述

  1. 维护空闲空间列表
  2. 新的对象进来,在空闲空间列表中查找可以放得下该对象的空间
  3. 放入对象并更新空闲空间列表

该方式适用于标记清除算法



线程安全问题 | 避免空间冲突的方式

线程安全问题:由于在对象的创建是一件非常频繁的事情,假设有100个线程同时需要创建对象,他们进入到堆内存后(以上述方式1为例),100个线程同时拿到了当前分界指针的位置,并在该位置开始写入内容,那么就会造成内存混乱,100个线程创建的对象互相重写前一个线程写入的内容,最终必定会造成该位置的内容不可用。

在这里插入图片描述

最终的结果可能比我画的图要复杂的多,各个对象的字节码穿插出现。

如何才能避免这种问题呢?JVM采用了两种方案:


方式1 | 同步处理(加锁)

事实上虚拟机采用的是CAS配上失败重试的方式保证更新操作的原子性,当一个线程抢占到锁之后,其它线程只能等待,当该线程执行结束后,该内存空间已经有对象了,且分界指针已经更新,此时其余线程再次抢占锁,如此循环往复,就没有了线程安全问题。


方式2 | 本地线程分配缓存

本地线程分配的方式如下图所示,JVM会为每一个线程分配一个新的空间用来存储接下来该线程中new出来的对象,这样就不会存在空间抢占问题,从而避免了线程安全问题。

在这里插入图片描述

  1. 为每一条线程分配空闲空间
  2. 将线程创建的对象写入其对应的缓存空间中去
  3. 若是缓存空间不足则重新分配空间




对象的内存布局

一个对象由三个部分组成,分别为对象头、实例数据、对齐填充,如下图所示:

在这里插入图片描述

这张图每个内容都有一定的含义,下面会详细介绍。

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字节,则对象的布局如下:

在这里插入图片描述

  1. 16字节的对象头

  2. 6字节的实例数据

  3. 由于实例数据不足8位,用2字节的对齐填充进行补齐


对象的访问定位

这一段写的比较多,看起来下划线、加粗、高亮交替出现也比较乱,但是真的很重要也很有趣,我尽量用较少的语言描述清楚。

关于下文会用到的栈、栈帧等内容可以在《【JVM】Java虚拟机运行时数据分区介绍》找到对应介绍。

我们在代码中定位对象的情况无非只有几种(代码如下):

  1. 作为成员变量
  2. 作为成员方法
  3. 作为方法参数
  4. 作为方法中的实例对象

不过,需要注意的是,无论作为哪种方式出现在代码中,最终对他们的操作都一定是在方法中的,而方法是以栈帧的形式出现在栈中的,所以对于对象的任何操作都可以理解成是从栈帧出发寻找到这个对象的存储地址。

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会定期扫描实例对象是否还“有用”,如果没有用的话就给它清理掉。




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

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

相关文章

昇思25天学习打卡营第1天|初步了解

1在昇思平台上申请过相关资源之后&#xff0c;将示例代码粘贴到输入框内。可以在下图中创建一个新的文档。 2不过初次运行的时候会遇到一个问题&#xff0c;点击运行的时候会出现新的输入框&#xff0c;而不是直接运行。遇到此问题等待就可以了&#xff0c;或者稍微等一下再运…

linux系统判断网络物理连接状态

最近发现 /sys/class/net/ 似乎可以获取网口物理连接状态 于是乎在T113i主板上进行了测试。当前是双网口交换机芯片。如图所示&#xff1a; 具体的是 eth0和eth2是单网口&#xff0c;eth1是交换机芯片接的四个网口。 对于carrier 下面对单网口和交换机芯片的网口进行测试。命…

鸿蒙语言基础类库:【@ohos.util.Vector (线性容器Vector)】

线性容器Vector 说明&#xff1a; 本模块首批接口从API version 8开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。开发前请熟悉鸿蒙开发指导文档&#xff1a;gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 Vect…

Perl语言简介

1.简介 Perl 是 Practical Extraction and Report Language 的缩写&#xff0c;可翻译为"实用报表提取语言"。   Perl 是高级、通用、直译式、动态的程序语言。   Perl 最初的设计者为拉里沃尔&#xff08;Larry Wall&#xff09;&#xff0c;于1987年12月18日发…

python+pygame实现五子棋人机对战之三

上回讲过&#xff1a; pythonpygame实现五子棋人机对战之一 pythonpygame实现五子棋人机对战之二 界面已经有了&#xff0c;并且可以支持鼠标操作选择菜单和人机对战开始下棋了&#xff0c;那电脑是如何应手落子呢&#xff1f;以下内容是通用的类&#xff0c;全部放在utils.…

SQL优化之深分页

SQL优化之深分页 我们都知道&#xff0c;大型项目中的SQL语句&#xff0c;应该尽量避免深分页。 那么问题就来了&#xff1a; 深分页的性能差在哪&#xff1f;什么方案能避免深分页呢&#xff1f; 什么是深分页 深分页&#xff0c;即SQL查询过程中&#xff0c;使用的页数过…

无需构建工具,快速上手Vue2 + ElementUI

无需构建工具&#xff0c;快速上手Vue2 ElementUI 在前端开发的世界中&#xff0c;Vue.js以其轻量级和易用性赢得了开发者的青睐。而Element UI&#xff0c;作为一个基于Vue 2.0的桌面端组件库&#xff0c;提供了丰富的界面组件&#xff0c;使得构建美观且功能丰富的应用变得…

第6章 IT服务运营管理

第6章 IT服务运营管理 6.1 概述 大量企业的实践表明&#xff0c;IT服务运营方面的问题更多的不是来自产品或技术&#xff08;如硬件、软件、网络、电力故障等&#xff09;方面&#xff0c;而是来自管理方面。IT服务的提供者&#xff0c;无论是企业内部的IT部门&#xff0c;还…

nuPlan 是一个针对自动驾驶车辆的闭环机器学习(ML-based)规划基准测试

nuPlan: A closed-loop ML-based planning benchmark for autonomous vehicles nuPlan 是一个针对自动驾驶车辆的闭环机器学习&#xff08;ML-based&#xff09;规划基准测试 Abstract In this work, we propose the world’s first closed-loop ML-based planning benchmar…

Linux文件编程应用

目录 一、实现cp命令 二、修改程序的配置文件 三、写一个整数/结构体到文件 1.写一个整数到文件 2.写一个结构体到文件 四、写结构体数组到文件 我们学习了文件编程的常用指令以及了解文件编程的基本步骤后&#xff0c;试着来写一些程序实现某些功能。&#xff08;没有学…

Java设计模式的7个设计原则

Java设计模式的7个设计原则是面向对象设计领域中的重要指导方针&#xff0c;它们旨在提高软件系统的可维护性、可扩展性、可复用性和灵活性。以下是这7个设计原则的详细解释&#xff1a; 1. 开闭原则&#xff08;Open-Closed Principle, OCP&#xff09; 定义&#xff1a;一个…

体验一下智能指针的强大

课程总目录 文章目录 一、智能指针基础知识二、不带引用计数的智能指针auto_ptrscoped_ptrunique_ptr 三、带引用计数的智能指针模拟实现一个带引用计数的智能指针shared_ptr交叉引用问题 四、多线程访问共享对象的线程安全问题五、智能指针删除器六、建议用make_shared代替sha…

如何找工作 校招 | 社招 | 秋招 | 春招 | 提前批

马上又秋招了&#xff0c;作者想起以前读书的时候&#xff0c;秋招踩了很多坑&#xff0c;但是第一份工作其实挺重要的。这里写一篇文章&#xff0c;分享一些校招社招的心得。 现在大学的情况是&#xff0c;管就业的人&#xff0c;大都是没有就业的辅导员&#xff08;笔者见过…

JavaWeb(一:基础知识和环境搭建)

一、基本概念 1.前言 JavaWeb&#xff1a;在Java中&#xff0c;动态web资源开发网页的技术。 web应用分两种&#xff1a;静态web资源和动态web资源 Ⅰ. 静态web资源&#xff08;如html 页面&#xff09;&#xff1a; 指web页面中的数据始终是不变。 所有用户看到都是同一个…

基于YOLOV8的数粒机-农业应用辣椒种子计数计重双标质量解决方案

一:辣椒种子行业背景调查 中国辣椒年产量稳居世界第一,食辣人口超5亿。中国辣椒全球闻名,小辣椒长成大产业,带动全球食品行业腾飞。 在中国,“辣”是不少地方餐桌上的一大特色。从四川的麻辣火锅到湖南的剁椒鱼头再到陕西的油泼辣子面,由南到北,总有食客对辣有着独一份偏…

力扣-回溯法

何为回溯法&#xff1f; 在搜索到某一节点的时候&#xff0c;如果我们发现目前的节点&#xff08;及其子节点&#xff09;并不是需求目标时&#xff0c;我们回退到原来的节点继续搜索&#xff0c;并且把在目前节点修改的状态还原。 记住两个小诀窍&#xff0c;一是按引用传状态…

连接与隔离:Facebook在全球化背景下的影响力

在当今全球化的背景下&#xff0c;Facebook作为全球最大的社交网络平台&#xff0c;不仅连接了世界各地的人们&#xff0c;还在全球社会、经济和文化中发挥着深远的影响。本文将深入探讨Facebook在全球化进程中的作用&#xff0c;以及其对个体和社会之间连接与隔离的双重影响。…

【区块链农场】:农场游戏+游戏

我的酒坊是一款非常受玩家欢迎的经营手游,游戏中你需要合理经营一家酒厂,将其做大做强。通过制定合理的战略,例如新建厂房,并采用传统工艺制作,针对不同的人群研制多重口味。

Ubuntu与Windows通过WIFI与以太网口共享网络,Ubuntu与Windows相互ping通,但ping百度失败

Linux开发板&#xff08;正点原子阿尔法_IMX6U&#xff09;与Ubuntu的文件传输SCP 报错 SSH: no matching host key type found. Their offer: ssh-rsa-CSDN博客 前面的文章提到了如何将Ubuntu与Windows通过WIFI共享网络给以太网&#xff0c;从而实现Linux开发板、Ubuntu、Win…

Umi.js 项目中使用 Web Worker

1.配置 Umi.js 在 Umi.js 中&#xff0c;需要通过配置来扩展 Webpack 的功能。在项目根目录下修改 config/config.ts 文件&#xff1a; export default defineConfig({chainWebpack(config) {config.module.rule(worker).test(/\.worker\.ts$/).use(worker-loader).loader(wo…