本文主要讲解了JVM对象分配原理
1. 分配方式
1)指针碰撞:分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离(用处较少,多用于复制整理算法的垃圾回收器)。
2) 空闲列表:虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录(一般用于标记清除的垃圾回收器)。
2. 分配安全
对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
解决方案:
1)对分配内存空间的动作进行同步处理。
2)采用乐观锁(CAS)失败重试保障分配原子性。
3)把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local AllocationBuffer,TLAB),只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
3. 对象头解析
对象头包含两种属性:MarkWord 和类型指针。
3.1 mark word
这里以32位虚拟机为准,64位同理。
| 锁状态 | 25bit | 4bit | 1bit | 2bit | |
| 23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
| 无锁 | 对象的HashCode(首次调用) | 分代年龄 | 0 | 01 | |
| 偏向锁 | 线程ID | Epoch | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向线程栈中 Lock Record 的指针 | 00 | |||
| 重量级锁 | 指向监视器(monitor)的指针 | 10 | |||
| GC标记 | 空 | 11 | |||
3.2 类型指针
对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
4. 对象访问定位
对象访问分为句柄和直接指针两种。
句柄:Java 堆中将可能会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。使用句柄来访问的最大好处就是 reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。
直接指针:Java 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问
的开销。使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在 Java 中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。