对象在Java堆中创建、分配和访问
我们已经知道Java虚拟机有5个内存区域
- 程序计数器不可能发生内存溢出
- 虚拟机栈和本地方法栈在同时执行大量方法时可能会出现内存溢出。
- 我们最关注的还是对象在java堆中如何创建、分配和布局。
1. 对象的创建
对象如何被创建出来的
- 虚拟机加载到new指令
- 检查这个指令的参数是否在常量池中定位到一个类的符号引用
- 检查这个符号引用代表的类是否已被加载、解析和初始化
- 如果没有,执行加载、解析和初始化步骤
- 有,继续
- 虚拟机为新生对象分配内存
- 虚拟机将分配到的内存空间都初始化为零值
- 虚拟机对对象进行必要的设置
- 执行<init>方法进行初始化
- 对象创建完成
问题1:第2步是什么意思
-
理解常量池:class文件中,用于存放编译器各种字面量和符号引用。
而符号引用的作用就是虚拟机在运行时可以通过符号引用找到该类的具体引用。
当然这里分两种情况:- 若类是接口类型,则运行时才可以确定具体引用。
- 若类是不可变的,则编译器就可以确定具体引用。
-
定位到常量池的符号引用,就可以找到该类的具体引用,才能进一步检查类的真实情况。
虚拟机如何为新生对象分配内存
虚拟机采用两种方式分配内存,这取决于java堆中内存是否完全规整。
- 内存规整:即所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器。
分配方式:指针碰撞,即中间分界点的指针向空闲空间那边移动一段与对象大小相等的距离。 - 内存不规整:即所用的内存和空闲的内存相互交错
分配方式:空闲列表,即虚拟机维护一个表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上记录。
java堆内存为什么会有规整和不规整的情况呢
java堆内存是否规整取决于所采用的垃圾收集器上是否带有压缩整理功能。
带有压缩整理功能的垃圾收集器可以让堆内存规整。
这么说是有不同垃圾收集器
是的,不同的堆区域使用不同的垃圾收集器,后续介绍垃圾收集器。
有个问题,堆内存是共享的,当不同线程并发创建对象分配内存时,无论使用哪种分配方式都不是线程安全呀
有两种方案:
- 对分配内存空间的动作做同步处理
虚拟机采用CAS配上失败重试保证操作的原子性。
CAS即:compare and swap, 比较替换。方法有三个参数:最新地址v, 期待值a, 更新值b。先查v值,若v==a, 则更新b, 若v!=a,则失败重试,则循环拿最新的值b,直到拿到期望的值。 - 每个线程在Java堆中预先分配一小块内存,叫做本地线程分配缓存(Thread Local Allocation Buffer)
在TLAB上分配内存空间。当TLAB上内存用完并分配新的TLAB时,才需要同步锁定。
什么时候使用TLAB呢
使用TLAB可以通过参数设定:-XX:+/-UseTLAB
如果使用了TLAB, 虚拟机将分配到的内存空间初始化为零值这一操作,提前至TLAB分配时进行,提高性能。
为什么需要将分配的内存空间初始化为零值这一操作
这一步骤保证了Java代码在不经过初始化步骤就可以直接使用对象的实例字段。这些实例字段都是对应的零值。
对对象进行必要设置,是哪些设置
- 设置对象对应的类
- 设置类的元数据引用
- 设置对象hash
- 设置对象的GC分代年龄
- 是否启用偏向锁
这些对象的设置是存放在哪里的,如何能看到
这些设置都是存放在对象的对象头中,这一步骤也可以说:设置对象头信息。
对象在Java堆中分配内存空间,Java对象在内存中如何布局的呢
对象在内存中分为三个部分:
- 对象头
- 实例数据
- 对齐填充
之前在创建对象时有一步就是设置对象头信息,这对象头中存储哪些信息
对象头有两部分信息:
- 存储对象自身运行时信息,这些信息被称为
Mark Word
,为了空间利用率,对象状态改变就会更新这些信息- HashCode
- GC年龄分代 (垃圾回收会说明,简单的说就是对象经历过一次GC没死,年龄+1,到15岁就从新生代到老年代)
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 存储类型指针,即对象指向它类元数据的指针,来确定是哪个类的实例
- 如果对象是数组,还存储记录数组长度的数据
实例数据存储的是程序代码定义的各种类型的字段内容吗
是的,这部分也是对象真正存储的有效信息。
包括父类继承的和子类定义的。
对齐填充存储的是什么
对齐填充不是必然存在,仅仅是占位符。
虚拟机的自动内存管理系统要求:对象起始地址必须是8字节的整数倍。
如果对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象存在堆中,对象引用存在栈中,栈中对象引用如何定位到堆中对象
虚拟机规范并没有明确规定,不同的虚拟机有不同的实现。主流实现有两种:
- 句柄访问:Java堆划分出一块内存作为句柄池,栈中对象引用存储的是句柄池中句柄地址,句柄中包含对象实例数据地址和对象对应的类数据地址。
栈对象引用 -> 句柄池中句柄
- 句柄信息1 -> 堆中对象实例
- 句柄信息2 -> 方法区中对象对应的类数据
- 直接指针:栈中对象引用存储的是对象实例地址,对象实例中有对象对应的类数据地址
栈对象引用 -> 堆中对象实例 -> 方法区对象对应的类数据
两种方法都有什么优点
- 句柄访问:减少指针维护。
在GC发生时,大量对象被移动,这时只需改动句柄池指针即可。 - 直接指针:减少一次指针引用。
减少一次指针的引用查找的时间开销,对象查询的时间更快。
对象的访问在Java中非常频繁,这会大大优化程序的执行效率。
Sun HotSpot虚拟机使用的就是直接指针。
总结
本节在知道Java虚拟机五个数据区域后,重点关注了Java堆中对象如何创建,内存如何分配,对象如何布局等信息。
-
对象创建
- 虚拟机加载到new指令
- 检查指令参数在常量池中定位到一个符号引用
- 检查符号引用的直接引用类,是否已加载、解析和初始化
- 没有,则执行类的加载、解析和初始化
- 在堆内存中给对象分配内存空间
- 分配方式
- 指针碰撞
- 概念:堆内存完全规整,使用的内存在一边,空闲的在另一边,中间有个指针作为分界点。分配空间时,只需要将分界点指针移动对象空间大小的距离即可。
- 原理:GC带内存整理功能,堆内存保持完全规整。
- 空闲列表
- 概念:堆内存是非连续的,虚拟机维护一个表,记录所有未使用的内存空间。分配空间时,查找表,找到足够大的内存空间分配,并更新空闲列表。
- 原理:GC不带内存整理功能,堆内存时非连续的。
- 指针碰撞
- 解决并发冲突
- 分配内存动作进行同步
- 原理:使用CAS配上失败重试进行同步处理
- 使用TLAB分配内存空间
- 原理:每个线程都在堆中开辟一个小空间——Thread Local Allocation Buffer, 各个内存上的对象在各自线程的TLAB上分配。只有当TLAB空间使用完并分配新的TLAB时,使用锁同步。
- 分配内存动作进行同步
- 分配方式
- 给对象的内存空间都初始化为0值
- 设置对象头信息
- 执行初始化<init>方法
-
对象在Java堆中布局
- 对象头
- 运行时对象信息
- HashCode
- GC年龄分代
- 锁状态标志
- 线程持有锁
- 偏向线程ID
- 偏向时间戳
- 对象对应的类信息
- 对应的类数据信息。
- 特殊情况:虚拟机使用
句柄访问
对象,则类上不会有对应的类信息。
- 运行时对象信息
- 实例数据
- 程序代码定义的各种类型的字段信息
- 对齐填充
- 满足虚拟机规定:对象起始位置必须是8的整数倍,不满足的自动补全。
- 对象头
-
对象访问方式
-
- 句柄访问
- 原理:在堆中建立句柄池,栈中对象引用 -> 句柄池句柄 ( 句柄信息1 -> 堆中对象实例; 句柄信息2 -> 方法区中对象对应的类数据)
- 优势:GC时大量移动对象,只需要修改句柄池中引用,无需修改栈中对象引用,减少引用的维护。
- 缺点:对象访问时多一步指针定位的时间开销,Java程序对象访问很多,效率有一定降低。
-
- 直接指针
- 原理:栈中对象引用 -> 堆中对象实例 -> 方法区对象对应的类数据
- 优势:减少对象访问的一次指针定位的时间开销,Java程序对象访问很多,效率有一定提升。
- 缺点:GC时大量移动对象,需要修改所有关联的栈中对象引用
-
想共同学习jvm的可以加我微信:1832162841,或者进QQ群:982523529
网友评论