这是我参与更文挑战的第 6 天,活动详情查看: 更文挑战
对象在内存中的结构
1.对象的实例化
对象创建的方式:
- 构造器创建(new)
- 反射
- clone():不需要调用任何的构造器,需要当前类实现Cloneable接口和其中的clone()方法。(潜复制)
- 使用反序列化,从文件或网络获取对象的二进制流
- 第三方库
对象创建的步骤:
- 判断对象是否加载、链接、初始化:JVM判断这个类是否加载,如果没有会在双亲委派模式加载该类的字节码文件,并生成对应的对象。
- 为对象分配空间:计算对象大小,然后再堆空间分配一块内存给对象。
- 如果内存规整:使用指针碰撞法来为对象分配内存,就是已使用的内存和未使用的内存分开,中间用一个指针作为分界点,分配内存就是把指针向空闲内存那一边挪动一段对象大小相等的距离。
- 如果内存不规整:虚拟机需要维护一个列表,记录内存上哪些内存是可以用的,再分配的时候从列表找到一块足够的内存划分给对象实例,并且更新列表上的内容,这种方法也叫空闲列表法。
- 处理并发安全问题:
- 使用CAS机制,失败重试,区域加锁保证原子性。
- 每个线程都会分配一块TLAB。
- 初始化分配的空间:所有属性设置默认初始化。
- 设置对象头:类的元数据信息、HashCode、对象的GC信息和锁信息等数据存储在对象头中
- 执行init()方法进行初始化
2.对象的内存布局
- 对象头(两个部分):
- 运行时元数据:
- 哈希码(主要用于引用到当前对象的地址)
- GC分代年龄(年龄计数器)
- 锁状态标识
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针:指向元数据,确定该对象所属类型
- 如果是数组对象,还要记录数组长度
- 运行时元数据:
- 实例数据:
- 说明:存储对象真正有效信息、定义类型、字段等(包括从父类继承下来的)
- 规则:父类定义的变量会出现在子类之前;相同宽度(字节)的字段会分配到一起;默认情况子类的窄变量可能插入父类变量的空隙。
- 对其填充:不是必要存在的,起占位符的作用。
3.对象访问定位
JVM如何通过栈帧中的对象引用访问到其内部的对象的实例呢?
两种方式:
- 句柄访问:
- 说明:栈的本地变量表记录了对象引用(reference),在堆空间有一个句柄池,一个对象对应一个句柄,主要记录两个信息,一个指向对象实例的指针,还有个指向对象类型(方法区)数据的指针
- 缺点:缺点是需要在堆空间额外维护一个句柄池空间,并且需要在句柄中维护一个变量区访问实体,效率低。
- 优点:栈空间维护的对象地址比较稳定,如果堆空间中的对象发生移动,只需要修改句柄中的变量,栈中的引用不用修改。
- 直接指针(Hostpot):
- 说明和句柄访问不同的是,对象引用直接指向了对象实例,对象实例中有一个指向方法区对象类型数据的指针。
- 缺点:如果对象移动,那么需要修改栈空间的引用地址。
- 优点:相对于句柄访问的方式,直接访问效率更高,内存节省一些。