JVM:对象创建的过程
对象创建的过程
如图是大致流程
- 在常量池中检查是否存在创建对象使用类的符号引用
- 分配内存空间(这里指大部分情况,即对象实例在堆上分配)
- 对分配的空间进行相应数据的填充
- 将对象的引用(也可以是句柄,但hotspot采用引用)入栈,即给对象变量赋值
1.常量池中检查类符号引用
- 遇到new指令后,要先判断使用的类是否已经加载进来,因为对象的创建是依据类信息来创建的,简单来说就是需要模板或者设计图纸
- 如果类加载进来了,那么常量池中就会存在该类的符号引用,通过该引用就可以找到存放类信息的内存空间,就可以拿到设计图纸了
- 如果类没有被加载进来就需要先将类加载进来(类加载过程又是一系列很复杂的过程)
2.分配内存空间
- 分配多大:类信息中有
- 在堆的那部分空间分配:新生代?老年代?
- 空间分配方式(堆内存整齐是指使用过的内存全部在一边,另一边没使用过的;整齐否依赖于垃圾回收机制)
- 指针碰撞:内存整齐,将指向可没使用内存首地址的指针向后移动对象对象个空间
- 空闲列表:内存不整齐,通过在空闲列表(所有没使用过内存形成的表)上划分出一块对象大小的空间,更新空闲列表
- 分配过程的安全保障
- 每个对象分配都采用同步处理:CAS+失败重试 原子性操作
- TLAB:分配TLAB时同步处理,该线程对象在分配的缓冲区可直接分配空间
- 对象空间数据部分初始化为零,如果采用了TLAB则该过程可以在TLAB分配时就完成
3.内存空间赋值
- 对象空间=对象头(对象自己的运行时数据+对象的类型指针+[数组长度])+实例数据+对齐填充
- 先划分好空间区域,然后填写对象头,再然后依据来填写实例数据,最后补齐空白(对象大小必须是8字节的整数倍)
- 对象头
- 运行时数据(MarkWord)(25+4+2+1(0固定值))
- 组成:哈希码、GC年代、锁状态(同步相关)、线程持有锁、偏向线程id、偏向时间等
- 长度:32/64bit
- 组成不是全部都有,其结构非固定,为了提高空间利用率可重复使用,当两位的标志位不同时,其结构不同
- 类型指针:类元数据指针,用来确定对象是哪个类的实例(对于通过句柄找对象的方式则不用存类型指针)
- 数组长度:不是对象,是数组时才有这一项,对象大小可以通过类元信息知道,但是数组大小就无法确定了,所以必须增加这一项来确定数组长度才能知道哪里是对象的结尾
- 运行时数据(MarkWord)(25+4+2+1(0固定值))
25 | 4 | 2 | 1 |
---|---|---|---|
哈希码 | GC年代 | 01无锁 | 0 |
锁记录指针+4bit | 00轻量级锁 | 0 | |
重量级锁指针+4bit | 10重量级锁 | 0 | |
空 | 空 | 11GC标记 | 0 |
偏向线程id、偏向时间戳、对象分代年龄 | GC年代 | 01可偏向 | 0 |
- 实例数据
- 按照虚拟机分配策略和字段在代码中的定义顺序来定义实例每个空间的类型大小
- hotspot虚拟机分配策略:long/double–int–short/char–byte/boolean–oop(对象指针),即占内存大的类型在前,小的在后
- 在虚拟机分配策略基础上,父类在前子类在后
- 如果参数CompactField设置为true,则子类小的可能插入到父的空隙当中以节省内存空间
4.对象的访问定位
- 句柄:堆空间有专门的句柄池,存储稳定,当对象改变存储位置时refrence不需要改变,但是需要两次寻址
- 直接指针:一次寻址速度快,因为对象访问频繁这样可以省时好多,hotspot采用这种方式