HotSpot虚拟对象
对象的创建
创建对象通常仅仅是一个new关键字而已,而在虚拟机中,对象(仅限普通的对象,不包含数组和Class对象等)的创建是怎么一个过程呢?
当Java虚拟机遇到一条字节码new指令时。
首先,将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用。并且检查这个符号引用代表的类是否已被加载,解析和初始化过。若没有,那必须先执行相应类的加载过程。
类加载检查通过后,接下来虚拟机将为新生对象分配内存。即等同于把一块确定大小的内存块从Java堆中划分出来。那么为新生对象分配内存的方式有哪几种?
- 假设Java堆内存是规整的,使用过的内存放在一边,空闲的内存被放到另一边。以中间的指针为分界点。那分配的内存就仅仅为指针向空闲空间方向挪动一段 与对象大小相等的内存距离。这种分配方法称为"指针碰撞"。
- 如果Java堆内存是不规则的。即已使用的内存和空闲的内存相互交错在一起了。
- 这时,虚拟机必须维护一个列表,记录上哪些内存块是可用的。
- 在分配时,从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
这种分配方式为"空闲列表" (Free List)。
得出的结论是:
选择哪种分配方式是由Java堆是否规整决定的。而Java堆是否规整由使用的垃圾收集器是否带有空间压缩整理的能力决定的。因此,当使用Serial,ParNew等带压缩整理过程的垃圾收集器时,系统采用的分配算法为指针碰撞,即简单又高效;
而使用CMS基于Sweep的收集器时,理论上只能使用复杂的空闲列表来分配内存。
创建对象的另一个要考虑的问题
对象创建在虚拟机中是一种非常频繁的行为。即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的。
解决这种问题有两种可选方案:
- 对分配内存空间的动作进行同步处理-实际是虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
- 将内存分配的动作 按照线程 划分在不同空间之中进行。即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲 TLAB。线程分配内存优先到本地缓冲区TLAB,本地缓冲区用完了。分配新的
缓冲区是才需要同步锁定。 参数**-XX:+/-UseTLAB参数决定是否使用TLAB**。
对象的内存布局
在HotSpot虚拟机里,对象在堆内存的存储布局可以划分为三个部分:对象头(Header),实例数据(Instance Data),和对齐填充(Padding)。
其中对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据。如哈希吗HashCode,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID等。这类的数据长度在32位和64位的虚拟机中分别为32比特和64个比特。称它为"Mark Word"。Mark Word被设计为一种动态定义的数据结构,根据对象的状态复用自己的存储空间。这样,可以在极小的空间内存存储尽量多的数据。
例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,其中25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特存储锁标志位,1个比特固定为0。在其他状态下,对象的存储内容如下。
存储内容 | 标志位 | 状态 |
---|---|---|
对象的哈希码,对象分代年龄 | 01 | 未被锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀 重量级锁定 |
空 | 11 | GC标记 |
偏向线程ID,偏向时间戳,对象分代年龄 | 01 | 可偏向 |
对象的访问定位
Java程序中会通过栈上的reference数据来操作堆上的具体对象。在《Java虚拟规范》只规定它只是一个指向对象的引用。没有定义这个引用具体通过什么方式去定位,访问到堆中的对象的具体定位,所以对象访问方式由虚拟机实现而定。主流使用的是句柄和直接指针。
- 使用句柄访问的话,Java堆会划分一块内存作为句柄池,reference中存储的就是对象的句柄地址。
这个句柄地址包含对象实例和类型数据各自具体的地址信息。
- 若使用直接指针的话,Java堆中对象的内存布局就要考虑如何放置访问类型数据的相关信息。
reference中存储直接就是对象地址。
句柄访问方式的最大好处是reference中存储的是稳定句柄地址。在对象被移动时只需改变句柄中的实例数据指针。而reference本身不需要被修改。
直接指针的好处就是速度快,它节省了一次指针定位的时间开销。但就对象访问在Java中非常频繁,这样积少成多的开销也是极为可观的执行成本。在HotSpot虚拟机中,主要使用第二种方式访问对象。