1-2 JAVA内存区域 - HotSpot虚拟机对象探秘

一、对象的创建

 

简述:java是一门面向对象的编程语言,在程序的运行过程当中无时无刻都有对象被创建出来,在语言层面上,一个对象被创建只需要一个new关键字即可,那么接下来我们要说的是在虚拟机当中一个对象的创建都需要经历哪些步骤。

 

1、检测类是否加载:首先当虚拟机遇到一个new指令时,会根据指令的参数检查是否能在常量池中定位到一个类的符号引用,并检查这个符号引用所对应的类是否被虚拟机加载、解析和初始化过,如果没有则需要重新执行类的加载过程。

 

2、分配内存空间:当检测通过后,虚拟机需要为新生的对象获取内存,当类的加载过程运行完后,对象所占用的内存空间就已经确定了,虚拟机只需要从java堆中划分出对应大小的内存即可。

 

3、空间分配方式:内存的获取分为两种方式,第一种叫指针碰撞,第二种叫空闲列表;假设java堆内存是一块规整并且连续的内存空间,并且已分配的内存和未分配的内存分储两侧,中间用一个指针来做标注,那么虚拟机只需要将指针向未分配内存的方向移动出与待分配空间对象大小一致的距离即可完成内存分配工作,这种模式叫做指针碰撞模式;假设java堆中的内存空间并不规整,可用内存与已分配内存相互交错存储,指针碰撞的模式就不足以完成空间分配的任务了,那么虚拟机就需要单独维护一张表,来存放可用空间的地址,当申请内存空间时虚拟机需要到表中查找一块足够大的内存空间分配给新生对象,这种内存分配模式叫空间列表模式。

 

4、空间分配并发处理:由于虚拟机创建对象是极度频繁的操作,那么在分配空间的过程中就非常容易发生线程安全的问题,比如A线程用指针来分配内存空间,还没保存,线程B就调用这个指针进行空间分配;解决问题的方式有两种,一种是对内存分配的操作进行同步处理--通过cas和失败重试的方式来解决更新操作的原子性问题;另一种是将内存分配操作按线程分配到不同的空间上进行,就是为每个线程在java堆中都划分出一小块内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer, TLAB),那个线程需要分配内存,就在哪个线程的TLAB上进行内存分配,只有当线程的TLAB需要扩展时,才会进行同步操作。虚拟机是否使用TLAB可以使用-XX:+/-UseTLAB的参数来设定。(CAS简述:实际为乐观锁的一种实现方式,存有三个值:内存位置V,预期原值A,新值B,对比内存位置V的值是否与原值A相等,相等则用新值B覆盖内存位置V,否则拿到内存位置V的值更改预期原值A,继续对比,直到成功)

 

5、空间初始化:当对象空间分配完成后,虚拟机会对分配的内存空间进行初始化为零操作(不包括对象头),TLAB模式下则在TLAB分配内存时进行,此操作保证程序中对象创建后即便不赋初值也能够正常使用。

 

6、初始化对象:在对象头(Object_Header)中设置该对象是哪个类的实例、如何找到对象的元数据信息、对象的哈希码、对象的GC分代年龄等信息,根据虚拟机的运行状态不同,如是否启用偏向锁等,对象头会有不同的设置方式。(偏向锁:在线程无竞争的情况下(只有一个线程A工作),为线程A添加偏向锁可以消除同步原语,能够有效提高程序运行效率,当有其他线程抢占锁,虚拟机会将线程A的偏向锁去除,转换为其他轻量级锁)

 

总结:到此为止,站在虚拟机的层面上一个对象已经被创建成功,但在java程序的角度,对象的创建才刚刚开始,对象的所有字段为零,一般来说,执行new指令后会接着执行<init>方法,把对象按照程序员的意思来进行初始化,这样一个真正的对象才算完全生产出来。

 

二、对象的内存布局

 

 

简述:对象在内存中的存储分为三个区域,分别为:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

 

1、对象头(Header)主要存储两部分内容,自身运行时数据(Mark_Word)和类型指针。

 

自身运行时数据主要包括哈希码(Hash_Code)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

为了充分利用虚拟机的空间,Mark_Word被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间,下面介绍对象的不同状态下自身运行时数据分别存储什么。

 

未锁定状态:对象哈希码、对象分代年龄、锁标志位

轻量级锁定:指向锁记录的指针

膨胀(重量级锁定):指向重量级锁的指针

GC标记:空,不需要记录信息

可偏向:偏向线程ID、偏向时间戳、对象分代年龄

 

另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(并不是所有虚拟机实现都必须在对象数据上保留类型指针)

 

2、实例数据(Instance_Data)存储的是对象的有效信息,也就是在程序中定义的各种类型的字段内容,包括从父类继承下来的和子类中定义的。这些记录的顺序受虚拟机分配策略参数和java源码中字段定义的顺序影响。HotSpot虚拟机默认的分配策略为:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary_Object_Pointers),我们能发现在分配策略中,相同宽度的字段总是被分配在一起,在满足这个前提的情况下,父类的字段总是排在子类字段的前面,并且较窄的子类字段会被插入到父类变量的空隙中。

 

3、对齐填充(Padding)这部分内容没有特别的意义,虚拟机规定对象的存储空间为8字节的整数倍,当对象实例部分没有对齐时,就需要对齐填充来补全。

 

三、对象的访问定位

 

简述:对象被创建出来的目的就是为了程序的使用,java程序是通过虚拟机栈局部变量表中对象引用(reference)来操作java堆中的对象的,由于reference中只存储了对象的一个引用,没有规定如何定位和访问堆中的对象数据和方法区中类型数据,这部分取决于虚拟机的具体实现,目前主流的虚拟机实现有使用句柄和直接指针两种方式。

 

1、句柄,虚拟机需要在堆中单独划分出一部分内存来作为句柄池,句柄池中存放对象实例数据和类型数据的地址信息。采用句柄的方式最大的好处是reference中存储的是稳定的句柄地址,当对象被移动时(垃圾收集时移动对象是非常普遍的行为),不需要更改reference中的地址,只需要修改句柄中的地址就可以。缺点是reference需要先定位句柄位置,再通过句柄去定位对象实例数据和类型数据的位置。

 

2、直接指针,reference中直接存放对象实例数据的地址信息,想要定位和访问类型数据就需要在java堆对象的布局中考虑存储访问类型数据的相关信息。直接指针的最大优点是速度更快,它节省了一次定位句柄池的操作,要知道在程序运行的过程中,对象定位和访问操作会非常频繁,这部分的开销积累下来会节省非常多的执行成本。

 

未完待续……

 

感谢阅读:

由于个人水平有限,如果有不对的地方希望各位能够留言指正,谢谢!

参考书籍:

《深入理解java虚拟机》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值