一、对象的创建
一个对象创建的过程需要经历
1. 类加载检查
在执行一个 new 指令的时候,Java 虚拟机首先会到常量池中寻找是否存在有当前指令参数的类的符号引用,并且检查该类是否执行过加载,解析,初始化等操作,如果没有则执行类的加载过程。
2. 内存分配
通过类加载检查之后,类所占用的内存大小就可以确定了,这个时候需要对该类进行内存分配。 对于对象的内存分配是一个较为复杂的流程,在这里我们先可以简单的将这个过程理解为,在堆中划分一块确定大小的内存空间。 在这里我们需要了解的是如何划分内存,以及在并发的情况下如何解决分配冲突。
对于划分内存
指针碰撞(默认的分配方式):在连续分配的内存空间中,即堆内存空间绝对规整,虚拟机会保存最后分配内存的位置即分界点,在需要内存分配时即在分界点分配一个对象大小的内存空间并将指针移动一个与对象大小相等的距离即可
空闲列表:
而对于不规整的堆内存空间,即已分配空间与空闲空间交错,这个时候虚拟机需要维护一个空闲列表,来标志堆内存中可分配的空间,在需要进行内存分配的时候会先读取空闲列表寻找足够大小的内存空间进行分配,并更新空闲列表信息。相对于指针碰撞的分配方式空闲列表的分配方式因为要维护一个列表以及不规整的内存空间导致的空间不连续其性能要相对低一些。
对于并发分配冲突
CAS (Compare And Swap)
虚拟机采用 CAS 比较交换和失败重试的机制来保证更新的原子性,即多个线程同时对一块内存进行分配争抢时,最先争抢到的进行该块区域的内存分配,其他分配失败线程移到下一块可用空间进行失败重试,重复上面的操作直至分配成功。
TLAB 本地线程缓冲分配(Thread Local Allocation Buffer)
TLAB 本地线程缓冲分配,虚拟机会预先为每一个线程在堆内分配一块内存空间,默认为堆内存的 1%,该线程的所有内存分配都会先在预先分配好的区域进行分配,如果分配不下则转为 CAS 进行分配。这个有些类似于虚拟机栈的那种分配模式,同样是给栈预先分配一块内存空间,可以看出这样的思想是通用的,预先给每个线程分配好一块内存空间可以减少多个线程抢占同一块内存带来的性能消耗。 通过 -XX:+/- UseTLAB 参数来控制是否使用 TLAB 的分配模式 JVM 默认开启该模式(-XX:+ UseTLAB)
3. 初始化
初始化的过程会对新建对象的属性进行默认的赋值,这个值是 Java 定义的默认值,这个过程保证了对象的属性可以不赋初值就可以使用。
4. 设置对象头 Object Header
一个对象是由三部分组成
- 对象头 对象头会存储类的一些基本信息, 锁标志,分代年龄,对象哈希码及类元信息位置。
- 实例数据 实例数据即我们所定义的类的字段信息
- 对齐填充 对齐填充是类在做内存分配的时候为了内存分配的时候为了规整会对类的大小进行一个填充对齐,保证其大小为 8字节 的倍数
这里顺便复习一下单位的换算
8 bit = 1 byte
1024 byte = 1 kb
1024 kb = 1 m
1024 m = 1 g
标志位 Mark Word
记录了对象的 锁状态、哈希值、线程持有锁、偏向线程ID、分代年龄。 其在 32 位机器中占用 4 个字节,64 位机器中占 8 字节。
类型指针 Klass Painter
类型指针存储了类元信息的内存地址信息。开启指针压缩占 4 个字节,关闭占 8 个字节。默认开启。
数组长度(仅数组对象有)
数组长度为数组对象特有,占用 4 个字节
我们可以通过添加 jol 来查看对象内存分配信息
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
示例代码:
package think_in_jvm;
import org.openjdk.jol.info.ClassLayout;
public class JolSimpleDemo {
public static void main(String[] args) {
ClassLayout layout = ClassLayout.parseInstance(new Object());
System.out.println(layout.toPrintable());
System.out.println();
ClassLayout layout1 = ClassLayout.parseInstance(new int[19]);
System.out.println(layout1.toPrintable());
System.out.println();
ClassLayout layout2 = ClassLayout.parseInstance(new A());
System.out.println(layout2.toPrintable());
}
// -XX:+UseCompressedOops 默认开启的压缩所有指针
// -XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针Klass Pointer
// Oops : Ordinary Object Pointers
public static class A {
//8B mark word
//4B Klass Pointer 如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,则占用8B
int id; //4B
String name; //4B 如果关闭压缩-XX:-UseCompressedOops,则占用8B
byte b; //1B 这里会进行一个内部对齐 3B 的对齐填充 实则占用 4B
Object o; //4B 如果关闭压缩-XX:-UseCompressedOops,则占用8B
}
}
以下为控制台打印结果
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 20 (01101101 00000001 00000000 00100000) (536871277)
12 4 (object header) 13 00 00 00 (00010011 00000000 00000000 00000000) (19)
16 76 int [I.<elements> N/A
92 4 (loss due to the next object alignment)
Instance size: 96 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
think_in_jvm.JolSimpleDemo$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 65 cc 00 20 (01100101 11001100 00000000 00100000) (536923237)
12 4 int A.id 0
16 1 byte A.b 0
17 3 (alignment/padding gap)
20 4 java.lang.String A.name null
24 4 java.lang.Object A.o null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
Object 类:
int 数组:
自定义对象 A:
常用数据类型占用内存空间大小
设置参数后:-XX:-UseCompressedOops
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 1c 61 17 (00000000 00011100 01100001 00010111) (392240128)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
[Ljava.lang.Object; object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) c0 dc 6c 17 (11000000 11011100 01101100 00010111) (393010368)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 (object header) 13 00 00 00 (00010011 00000000 00000000 00000000) (19)
20 4 (alignment/padding gap)
24 152 java.lang.Object Object;.<elements> N/A
Instance size: 176 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
think_in_jvm.JolSimpleDemo$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 08 e2 ca 17 (00001000 11100010 11001010 00010111) (399172104)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 8 double A.d 0.0
24 4 int A.id 0
28 1 byte A.b 0
29 3 (alignment/padding gap)
32 8 java.lang.String A.name null
40 8 java.lang.Object A.o null
48 8 think_in_jvm.JolSimpleDemo.A A.objA null
Instance size: 56 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
关于指针压缩 指针压缩在 jdk 1.6 之后是默认开启的,开启了指针压缩会节省大量的内存空间,我们都知道在 32 位的机器上 2 的 32 次方 即 4 g 大小的内存寻址,如果想支持 8 g 则需要 33 位才能去表示另外 4 g 的内存地址。对于 64 位机器其支持 2 的 64 次方 的内存,即2147483648 g,这个是非常大的一个数了,可能在近几百年内都是够用的,我们去用 64 位去表示一个对象的位置,对于我们现在常用的 8 g、16 g、32 g 内存来说显然是有些浪费的,所以提供了指针压缩的技术,在真正寻址的时候会进行解压来定位寻址,一般最大支持到 35 位的指针压缩即 32 g内存,如果你的机器超过 32 g 则会使用 64 位来表示。所以一般推荐分配机器内存不超过 32 g。
5. 执行<init> 方法
执行对象初始化,这个过程会将用户设置的初值赋值给对应属性变量,并执行初始化方法。