创建对象的步骤
创建一个对象Object object = new Object();
从字节码角度
- new:如果找不到Class实例对象,就会对该类进行加载。加载成功后,在堆中分配内存。内存分配完毕后,将指向这个实例对象的引入放入栈顶
- dup:将栈顶的实例变量的引用复制一份,这时两个实例变量的引用指向了同一个对象实例。如构造方法()有参数,会将参数放入操作数栈中。
- INVOKESPECIAL:用栈顶的引用,调用init<>的构造方法。
从执行步骤的角度
- 判断类是否被加载
- 在堆中分配内存地址
- 除了并发安全问题(TLAB、CAS)
- 对对象中的实例变量进行初始化。即设置默认值
- 设置对象的对象头
- 指向init<>方法进行初始化
从内存的角度
见jvm堆中对象的创建
对象的内存布局
对象的内存布局中,数组对象和一般对象的内存布局有点不一样,数组的内存布局多一个长度字段,其余的都是一样的。
图示:该图来源于网络
其中
- MarkWord是对象中的一些信息,如hashCode、锁的类型、GC分代年龄
- Klass Pointer的这个对象指向的类信息的指针
- Instance Data:是这个对象中的实例变量数据
- 对其填充的保证对象占用的是8的倍数的字节
对象头的说明:
因为对象头中包含锁的信息,而锁的信息的知识是JUC中的synchronized
的知识,所以这里就关注第一个无锁的状态。
可以看到,对象头占用了68位bit,即8字节。在无锁情况下
,有26位都是没有被使用的。
代码测试:
测试类:
public class User {
int age = 10;
boolean a = false;
char c = 'a';
}
需要引入maven依赖:
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
测试代码1:
public static void main(String[] args) {
User user = new User();
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
输出:
可以看到,输出的结果以次是:对象头信息、类信息的指针、实例数据、字段补充
其中有一些不一样的信息:
- 对象头信息的hashCode是为0,这是因为hashCode要在调用hashCode的时候才会被赋值
- 类指针是占用的4字节,而不是8字节。主要是使用了指针压缩,可以使用
-XX:-UseCompressedClassPointers
来关闭指针压缩 - 实例变量占用的字节和Java基础中描述的是一样的。但是其在内存被根据类型的大小对数据进行重排,这就可以提高内存的利用率,如图:
- 补充位是将内存补充为8的整数被字节。这里补充到了24为。
测试代表2:调用了hashCode,并且关闭指针压缩-XX:-UseCompressedClassPointers
。
public static void main(String[] args) {
User user = new User();
user.hashCode();
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
对象头的16进制为为:
0x0000006e0be85801 对应的二进制如下:
00000000 00000000 00000000 01101110 00001011 11101000 01011000 00000001
0x6e0be858 对应的二进制如下:
01101110 00001011 11101000 01011000
完成符合前面的概念,对象头也是一样的
宋红康老师的对象布局图
宋红康老师的对象布局图可以说是最详细的布局图,图如下:
对象的访问定位
即用哪种方式访问到对象的位置,有句柄访问
和直接使用指针访问
句柄访问
即通过一个中间状态来关联对象实例和对象的类
好处:在进行垃圾回收后,对象的移动只需要改变中间状态的指针即可,不用修改栈中的指针
直接使用指针访问
这是hosport虚拟机的访问方式,也是前面对象布局中的结果,直接将类型指针放在对象实例数据中
好处:好处:速度更快,java中对象访问频繁,每次访问都节省了一次指针定位的时间开销。