对象的创建
1.类加载检查
虚拟机遇到一条new的指令,首先去检查这个指令的参数能否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行类的加载过程。
2.分配内存
在类加载检查 通过后,接下来虚拟机将为新生对象分配内存 ,对象所需内存在类加载之后便可确定,为对象分配空间的任务等于说从把一块确定大小的内存从Java堆中划分出来。分配方式有指针碰撞 和空闲列表 两种方式,选择哪种方式由Java堆是否规整决定 ,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:
指针碰撞:
适用场景:堆内存规整的情况下(没有内存碎片)。
原理:用过内存的整合到一边,没有用过内存的在另一边,中间是分界指针,指针向着没有用过内存的一边移动对象内存大小位置即可。
空闲列表:
适用场景:堆内存不规整的情况下。
原理:虚拟机会维护一个列表,记录哪些内存块是可用的,分配空间时,找一块内存大小足够的内存块来划分给对象示例,最后更新列表。
内存分配并发问题:
在创建对象时,一个很大的问题就是线程安全,因为在并发场景下,创建对象十分频繁,作为虚拟机来说,必须保证线程安全,虚拟机有两种方法来保证线程安全:
CAS+失败重试 :CAS是乐观锁实现的一种,如果不了解CAS和乐观锁的可以看我之前写的关于乐观锁的博客:【并发编程 | 第四篇】悲观锁与乐观锁的学习-CSDN博客,所谓乐观锁就是,每次操作都不加锁,而是假设没有冲突去完成某个操作,如果失败,就重试直到成功为止。虚拟机利用CAS+重试的方式更新操作的原子性。
TLAB:为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化零值,这一步操作保证了对象的示例在Java代码执行时不需要赋值就可直接使用,程序能访问到这些字段的数据类型所对应的初始值。
4.设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置 ,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5.执行init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象的内存布局
在HotSpot虚拟机中,对象在内存中的布局可以分为三块区域:对象头、实例数据、对齐填充。
对象头包含两部分信息:
1.标记字段:用于存储对象自身运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
2.类型指针:对象指向它的类元数据指针,虚拟机通过这个指针来确定它是哪个类的示例对象。
实例数据部分是对象真正存储的有效信息,也就是在程序中所定义的各种字段的信息。
**对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。**因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!