不同的虚拟机中对象的具体分布是不同的,目前Hotspot虚拟机使用的较为广泛,以该虚拟机为例,撰写一下Java堆中对象分配、布局和访问的全过程
Java对象的创建
首先需要提的是,Java的对象创建只能通过new指令
Java作为一种“安全”的语言,检查是家常便饭,new操作就存在了检查
new指令的过程:
类加载检查
- 检查该指令的参数能否在常量池中定位到
- 检查该类是否已经被加载
- 若为加载,则执行相应的类加载过程
虚拟机为新生对象分配内存
在这里开始描述前,有必要提前说明:对象在内存中占用的空间的确定的
逻辑很简单,从堆中分配内存。
但是因为Java垃圾回收、高并发的原因,存在以下几个问题:
划分内存的方法
-
堆中内存是规整的,“指针碰撞”
使用过的内存放一边,未使用过的放在另一侧,中间使用一个指针作为分界点指示。
这种情况只需要移动指针即可,称为“指针碰撞”
-
堆中内存不规整,“空闲列表”
使用过的内存与空闲内存相互交错,虚拟机需要维护一个记录表,记录可用的内存块,分配时寻找合适的内存块进行分配,称为“空闲列表”
分配空间时,由于多线程操作导致的问题
假设有三个线程,同时向虚拟机申请内存,若一起处理,势必造成数据错乱
虚拟机有两个方案来应对这个情况:
-
对分配空间进行同步处理。
虚拟机采用了CAS配合失败重试的方式保证更新操作原子性
CAS:
Compare And Swap ,一种无锁的方式处理并发,给数据标记,每次操作进行加1,结束后核实该标记是否被改动
-
将内存划分为小块进行分配。
每个线程预先在Java堆中分配一小块内存,称为本地线程分配缓冲
对象初始化
所有空间都初始化为零值(不包括对象头)
零值:引用统一为null,布尔为false,其他均为0
对象头进行必要的设置
设置该对象为哪个对象的实例、寻找类的元数据的方式、对象哈希码、对象的GC分代年龄信息等。
执行初始化程序
方法执行,根据用户编码进行初始化。
总结如下
Java对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块
对象头(Header),实例数据(Instance Data),对齐填充(Padding)
对象头
对象头包括两部分:自身运行时数据与类型指针
-
自身运行时数据
哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
存储在32bit或64bit中
称为“Mark Word”
非固定的数据结构,以便于高效存储尽可能多的信息,根据对象状态复用自己的空间。
-
类型指针
对象指向它的类元数据的指针,通过该指针,可以得知对象的类型
-
若为数组,则对象头中还有一块用于记录数组长度的数据
实例数据
程序代码中所定义的各种类型的字段内容。父类继承与子类定义都被记录。
存储顺序受虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
对齐填充
无特别意义,保证对象大小为8字节的整数倍
对象的访问定位
Java通过reference类型代表对象的引用。
Java虚拟机未规定该引用怎样定位对象的具体位置
Reference有两种主流方式寻找对象具体位置:句柄和直接指针
句柄访问
Java堆中划出一块内存作为句柄池,reference中存储对象的句柄地址,句柄中包含对象的实例数据与类型数据各自的具体地址信息。
句柄
在程序设计中,句柄(handle)是Windows操作系统用来标识被应用程序所创建或使用的对象的整数。其本质相当于带有引用计数的智能指针。当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,可以使用句柄。
句柄与普通指针的区别在于,指针包含的是引用对象的内存地址,而句柄则是由系统所管理的引用标识,该标识可以被系统重新定位到一个内存地址上。这种间接访问对象的模式增强了系统对引用对象的控制。(参见封装)。通俗的说就是我们调用句柄就是调用句柄所提供的服务,即句柄已经把它能做的操作都设定好了,我们只能在句柄所提供的操作范围内进行操作,但是普通指针的操作却多种多样,不受限制。
直接指针访问
Java堆对象的布局中放置类型数据的相关地址信息,reference中存储的就是对象地址
两种方式的比较
- 句柄访问:reference中存储的是句柄地址,对象被移动(垃圾回收为了整理空间,会普遍发生移动)时,改变句柄中实例数据指针就行。
- 直接指针:直接指针速度更快,节省了一次指针定位的开销,HotSpot采用了直接指针。
有用点个赞吧,欧尼该!