共计 3415 个字符,预计需要花费 9 分钟才能阅读完成。
第 10 章 对象的实例化内存布局与拜访定位
1、对象的实例化
大厂面试题
美团:
- 对象在
JVM
中是怎么存储的? - 对象头信息外面有哪些货色?
蚂蚁金服:
二面:java
对象头里有什么
对象实例化
1.1、对象创立的形式
对象创立的形式
- new:最常见的形式、单例类中调用 getInstance 的动态类办法,XXXFactory 的静态方法
- Class 的 newInstance 办法:在 JDK9 外面被标记为过期的办法,因为只能调用空参结构器,并且权限必须为 public
- Constructor 的 newInstance(Xxxx):反射的形式,能够调用空参的,或者带参的结构器
- 应用 clone():不调用任何的结构器,要求以后的类须要实现 Cloneable 接口中的 clone 办法
- 应用序列化:序列化个别用于 Socket 的网络传输
- 第三方库 Objenesis
1.2、对象创立的步骤
从字节码对待对象的创立过程
- 代码
public class ObjectTest {public static void main(String[] args) {Object obj = new Object();
}
}
-
main() 办法对应的字节码(前面细讲):
- 调用 new 指令后后,加载 Object 类
- 调用 Object 类的 init() 办法
0 new #2 <java lang object>
3 dup
4 invokespecial #1 <java lang object.<init>>
7 astore_1
8 return
</java></java>
创建对象的步骤
1、判断对象对应的类是否加载、链接、初始化
- 虚拟机遇到一条 new 指令,首先去查看这个指令的参数是否在 Metaspace 的常量池中定位到一个类的符号援用,并且查看这个符号援用代表的类是否曾经被加载,解析和初始化。(即判断类元信息是否存在)。
- 如果该类没有加载,那么在双亲委派模式下,应用以后类加载器以 ClassLoader + 包名 + 类名为 key 进行查找对应的.class 文件,如果没有找到文件,则抛出 ClassNotFoundException 异样,如果找到,则进行类加载,并生成对应的 Class 对象。
2、为对象分配内存
- 首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是援用变量,仅调配援用变量空间即可,即 4 个字节大小
-
如果内存规整:采纳指针碰撞分配内存
- 如果内存是规整的,那么虚拟机将采纳的是指针碰撞法(Bump The Point)来为对象分配内存。
- 意思是所有用过的内存在一边,闲暇的内寄存另外一边,两头放着一个指针作为分界点的指示器,分配内存就仅仅是把指针往闲暇内存那边移动一段与对象大小相等的间隔罢了。
- 如果垃圾收集器抉择的是 Serial,ParNew 这种基于压缩算法的,虚拟机采纳这种调配形式。个别应用带 Compact(整顿)过程的收集器时,应用指针碰撞。
- 标记压缩(整顿)算法会整顿内存碎片,堆内存一存对象,另一边为闲暇区域
-
如果内存不规整
- 如果内存不是规整的,已应用的内存和未应用的内存互相交织,那么虚拟机将采纳的是闲暇列表来为对象分配内存。
- 意思是虚拟机保护了一个列表,记录上哪些内存块是可用的,再调配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种调配形式成为了 “ 闲暇列表(Free List)”
- 抉择哪种调配形式由 Java 堆是否规整所决定,而 Java 堆是否规整又由所采纳的垃圾收集器是否带有压缩整顿性能决定
- 标记革除算法清理过后的堆内存,就会存在很多内存碎片。
3、解决并发问题
- 采纳 CAS+ 失败重试保障更新的原子性
- 每个线程事后调配 TLAB – 通过设置 -XX:+UseTLAB 参数来设置(区域加锁机制)
- 在 Eden 区给每个线程调配一块区域
4、初始化调配到的内存
所有属性设置默认值,保障对象实例字段在不赋值能够间接应用
5、设置对象的对象头
将对象的所属类(即类的元数据信息)、对象的 HashCode 和对象的 GC 信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置形式取决于 JVM 实现。
6、执行 init 办法进行初始化
- 在 Java 程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给援用变量
- 因而一般来说(由字节码中追随 invokespecial 指令所决定),new 指令之后会接着就是执行 init 办法,把对象依照程序员的志愿进行初始化,这样一个真正可用的对象才算实现创立进去。
回顾给对象属性赋值的程序:
- 属性的默认值初始化
- 显示初始化 / 代码块初始化(并列关系,谁先谁后看代码编写的程序)
- 结构器初始化
从字节码角度看 init 办法
- 代码
public class Customer{
int id = 1001;
String name;
Account acct;
{name = "匿名客户";}
public Customer(){acct = new Account();
}
}
class Account{}
-
init() 办法的字节码指令:
- 属性的默认值初始化:
id = 1001;
- 显示初始化 / 代码块初始化:
name = "匿名客户";
- 结构器初始化:
acct = new Account();
- 属性的默认值初始化:
0 aload_0
1 invokespecial #1 <java lang object.<init>>
4 aload_0
5 sipush 1001
8 putfield #2 <com atguigu java customer.id>
11 aload_0
12 ldc #3 < 匿名客户 >
14 putfield #4 <com atguigu java customer.name>
17 aload_0
18 new #5 <com atguigu java account>
21 dup
22 invokespecial #6 <com atguigu java account.<init>>
25 putfield #7 <com atguigu java customer.acct>
28 return
</com></com></com></com></ 匿名客户 ></com></java>
2、对象的内存布局
对象内存布局
2.1、对象头
对象头
对象头蕴含两局部:运行时元数据(Mark Word)和类型指针
-
运行时元数据
- 哈希值(HashCode),能够看作是堆中对象的地址
- GC 分代年龄(年龄计数器)
- 锁状态标记
- 线程持有的锁
- 偏差线程 ID
- 偏差工夫戳
-
类型指针
- 指向类元数据 InstanceKlass,确定该对象所属的类型。指向的其实是办法区中寄存的类元信息
阐明:如果对象是数组,还须要记录数组的长度
2.2、实例数据
实例数据(Instance Data)
-
阐明
- 它是对象真正存储的无效信息,包含程序代码中定义的各种类型的字段(包含从父类继承下来的和自身领有的字段)
-
规定
- 雷同宽度的字段总是被调配在一起
- 父类中定义的变量会呈现在子类之前(父类在子类之前加载)
- 如果 CompactFields 参数为 true(默认为 true):子类的窄变量可能插入到父类变量的空隙
2.3、对齐填充
对齐填充
不是必须的,也没特地含意,仅仅起到占位符的作用
内存布局总结
- 代码
public class Customer{
int id = 1001;
String name;
Account acct;
{name = "匿名客户";}
public Customer(){acct = new Account();
}
}
class Account{}
public class ObjectTest {public static void main(String[] args) {Object obj = new Object();
}
}
- 图解内存布局
3、对象的拜访定位
JVM 是如何通过栈帧中的对象援用拜访到其外部的对象实例呢?
对象的两种拜访形式:句柄拜访和间接指针
1、句柄拜访
- 毛病:在堆空间中开拓了一块空间作为句柄池,句柄池自身也会占用空间;通过两次指针拜访能力拜访到堆中的对象,效率低
- 长处:reference 中存储稳固句柄地址,对象被挪动(垃圾收集时挪动对象很广泛)时只会扭转句柄中实例数据指针即可,reference 自身不须要被批改
2、间接指针(HotSpot 采纳)
- 长处:间接指针是局部变量表中的援用,间接指向堆中的实例,在对象实例中有类型指针,指向的是办法区中的对象类型数据
- 毛病:对象被挪动(垃圾收集时挪动对象很广泛)时须要批改 reference 的值
你只管学习,我来负责记笔记???? 关注公众号!, 更多笔记,等你来拿,谢谢