哈喽,大家好🎉,我是世杰。
本文我为大家介绍面试官经常考察的「Java对象创建流程」
照例在开头留一些面试考察内容~~
带着这些问题,让我们开始吧!🎉🎉🎉
当虚拟机遇到一个字节码 new 指令的时候,首先去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用。并且检查这个符号引用代表的类是否被虚拟机类加载器加载。如果没有,必须先执行类加载的流程。(PS:类加载的过程可以看我之前的文章)
new指令对应到语言层面上讲是,new 关键词、对象克隆、对象序列化等。
在类的检查通过过后
1、虚拟机就会为新生成对象分配内存。对象所需要的内存大小在类加载的时候决定。
2、内存分配完成后,虚拟机会将这块分配到的内存空间(不包括对象头)都初始化为零值。
3、之后要进行对象进行初始化设置,比如元数据、对象的哈希编码、对象的 GC 分代年龄、偏向锁状态等信息这些信息都用于存放到对象头(Object Header)中。
4、执行 new 指令之后会接着执行构造器方法,把对象按照程序员的意愿进行初始化(构造方法)
这样一个真正可用的对象才算完全产生出来。
『总结对象创建的过程』
在详细聊加载流程之前,先说说对象在JVM堆中的内存布局(这里讲的HotSpot虚拟机对象结构)
被JVM加载对象内部结构分为:对象头、实例数据、对齐填充。
Mark Word
),如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分我们称之为"Mard Word"。Class Pointer
),即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。Length
),如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类中继承下来的,还是在子类中定义的,都需要记录下来。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oop,从分配策略中可以看出,相同宽度的字段总是分配到一起。
存放类的属性(Field)数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度
这部分内存按4字节对齐。
创建对象在判断类加载之后,还会判断内存是否规整,根据判断结构选择使用空闲列表还是指针碰撞的内存分配方式,在分配内存时还会考虑线程并发处理,使用CAS或者是TLAB来处理,然后在执行初始化零值、设置对象头、执行<init>方法
类加载检查通过后,那就要为实例化的对象分配内存。对象所需内存的大小在类加载完成后便完全确定(对象内存布局),为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
『分配方式』
根据Java堆中是否规整有两种内存的分配方式:(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定)
『并发处理』
对象频繁分配的过程中,即使只修改一个指针所指向的位置,但是在并发的情况下也不是线程安全的,可能出现正在给 A 对象分配内存,指针还没有来得及修改,对象 B 又同时使用原来的指针进行内分配的情况。需要借助以下方式实现线程安全
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这里的零值是指JAVA中字段默认的值。
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
完成上述流程,其实已经完成了虚拟机中内存的创建,但是我们在 Java 执行 new 创建对象的角度才刚刚开始,我们还需要调用构造方法初始化对象(可能还需要在此前后调用父类的构造方法、初始化块等)。进行 Java 对象的初始化。
参考文章
多态是Java语言极为重要的一个特性,可以说是Java语言动态性的根本,那么线程执行一个方法时到底在内存中经历了什么,JVM又是如何确定方法执行版本的呢?