JVM——对象创建
一、对象的创建过程
当虚拟机遇到一条字节码new指令时,
① 检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析、初始化过,如果没有,需要先执行类加载过程。
② 类加载之后,虚拟机会为新生对象分配内存。对象所需内存的大小在类加载完成后便可以完全确定。分配内存即从Java堆中划分一块固定大小的内存。
Java堆内存分配管理采用操作系统中的内存连续分配(“指针碰撞”)+“空闲表法”(管理空闲内存的方法之一)。
由于创建对象是非常频繁的现象,无论上述两种内存分配,都是修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能导致创建两个对象占用到了同一块内存
解决方案:
- 通过CAS失败重试的方法保证内存分配时的原子性
- 不同线程:给每个线程预先分配一小块内存TLAB,将线程创建对象时分配的内存,进行了内存隔离,当TLAB用完后,再分配新的TLAB时再采用CAS保证原子性。
虚拟机是否使用TLAB,可通过-XX:+/-UseTLAB参数来设定
③ 内存分配完成后会将内存空间(除对象头外)初始化为零值,这大概也是实例变量具有默认值原因吧。
④ 接下来,Java虚拟机还要对对象的对象头进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,
⑤ 上面工作完成后,Java虚拟机视角来看,一个对象已经产生。但是从Java程序角度来看,对象创建才刚刚开始——构造函数,即class文件中的<init>方法执行
二、对象的内存布局
在HotSpot虚拟机中,对象在堆内存中的存储布局可以分为三部分:对象头、实例数据、对齐填充。
1、对象头
对象头包含两类信息:
- “Mark Word”:用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32/64位虚拟机中分别是32/64bit.
- 类型指针:对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
其中“Mark Word”储存的对象运行时数据,其实已经超出的32/64bit结构所能记录的最大限度,
2、实例数据与对齐填充
实例数据部分是对象真正存储的有效信息,这些数据存储有一个优化:存储顺序优化
①存储顺序优化
HotSpot虚拟机默认分配顺序为longs/doubles、ints、shorts、chars、bytes、booleans、opps,可以看出顺序从大到小
-XX:FieldsAllocationStyle参数控制分配顺序策略 -- 在HotSpot的分配顺序下 -XX:CompactFields参数默认为true,子类变量被允许插入到父类变量的空隙中,为false时,父类在前,子类在后,各自再按照分配顺序
存储顺序优化其实就是为了避免对齐填充,
②对齐填充
对齐填充也叫边界对齐,是对“内存低位交叉编址”的一种优化,规定分配类型的其实地址必须是大小的倍数。例如一个int类型,其实地址必须是32的倍数。
现代的内存存储器基本都是有多个存储器组合而成的,例如一个64位存储器,可能由4个16位存储器组成,
低位交叉编址 xxxx00,xxxx01,xxxx10,xxxx11,
低位交叉编程连续的16位数据分别位于不同的存储体,可实现并行读取,近似认为是一个存取周期,
-- 64位的环境下 int a = 1; long b = 2L; short c = 3; long d = 4;
没有边界对齐时,读取b,d需要两个存取周期
有边界对齐时,读取b,d需要一个存取周期,但是会产生很多padding(无用的字节填充),浪费内存,是一种以空间换取时间的策略。
Java语言环境下,有了顺序分配优化后,避免了大量的padding,但是在对象的最后,还是会在最后产生padding的
HotSpot虚拟机要求对象其实地址必须是8字节的整数倍,
- 对象头刚好是8字节的整数倍
- 若实例数据不是8字节的整数倍,就会产生padding
三、对象的访问定位
对象创建成功后,便是对象的使用了。
Java程序会通过栈上的reference数据来操作堆上的具体对象,reference数据保存的可能是句柄,也可能是对象的直接地址指针。
保存句柄,Java堆中将可能划分出一块内存来作为句柄池,reference中保存的是句柄的地址,然后通过句柄包含对象实例数据地址信息与对象类型数据的地址信息
保存直接地址指针,那么regerence中保存的是(对象实例数据+对象类型地址指针)的地址指针,可通过对象地址类型指针找到地址类型信息
HotSpot虚拟机采用的是保存直接地址指针。不需要一次额外的间接访问。