jvm对象结构
java 虚拟机对象
我们写代码的时候经常会new 一个对象,但是我们new一个对象的时候,就行在我们的内存中发生了什么,我们new出来的对象究竟是个什么样子的?
对象的创建
当我们的虚拟机检测到new指令的时候,我们的虚拟机会拿着这个指令的参数去常量池中定位一个类的符号的引用,然后检查这个符号代表的类有没有被加载、解析和初始化过,如果没有,就执行一遍这个过程。
检查类加载后,虚拟机就开始分配内存了。对象所需要的内存是在类加载完成后就已经完全确定了。为java分配内存空间的方式有两种:指针碰撞和空闲列表。
指针碰撞:我们的堆内存都是规整的,空闲内存放在一边,使用过的内存放在另外一边,中间放着一个指针作为分界线,然后我们分配内存的时候把指针向空闲内存我们所需要的大小就可以了。指针碰撞的方法可以减少内存的浪费,当时我们操作内存的时候,需要一直维护保证我们的内存的规整性。
空闲列表:假设我们的内存不是规整的,空闲内存块和已经使用的内存块掺杂在一起,那么这时候我们就需要维护一张表。来记录都有哪些内存块是空闲的,并记录下来他们的大小。我们分配内存的时候,在空闲列表找合适的内存块即可。空闲列表不需要 时刻维护我们的内存的规整性,但是由于我们的空闲列表的内存块不一定正好有合适大小的内存块,所有,一定程度上会造成内存的浪费。但是我们可以定期或者遇到无法分配内存的时候,进行一次内存整理(参考to Survivor区,from Survivor作用 )。
使用哪种分配策略由内存是否规整来决定,而内存是否规整由垃圾收集器是否带有压缩整理功能来决定,所以最终使用哪种分配策略是由我们的垃圾收集器来决定的。目前比较通用的CMS垃圾收集器,采用的空闲列表法
内存分配的并发问题:对象的创建是一个非常频繁的过程,可能我们的一块内存正在分配给一个对象还没完成,这块内存又被拿去分配别的对象了。虚拟机采用CAS失败重试的机制保证操作的原子性。另外一种方法是使用本地线程分配缓冲(TLAB),即为每个线程在java堆中实现分配一小块内存,线程在自己的缓冲内存中操作。可以通过-xx:+/-UTLAB参数来设定
对象的设置:内存分配完成以后,虚拟机要将分配到的内存空间都初始化为0值。保证了对象在代码中可以创建出来不赋初始值便可以直接使用;接下来,虚拟机会对对象进行设置,其中包括对象头、实例数据、对齐填充:
对象头
-
对象是哪个类的实例,如何才能找到元数据的信息
-
对象的哈希码
-
对象的GC分代年龄
-
锁状态标志
-
线程持有的锁
-
偏向线程id
-
偏向时间戳
-
如果对象是数组,还有记录数组的长度
实例数据:
对象真正储存的有效信息,无论是父类继承的还是子类定义的,都需要记录下来
对齐填充
对齐填充并不是必然的,也没有也别的含义,仅仅是起着占位符的作用。HotSpot 虚拟机自动内存管理要求对象的起始地址必须是8字节的整数倍。换句话说,对象的大小必须是8字节的整数倍。而对象头必须是8字节的整数倍,因此,对象实例数据没有对齐的时候,必须通过对齐填充使得对象的大小必须是8的整数倍
完成对象头的设置之后,就开始调用init方法,为对象的字段赋值,然后一个真正可用的对象就诞生了
对象访问定位
上面我们说过,我们的虚拟机栈局部变量表中储存了对象的引用,这个引用可能是一个对象的内存地址,也有可能是对象的句柄的地址。
直接访问 :即我们储存的对象引用直接存的是对象内存地址,直接访问即可访问到对象。它最大的好处是直接访问速度更快。但是我们在垃圾回收的时候,有时候会堆内存进行压缩整理,导致我们的对象对应内存会被复制一定,这个时候我们又必须去虚拟机栈里更新的我们对应的对象引用;并且我们访问类型信息时候必须先访问到内存地址,在通过对象头的类型地址信息去访问类型信息
句柄:如果使用句柄访问,虚拟机会在堆中划分出来一块内存作为句柄池,用来存储句柄信息,句柄包括了对象实例内存地址和类型数据地址信息,我们的虚拟机栈中的局部变量表存的就是这个句柄的地址,我们访问对象时候通过地址访问到对应句柄,在通过句柄信息获取对象地址信息和类型地址信息。这样做的好处就是,内存地址改变时候,我们只需要去更新对应的句柄就可以了
浙公网安备 33010602011771号