首页 技术 正文
技术 2022年11月16日
0 收藏 389 点赞 3,893 浏览 2462 个字

本文摘自深入理解 Java 虚拟机第三版

对象的创建

Java 是一门面向对象的语言,Java 程序运行过程中无时无刻都有对象被创建出来。从语言层面看,创建对象只是一个 new 关键字而已,而在虚拟机中,对象(仅限于普通 Java 对象,不包括数组和 Class 对象等)的创建又是怎么一个过程呢?

以 Hotspot 虚拟机为例,当虚拟机遇到一条字节码指令,首先会检查这个指令的参数是否能在常量池中定位到一个符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化,如果没有,那么会先执行对应的类加载过程

类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载过程中即可完全确定,为对象分配空间的任务实际上等同于把一块确定大小的内存区域从 Java 堆中划分出来,并分配给对象。划分内存的方式主要有两种:

  • 指针碰撞法

    即假设 Java 堆中内存是绝对规整的,已被使用的内存放在一边,空闲的内存放在另一边,中间有一个指针作为分界点的指示器,那么当需要分配内存时,只需要将指针向空闲空间的方向挪动一段与对象大小相等的距离即可

  • 空闲列表法

    这个情况就是 Java 堆中内存并不是规整的,已使用内存和空闲内存相互交错在一起。此时虚拟机必须维护一个列表,记录哪些内存块可用,分配时从列表中找出一块足够大的空间划分给对象实例,并更新列表记录

由此可见,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理的能力决定

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

内存分配完成后,虚拟机必须先将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了 TLAB 的话,这项工作也可以提前在 TLAB 分配时顺便进行。这步操作保证了对象的实例字段在 Java 代码中可以不赋初值就直接使用,使程序可以访问到这些字段的数据类型对应的零值

接下来,Java 虚拟机还需要对对象的对象头(Object Header)进行设置,对象头存放一些信息如这个对象是哪个对象的实例、如何找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等

上面的工作完成之后,从虚拟机的视角来看,一个新的对象已经诞生了,而从 Java 程序的角度来看,对象的创建才刚刚开始,对象的构造函数,即 Class 文件的 <init> 方法尚未执行,所有的字段都是默认的零值。new 指令之后会接着执行 <init> 方法,对对象进行初始化,这样一个真正可用的对象才算完全构造出来

对象的内存布局

在 HotSpot 虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

1. 对象头

Hotspot 虚拟机对象的对象头部分包括两类信息,第一类是用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有锁、偏向线程 ID、编向时间戳等。这部分数据的长度在 32 位和 64 位虚拟机中分别为 32 个比特和 64 个比特,官方称之为 “Mark Word”

但实际上,对象需要存储待运行时数据很多,已经超过了 32、64 位 Bitmap 结构所能记录的最大限度,因此 Mark Word 被设计成一个有着动态定义的数据结构,以便尽可能多的复用自己的存储空间,例如在 32 位 JVM 中是这样存储的:

对象头的另一个部分是类型指针,即对象指向它的类型元数据的指针,Java 虚拟机通过这个指针来确定这个对象是属于哪个类的实例。然而,并不是所有的虚拟机都会在对象头上保留类型指针,换句话说,查找对象的元数据信息不一定要经过对象本身。此外,如果对象是个数组,那么在对象头中还必须有一块用于记录数组长度的数据

2. 实例数据

实例数据部分是对象真正存储的有效信息,即我们在代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来

3. 对齐填充

对齐填充并不是必然存在的,它仅仅起着占位符的作用,因为 Hotspot 虚拟机的自动内存管理机制要求对象的大小必须是 8 字节的整数倍。因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全

对象的访问定位

Java 程序会通过栈上的 reference 数据来操作堆上的具体对象。在 Java 虚拟机规范中,只是规定 reference 类型是一个指向对象的引用,并没有定义要用什么方式实现。因此对象访问方式是由虚拟机自主实现的,主流的访问方式主要有使用句柄和直接指针两种:

  • 句柄访问

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

  • 直接指针

    reference 中存储的就是对象地址,所以如果只是访问对象本身的话,可以避免多次开销。另外,如果使用直接指针访问,Java 堆中对象的内存布局就必须考虑如何放置用于访问类型数据的相关信息

两种方式各有千秋,使用句柄的好处就是 reference 中存储的是稳定的句柄地址,如果对象被移动(例如进行垃圾收集),那么只会改变句柄中实例数据指针,而 reference 本身不需要修改。而使用直接指针的好处就是速度更快,节省时间开销。在 HotSpot 虚拟机中主要使用直接指针方式进行对象访问

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:8,966
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,487
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,332
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,115
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,748
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,783