目录
对象的创建
1、当虚拟机遇到一条New指令时:会进行如下步骤
- 检查指令的参数(即工作中我们New的对象),能否在常量池中找到它的符号引用。
- 如果存在,检查符号引用代表的类是否被加载、解析、初始化过。如果没有,执行相应的类加载(《Jvm类的加载机制》)。
- 加载通过后,虚拟机将为新生对象分配内存(所需内存大小在类加载完成后便可确定)。在堆的空闲内存中划分一块区域(指针碰撞-内存规整、空闲列表-内存交错 的分配方式)。
- 每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。
- 内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
- 执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。
2、两种内存分配的方式:
指针碰撞:假设Java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边。中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针往空闲空间那边挪动一段与对象大小相等的距离。这种方式则属于指针碰撞。
空闲列表:如果堆中的内存并不是规整的,已使用的内存和空闲内存相互交错,显然无法使用指针碰撞。虚拟机就必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新记录表上的数据。这种方式属于空闲列表。
具体选择哪种分配方式由Java堆决定,而Java堆是否规整,则由GC收集器决定。因此使用Serial、ParNew 等带 Compact 过程的收集器时,系统采用的分配算法是指针碰撞。而使用 CMS 这种基于 Mark-Sweep 算法的收集器时,通常采用的空闲列表。
3、如何保证分配内存时线程的安全性
- 对分配内存的动作进行同步处理(实际上虚拟机采用 CAS 配上失败重试的机制保证了更新操作的原子性)
- 把分配内存的动作按照线程划分在不同的空间之中进行(即每个线程在 Java堆 中预先分配一小块内存(称为本地线程分配缓冲))。
对象的内存布局
在 HotSpot 虚拟机中,分为 3 块区域:
对象头(Header)
、实例数据(Instance Data)
和对齐填充(Padding)
1、对象头
- 存储对象自身的运行时数据
如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 Mark Word
- 类型指针
即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。
2、实例数据
实例数据才是对象真正存贮的有效信息(即程序中所定义的各种类型的字段内容,包含父类继承下来的和子类中定义的)。
3、对齐填充
不是必然存在的,仅仅起到占位符的作用,保证对象大小是某个字节的整数倍。
对象的访问定位
创建对象就是为了在程序中使用,我们的Java程序需要通过栈上的 reference 数据来操作堆上的具体对象。Java 堆中会分配一块内存作为句柄池。
1、句柄访问
reference 存储的是句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
优点:reference 中存储句柄地址是稳定的。在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
2、指针访问
reference中存储的直接就是对象地址。
优点:速度快,节省了指针定位的时间成本。