当Java虚拟机遇到一条字节码new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。
内存分配完成后,虚拟机必须将分配到的内存空间(不包含对象头)都初始化为零值(如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行TLAB为本地线程分配缓冲 详解可见下文)。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息保存在对象的对象头中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
至此,从虚拟机的角度来看,一个新的对象已经产生。然而从Java程序的角度来看,对象创建才刚刚开始--->构造函数,即Class文件中的<init>()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说,new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
加载--主要是将.class文件中的二进制字节流读入到新JVM中
连接
初始化--标记为常量值的字段赋值的过程,只对static修饰的变量或语句块进行初始化。
初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子<client>方法执行之前,父类的<client>方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法。
注意以下几种情况不会执行类初始化:
内存的分配方式有以下两种:
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由采用的垃圾收集器是否带有空间压缩整理的能力决定。
因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,即简单又高效。
而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂高效的空闲列表来分配内存。
指针碰撞方式存在的问题:
对象创建在虚拟机中是非常频繁的行为,仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的。
可能会出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:
由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。
Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)
MarkWord:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁的标志位的变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的还是在子类中定义的字段都必须记录起来。
对其填充不是必然存在的,也没有特别的含义,它仅仅起占位符的作用。由于任何对象的大小都必须是8字节的整数倍,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
原文:https://www.cnblogs.com/winter0730/p/14641986.html