文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
堆对象
对象的创建
遇到一条New指令,虚拟机的步骤:
检查这个指令的参数能否在常量池中定位到一个类的符号引用
- 并检查这个符号引用代表的类是否已被加载、解析和初始化过
如果没有,必须先把这个类加载进内存
类加载检查通过后,虚拟机将为新对象分配内存,类加载完就可以确定存储这个对象所需的内存大小
将分配到的内存空间初始化为零值
设置对象头(Object Header)中的数据:
- 包括这个对象是哪个类的实例、如何才能找到类的元数据、对象的哈希码(实际在调用
Object::hashCode()
方法才计算)、对象的GC分代年龄等此时从虚拟机的角度看,对象已经产生,但从
Java
程序的角度看,构造函数还没有执行执行完初始化函数,一个真正的对象才算完全构造出来
在第二步中,为对象分配内存,就是在内存划分一块确定大小的空闲内存,但存在两个问题:
如何划分空闲内存和已被使用的内存?
假设Java堆中内存是绝对规整的,空闲内存和被使用内存被分到两边,中间放置指针作为分界点的指示器
- 那分配内存就是把指针向空闲内存一定一段,这种方式成为 指针碰撞(
Bump The Pointer
)但如果Java堆内存不是规整的,那就没有办法简单地进行指针碰撞了,虚拟机需要维护一个列表
- 记录哪些内存块可以使用,在分配内存的时候,找到一块足够打的内存划分给对象实例
- 并更新列表上的记录,这种方式被称为 空闲列表(Free List)
事实上,这由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定
因此,当使用Seria、parNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效
而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上只能采用较为复杂的空闲列表来分配内存
如何处理多线程下,内存分配问题?
- 对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
- 把内存分配的动作按照线程划分在不同的空间之中进行
- 即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(
Theard Local Allocation Buffer, TLAB
)- 哪个线程要分配内存就先在本地线程分配缓冲中分配,只有缓冲使用完了,分配新的缓存区时需要同步锁定
- 通过
-XX:+/-UseTLAB
参数设置是否使用TLAB
对象的内存布局
对象头(Header):
第一部分:存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- (官方称之为
Mark Word
)第二部分:类型指针,即对象指向它的类型元数据的指针,虚拟机通过这个指针来判断这个对象是哪个类的实例
- 如果是数组对象,将会在对象头存储数组长度,以确定对象大小
实例数据(Instance Data):
在程序代码里面所有定义的各种类型的字段内容都必须记录。
这部分的存储顺序受到虚拟机分配策略参数(
-XX:FieldsAllocationStyle
参数)和Java源码中定义顺序的影响HotSpot虚拟机默认的分配顺序为
longs/doubles、ints、shorts/chars、bytes/booleans、oops (Ordinary Object Pointers, OOPs)
- 相同宽度的字段总是被分配到一起存放,满足这个条件的前提下,父类定义的变量会出现在子类之前
对齐填充(Padding):
- HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍
- 因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全
对象的访问定位
Java程序通过栈上的reference数据来操作堆上具体对象,主流的访问方式主要有以下两种:
- 使用句柄:Java堆可能会划分一块内存作为句柄池,reference中存储的就是对象的句柄地址
- 而句柄包含了对象实例数据与类型数据各自Juin的地址信息
- 优势:在对象被移动(垃圾收集时移动对象是非常普遍的行为)时
- 只会改变句柄中的实例数据指针,而不需修改reference
使用直接指针:Java堆中对象的内存布局必须考虑如何放置访问类型数据的相关信息,
reference
中存储的直接就是对象地址
- 如果只是访问对象本身,就不需要多一次间接访问的开销
优势:速度快,节省一次指针定位的时间开销