对象的内存布局
1、对象的实例化
1.1、创建对象的几种方式
- new是最常见的方式
- XXX的静态方法
- XXBuilder/XXXFactory的静态方法
- Class的newInstance():通过反射的方式,只能调用空参的构造器,权限必须是public。
- Constructor的newInstance(Xxx):反射的方式,可以调用空参、带参的构造器,权限没有要求,实用性更广。
- 使用clone():不调用任何构造器,当前类需要实现Cloneable接口,实现clone(),默认浅拷贝。
- 使用反序列化:从文件中、数据库中、网络中获取一个对象的二进制流,反序列化为内存中的对象
- 第三方库Objenesis,利用了asm字节码技术,动态生成Constructor对象
从执行步骤角度分析:
-
先判断对象对应的类是否加载、链接、初始化。
虚拟机遇到一条new指令,首先去检查这个指令的参数能否在元空间的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、链接和初始化了(就是检查类的元信息是否存在)
- 如果元信息没有找到,那在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的.class文件。
- 如果没有找到文件,则抛出ClassNotFoundException异常。
- 如果找到了,则进行类加载,并生成对应的Class类对象。
-
为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。
-
如果内存规整,使用指针碰撞法来为对象分配内存:
如果内存是规整的,所有使用过的内存在一边,没有使用的内存在另一边,中间就放一个指针作为分界线,给对象分配内存其实就是把指针向空闲的那一边挪动一段对象大小相等的距离。
-
如果内存不规整,零零碎碎的,虚拟机就需要维护一个空闲列表,使用空闲列表分配。
已使用的内存和未使用的内存相互交错存在,那么虚拟机将使用空闲列表法来为对象分配内存。这个空闲列表记录了哪些内存是空闲的,在分配的时候从列表中找到这一块足够大的空间划分给对象示例,并更新列表上的内容。
-
-
处理并发安全问题
在给对象分配内存时,需要保证new对象时候的线程安全性问题。
- CAS:失败重试、区域加锁:保证指针更新操作的原子性;
- TLAB:优先在线程专属的一小块堆内存空间中创建对象,其他线程不能在线程私有堆空间创建对象,只能有访问权限。
-
初始化分配到的空间
内存分配结束,虚拟机将分配到的内存空间都初始化为零值。保证了对象的实例字段在Java代码中可以不用赋初始值就能直接使用,程序能访问到这些字段的数据对应的初始零值。
-
**设置对象的对象头:**将对象的所属类、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头。
-
执行init方法进行初始化:从Java程序的视角来看,初始化才正式开始。初始化成员变量。执行实例化代码、调用类的构造方法,并把堆对象的首地址赋值给引用变量。
2、对象的内存布局
对象的内存布局包括:对象头、实例数据、对齐填充
2.1、对象头(Header)
对象头包含两个部分
-
一个是对象自身的运行时元数据(mark word)包含下面几个内容:
- 哈希值:对象在堆空间中都有一个首地址值,栈空间的引用根据这个地址指向堆中的对象,这就是哈希值的作用。
- GC分代年龄:对象是在Eden中创建的,在经历多次GC后,如果还没有被回收,就会在幸存者区来回移动,这个GC分代年龄就会GC一次增加一次,到达15次就得去老年代了。
- 锁状态标志,在同步中判断该对象是否是锁
- 线程持有的锁
- 线程偏向ID
- 偏向时间
-
另一个就是类型指针,指向元数据区的类元数据InstanceKlass,确定该对象所属的类型
-
此外,如果对象是一个数组,对象头中还必须有一块用于记录数组长度的数据
正常对象元数据就知道对象的确切大小,所以数组必须得知道长度。
2.2、实例数据(Instance Date)
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段,包括从父类继承下来的和本身拥有的字段。
需要遵顼的一些规则:
- 相同宽度的字段总是被分配在一起;
- 父类中定义的变量会出现在子类之前(加载子类之前要加载父类)
- 如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙
**对齐填充:不是必须的,也没特别含义,仅仅起到占位符的作用 **
3、对象的访问定位
创建对象的目的是为了使用它。定位,通过栈上reference访问。
JVM是如何通过栈帧中的对象引用访问到其内部的对象实例的呢?
《java虚拟机规范》没有说明,所以对象访问方式由虚拟机实现而定。主流有两种方式:
- 使用句柄访问
- 使用直接指针访问
3.1、句柄访问
- 实现:堆需要划分出一块内存来做句柄池,reference中存储对象的句柄池地址,句柄中包含对象实例与类型数据各自具体的地址信息。
- 好处:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针,reference本身不需要被修改。
3.2、直接使用指针访问
- 实现:reference中存储的就是对象的地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。
- 好处:速度更快,java中对象访问频繁,每次访问都节省了一次指针定位的时间开销。
HotSpot这里主要使用第2种方式:直接指针访问。
JVM可以通过对象引用准确定位到Java堆区中的instanceOopDesc对象,这样既可成功访问到对象的实例信息,当需要访问目标对象的具体类型时,JVM则会通过存储在instanceOopDesc中的元数据指针定位到存储在方法区中的instanceKlass对象上。