一 对象在内存中的布局
1.1对象的创建过程
对象的创建过程可以如下图所示:
1.2 什么是符号引用和直接引用,为什么需要在常量池定位到符号的引号?
在类的解析阶段,把虚拟机常量池内的符号引号替换为直接引用。
1.2.1 符号引用(SymbolicReferences)
就是用一组符号来描述所引用的目标,符号可以是任何形式的,只要使用时能够定位到目标即可。
我们知道Java类编译成class文件,编译的时候,该类并不知道其所引用类的实际内存地址,因此只能使用符号引用来代替。
1.2.2 直接引用
直接饮用就是直接指向目标的指针,比如指向对象,或者类方法,类变量的指针,亦或者指向实例变量和实例方法的指针
二 对象内存分配方式
2.1 指针碰撞
指针碰撞就是指需要分配内存的时候,只需要将指针向空闲区域移动即可。所以堆内存空间空闲内存需要十分规整才可以。
2.2 空闲列表
很多时候,正在使用的内存和未被使用的内存十分不规整,分布很零散。虚拟机维护了一个空闲列表,记录哪些内存块可用,每次要分配内存时候就去空闲啊列表查询,找到一个足够的内存块去分配。
我们比较这两者分配方式,很明显指针碰撞的分配效率高于空闲列表,但是前提是空闲内存规整。如果不规整的话,空闲列表是比较适合的,比指针碰撞节约内存。
三 内存分配线程安全问题
假设我们线程A请求分配内存,虚拟机从空闲列表查询取出了一块内存,分配给线程A的对象,这时候还没来得及更新空闲列表,线程B也请求分配内存,也把这块内存分配给了线程B,这就存在内存分配线程安全问题。
怎么解决呢?
第一种方式:加锁(影响性能)
第二种方式:TLAB (ThreadLocal Allocate Buffer)本地线程分配缓冲区
四 对象结构
# Header: 主要包括自身运行时数据;类型指针
# InstanceData
# Padding
4.1 对象头(header)
对象头用于存储一些元数据,比如自身运行时数据和类型指针。
自身运行时数据(Mark Word)
# 哈希值
# GC分代年龄
# 锁状态标志
# 线程持有的锁
# 偏向线程ID和时间戳
它占用内存大小和操作系统有关,如果是32位的操作系统,那么它就是32位;如果是64位的操作系统,那么他就是64位。如果数据远远大于32位,是如何进行存储的呢?
类型指针
指向对象指向其类的元数据的指针,虚拟机通过指针来确定对象是哪一个类的实例。
4.2 实例数据(InstanceData)
4.3 对齐填充(Padding)
为什么需要填充?
对齐填充并不是必然的,也没什么特殊的含义,仅仅起着占位符的作用。HotSpot虚拟机内存管理规定对象起始地址必须是8字节的整数倍,比如header部分正好都是8的整数倍,如果实例数据部分大小不够8的整数倍,即没有对齐,就需要使用Padding填充
五 对象的访问定位
栈中的reference引用指向堆内存的地址并不一定就是对象本身。
5.1 使用句柄
并不是直接指向,而是指向堆中一块区域,叫做句柄池。句柄池里保存了实例对象的地址。
优点:栈中reference的引用地址不需要改变,改变的只是句柄池的对象实例地址
5.2 直接指针
直接指向堆中对象内存区域,HotSpot就是使用这种