文章目录
- Java对象的内存分配
- 对象的创建流程
- 类加载检查
- 对象内存分配
- 对象的内存布局
- 对象头区域
- 实例数据区域
- 填充对齐区域
- 对象的访问定位方式
- 句柄访问
- 直接指针访问
Java对象的内存分配
对象的创建流程
- 虚拟机收到new指令触发。
- 类加载检查:会判断类是否已经被加载,如果没有被加载则需要先执行类加载流程,对象所需内存大小在类加载完后可以完全确定。
- 为对象分配内存,从堆中划分出一块确定大小的内存。
- 内存分配完后,虚拟机需要将分配到的内存空间初始化为零(不包含对象头),保证了对象的实例字段在不赋初始值时也能直接使用。
- 为对象进行必要的设置:设置这个对象属于哪个类的实例、类的元数据、对象的哈希码、对象的GC分代年龄等信息,并存放在对象头中。
- 一个新的对象创建完毕。
类加载检查
- 检查该指令单参数是否能在常量池中定位到一个类的符号引用;
- 检查这个符号引用代表的类是否已经被加载、解析、初始化,如果没有,需要先执行类加载流程。
对象内存分配
堆内存是否规整:
- 规整:已使用的内存在一边,未使用内存在另一边。
- 不规整:已使用内存和未使用相互交错。
堆内存是否规整是由垃圾收集器是否带有压缩整理功能决定的。
内存分配会根据堆内存是否绝对规整,分两种分配方式:
- 指针碰撞。
- 空闲列表。
分配过程:
- 如果堆内存绝对规整,采用指针碰撞方式。分配过程:将已使用内存和为使用内存之间放一个分界点的指针,分配内存时,指针会向未使用内存方向移动,移动一段与对象大小相等的距离。
- 如果堆内存不绝对规整,采用空闲列表方法,分配过程:虚拟机内部维护了一个记录可用内存块的列表,在分配时从列表找一块足够大的空间划分给对象实例,并更新列表上的记录。
给对象分配内存是线程不安全的。
对象的内存布局
在Java虚拟机(HotSpot)中,对象在堆内存中的可分三块:
- 对象头区域
- 实例数据区域
- 对齐填充区域
对象头区域
存储的信息包含两部分:
- 对象自身的运行时数据(Mark Word):哈希码、GC分代年龄、锁状态标志等。
- 对象类型指针:即对象指向它的类元数据的指针,虚拟机通过指针来确定对象属于哪个类的实例。
如果对象是数组,那么在对象头中必须还有一块用于记录数组长度的数据。
实例数据区域
真正存放对象实例数据的地方。
填充对齐区域
这部分不一定存在,没有特别含义,仅仅是占位符。这是因为HotSpot要求对象起始地址都是8字节的整数倍,如果不是则填充对齐。
对象的访问定位方式
创建对象后,Java程序通过栈上的引用类型(reference)访问堆中的对象实例。
由于引用类型数据(reference)在虚拟机中只规定了一个指向对象的引用,但没有规定采用何种方式区定位访问堆中的对象的具体位置,所以对象的访问方式具体由虚拟机实现。
目前大部分JVM虚拟机,包括HotSpot虚拟机采用的是直接指针访问。
目前对象访问方式有两种:
- 句柄访问
- 直接指针访问
句柄访问
在Java堆中划分出一块内存作为句柄池,虚拟机栈中reference数据中存储的是对象的句柄地址,程序通过访问栈中的reference数据获取对象的句柄地址,句柄中的每条记录包含对象实例数据的指针和对象类型数据的指针,再通过句柄定位和访问对象。
优点:
- reference存储稳定的句柄地址,对象移动(垃圾收集)时只会改变句柄中实例数据指针,reference不需要做改变。
缺点:
- 增加了指针定位的开销
场景:
- 适合频繁移动对象地址
直接指针访问
栈中reference数据存储的是对象地址,放在对象头中,程序通过访问栈中reference数据获取对象的地址,直接访问对象实例数据,再通过对象类型数据的指针访问对象类型数据。
优点:
- 速度快,节省了一次指针定位的开销。
缺点:
- 对象移动时需要重新定位。
场景:
- 适合频繁访问对象。