JVM—Java对象是如何创建、存储和访问的?

Java程序员都知道如何创建对象,不就是一个 Person person = new Person() 的语句就解决了么?然而,我们只知道new,却对于底层如何实现对象的创建、如何存储到内存中去、又如何被访问的知之甚少。

对象的创建

流程图

JVM—Java对象是如何创建、存储和访问的?

创建流程

  1. Java程序new一个对象。
  2. 虚拟机遇到一条 new指令 时,首先检查这个指令的参数是否能在常量池中定位到一个类的 符号引用 ,且检查该符号引用代表的 类是否已被加载、解析和初始化过 。若没有,需先进行相应的类加载过程。
  3. 在类加载检查通过后,虚拟机将为新生对象 分配内存 。(对象在内存中所需要的大小在类加载完成后就确定了)
  4. 内存分配完之后,虚拟机需要将分配到的内存空间 初始化为零值 (不包括对象头)。保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,可以访问对应的零值。(对应准备阶段)
  5. 虚拟机对对象进行必要的设置( 对象头的设置 )。如这个对象是哪个类的实例、如何找到类的元数据信息、对象哈希码、对象的GC分代年龄等信息。
  6. 以上虚拟机中新对象产生,对应到Java程序还需要继续执行 <init> 方法,将对象在程序中进行初始化。

内存空间分配方式

为对象分配空间就是从 Java堆 中划分出一块确定大小的内存给新生对象,考虑符合划分可用空间的两种方式: “指针碰撞”和“空闲列表”

  • 指针碰撞 :若Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个 指针作为分界点的指示器 ,所分配内存仅仅是把那个 指针向空闲空间那边挪动一段与对象大小相等的距离 。在使用 Serial、ParNew收集器时 等带有 Compact过程 时,系统分配算法是指针碰撞。
  • 空闲列表 :Java堆中内存不是规整的,已使用的内存和空闲的内存相互交错,VM需维护一个列表,记录上哪些内存是可用的,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。使用 CMS收集器 时,就是采用的空闲里列表,CMS是基于 Mark-Sweep算法(标记-清除) 的收集器。

并发安全问题

Java对象创建在程序中是非常常见的,所以在VM中对象创建是非常频繁,容易出现 多线程并发安全问题 :如程序中创建对象A和对象B,底层VM给A对象分配内存,指针没来及修改,对象B同时使用原来的指针分配内存。 解决方案有两种: 同步处理和本地线程分配缓冲

  • 同步处理 :分配内存空间的动作进行同步处理(CAS操作),VM采用 CAS配上失败重试 的方式保证更新操作的 原子性
  • 本地线程分配缓冲 :Thread Local Allocation Buffer, TLAB,把内存分配的动作 按照线程划分不同的空间 之中进行,即 每个线程在Java堆中预先分配一小块内存 ,即为TLAB,哪个线程要分配内存,就在哪个线程的TLAB上分配,只有用完后并分配新的TLAB,才需要同步定。通过 -XX:+/-UseTLAB 参数设定是否需要使用TLAB。

对象的内存布局

概述

Java对象在内存存储的布局分为3块: 对象头、实例数据和对齐填充

对象头

对象头(Header)分为两部分: 用于存储对象自身的运行时数据和类型指针

运行时数据

Mark Word ,用于存储对象自身的运行时数据包括:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

存储内容 标志位 状态
对象哈希码、GC分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标志
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

Mark Word是一个 非固定 的数据结构,在极小的空间内存储尽量多的数据,会根据对象的状态 复用 自己的存储空间,如在32位HotSpot VM中,若对象处于未锁定状态,Mark Word的32bit空间中25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,即 32(存储空间)=25(哈希码)+4(分代年龄)+2(锁标志位)+1(固定0)

类型指针

对象指向它的类元数据的指针 ,虚拟机通过这个指针来确定对象是哪个类的实例,但是并非查找对象的元数据就一定要通过对象本身,也只是适用于普通对象, 普通Java对象可以通过元数据信息 可以确定Java对象的大小。 不适用的Java对象,如Java数组对象 的对象头中必须有一块能保持记录数组长度的数据,因为从数组元数据中无法确定数组的大小。

实例数据

实例数据(Instance Data)是 对象真正存储的有效信息 ,也是程序代码中定义的 各种类型的字段内容 。这部分存储顺序会受到 VM分配策略参数字段在Java源码中定义顺序 的影响。

VM默认分配策略

HotSpot默认分配策略为longs/doubles、ints、shorts/chars、bytes/nooleans、oops,相同宽度的字段会被分配到一起,在父类中定义的变量会出现在子类之前。

对齐填充

对齐填充(Padding)是 非必要 的,只是起着 占位符的作用 。VM自动内存管理系统要求对象起始地址(对象大小)必须是 8字节的整数倍 ,对象头都是8字节的整数倍,而实例数据部分若没有8字节的整数倍,可以通过对齐填充进行补全。

对象的访问方式

概述

Java程序通过栈上的reference数据类操作堆上的具体对象(栈中的局部变量表存储了对象名的变量,堆中存储了对象的具体地址)。主流的对象访问定位方式有两种: 使用句柄和直接指针

使用句柄

使用句柄访问对象,Java堆中会划分出一块内存作为 句柄池reference中存储 的就是 对象的句柄地址 ,句柄中包含了对象实例数据与类型数据各自的具体地址信息。

JVM—Java对象是如何创建、存储和访问的?

直接指针

使用直接指针访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference中存储 的直接就是 对象地址 。(Sun HotSport VM的使用方式)

JVM—Java对象是如何创建、存储和访问的?

原文 

https://juejin.im/post/5f12f834e51d4534c243ada4

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » JVM—Java对象是如何创建、存储和访问的?

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址