一、对象的访问定位
堆中存对象,栈上存引用,JVM中有两种对象访问定位方式:
1、通过直接引用访问
如图,直接引用方式是局部变量存储是对象实例的地址,同时,对象所属的类也可以通过对象实例获取。
2、通过句柄池访问
如图,句柄访问方式是在堆中开辟了一块句柄池,句柄池存储了对象的引用和所属类的引用,而局部变量表存储了句柄池的地址。
- 对比:
句柄方式,当对象位置发改变时(如GC时,对象复制),不需要改变局部变量的引用。访问对象的类型数据时,可以直接获取,而不用获取对象实例之后再访问。
直接引用,可以直接通过引用获取到对象实例,速度更快,HotSpot虚拟机采用该方式。
二、对象的内存布局
对象在堆中的内存布局主要分为三个部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
1、对象头
- Mark Word: 存储对象自身运行时的数据,主要内容如下表
- 类型指针:指向对象所属类的类型数据。
- 数组长度:档案实例是一个Java数组时,对象头中用于记录数组长度的区域。
2、实例数据
对象存储的真正有效信息,各种字段内容,包括从父类继承的。
3、对象填充
没有实际意义,相当于占位符,如果对象实例所占空间不是8字节的整数倍,就通过对齐填充补充到8的整数倍(HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8的整数倍)
三、堆中的线程私有区域
堆中的区域并不是都是线程共享,为提高对象分配的效率,线程在堆中划分出了线程私有的分配缓冲区(Thread Local Allocation Buffer,简称TLAB)。默认情况下,TLAB仅占Eden区的1%,可以通过-XX:+UseTLAB启用TLAB,也可以通过-XX:TLABSize调节TLAB的大小。
- TLAB的优势:
- 提高分配效率,堆区作为共享区域,Eden区对象创建频繁,给对象分配内存时,为保证线程安全,避免多线程操作同一地址,必然需要一系统加锁机制,进而影响分配速度。TLAB是对象分配的首选区域,在TLAB中给对象分配内存失败时,才会在Eden区分配。
四、对象创建过程
对象创建过程:
1、当JVM执行到一条new指令时(如new #2 <heap/HeapPartDemo>
),就会去根据符号引用(#2)检测运行时常量池中这个符号引用指向的类是否已经完成加载、解析和初始化。如果没有就会进行该类的类加载过程。
2、 当检测到类已加载,就会开始分配内存,对象的大小在类加载阶段就已确定,在Java堆中为对象分配内存有指针碰撞和空闲列表两种方式,对象根据条件不一定分配在Eden区,这个栈上分配时再写;
- 指针碰撞(Bump The Pointer):如果Java堆中内存是规整的,已用内存区域和空闲内存区域用一个指针作为分界线,在分配内存给对象时,只要依据对象所需内存(对象大小在类加载时就会确定),将指针向空闲内存区域移动一段与对象大小相等距离即可,如Eden区内存区域是[0x00000000fd580000,0x00000000ff600000],,0x00000000fd969620是指针位置,分配时在,0x00000000fd969620基础上加上对象大小即可,0x00000000fd969620就是对象的起始地址,依据对象填充,这个地址换算过来应该是8的整数倍。
- 空间列表(Free List):如果Java堆中内存不规整,已用内存和空间内存交织在一起,就需要有一个表记录那些内存可用,这个表就被称为空闲列表,给对象分配内存就需要从空闲列表中找到一块足够的区域分配内存,同时更新表信息。
同样,对象分配内存也存在并发问题,对象分配内存线程问题的解决方式:
- 采用CAS配上失败重试的方式使得线程同步。
- 线程私有的分配缓冲区TLAB。
3、 完成内存分配之后,就会进行对象实例数据属性初始化,这一步保证了即使对象字段属性即使没有赋值,也能被使用,访问到对应的零值;
4、 实例数据初始化完成后,需要设置对象头,包括MarkWord、类型指针和数组长度;
5、 执行Class文件里