内容若有侵权,请联系删除,若有不妥之处,还请批评指正。⭐⭐⭐
引言
- 对象的实例化过程是分成两部分:类的加载初始化,对象的初始化
- 要创建类的对象实例需要先加载并初始化该类,main方法所在的类需要先加载和初始化
- 类初始化就是执行<clinit>方法,对象实例化是执行<init>方法
- 一个子类要初始化需要先初始化父类
对象的实例化过程
当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
举个通俗点的🌰: JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有classA的相关信息,于是JVM就会到相应的class文件中去寻找classA的类信息,并加载进内存中,这就是我们所说的类加载过程。由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。
类的加载过程
类的加载机制:如果没有相应类的class,则加载class到方法区。对应着加载->验证->准备->解析–>初始化阶段
○ 加载:载入class对象,不一定是从class文件获取,可以是jar包,或者动态生成的class
○ 验证:校验class字节流是否符合当前jvm规范
○ 准备:为 类变量 分配内存并设置变量的初始值( 默认值 )。如果是final修饰的对象则是赋值声明值
○ 解析:将常量池的符号引用替换为直接引用
○ 初始化:执行类构造器<client>( 注意不是对象构造器 ),为 类变量 赋值,执行静态代码块。jvm会保证子类的<clinit>执行之前,父类的<clinit>先执行完毕
● 其中验证、准备、解析3个部分称为 连接;
● <clinit>方法由 静态变量赋值代码和静态代码块组成;先执行类静态变量显示赋值代码,再到静态代码块码;
在加载类时,Java 虚拟机必须完成以下3件事情:
● 通过类的全名,获取类的二进制数据流
● 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
● 创建 java.lang.Class 类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
触发类加载的条件
-
第一次创建类的新对象时,会触发类的加载初始化和对象的初始化函数<init>执行,这个是实例初始化。
-
JVM启动时会先加载初始化包含main方法的类。
-
调用类的静态方法(如执行invokestatic指令)。
-
对类或接口的静态字段执行读写操作(即执行getstatic、putstatic指令);不过final修饰的静态字段的除外(已经赋值,String和基本类型,不包含包装类型),它被初始化为一个编译时常量表达式。
注意:在操作静态字段时,只有直接定义这个字段的类才会被初始化;如通过其子类来操作父类中定义的静态字段,只会触发父类的初始化而不是子类的初始化 -
调用JavaAPI中的反射方法时(比调用java.lang.Class中的方法(Class.forName),或者java.lang.reflect包中其他类的方法)。
-
当初始化一个类时,其父类没有初始化,则需先触发父类的初始化(接口例外)。
对象的实例化过程
内存分配
当类加载过程完成后,或者类本身之前已经被加载过,下一步就是虚拟机要为新生对象分配内存。对象所需要的内存空间在类加载过程完成后就可以完全确定下来,为对象分配内存空间就相当于从堆内存中划分出一块合适的内存来,分配内存的主要方式有两种:指针碰撞和空闲列表。
指针碰撞:这种方式将堆内存分为空闲空间与已分配空间,使用一个指针来作为二者之间的分界线,当要为新生对象分配内存空间的时候,相当于将指针向着空闲空间的方向移动一段与对象大小相等的距离,可见这种分配方式Java堆内存必须是规整的,所有空闲空间在一边,已分配空间在另外一边。
空闲列表:在虚拟机中维护一个列表,用来记录堆中哪一块内存是空闲可用的,在为新生对象分配内存时,从列表中寻找一块合适大小的可用内存块,分配完成后更新空闲列表,这种方式下堆内存的空闲空间与分配空间可以交错存在。
选择采用指针碰撞还是空闲列表法分配内存,主要由Java堆内存是否规整决定的,而Java堆内存是否规整又取决于所采用的垃圾收集算法,这就涉及到垃圾回收机制。
同时,由于创建对象的动作是十分频繁的,多线程可能存在多个线程同时申请为对象分配内存空间,这个时候如果不采取一定的同步机制,就有可能导致一个线程还未来得及修改指针,另一个线程就使用了原来的指针分配内存空间,因此衍生出来了两种解决方案:CAS + 失败重试、TLAB方式。
第一种方式很好理解,多个线程使用CAS的方式更新指针,多线程下只有一个线程可以更新完成,其他线程通过不断重试完成内存指针的重新移动。
第二种方式是每个线程提前分配一块内存空间,这个内存空间就是线程本地缓冲TLAB,这样线程每次要分配内存时,先去TLAB中获取,当TLAB中内存空间不足的时候才采用同步机制继续申请一块TLAB空间,这样就降低了同步锁的申请次数。
初始化零值
在为对象分配内存完成之后,虚拟机会将分配到的这块内存初始化为零值,这样也就使得Java中的对象的实例变量可以在不赋初值的情况下使用,因为代码所访问当的就是虚拟机为这块内存分配的零值。
设置对象头
对象头就像我们人的身份证一样,存放了一些标识对象的数据,也就是对象的一些元数据,我们首先看一下对象的构成。
在初始化了零值之后,怎么知道对象是哪个类的实例,就需要设置指向方法区中类型信息的指针,对象Mark Word中相关信息的设置,就在这个阶段完成。
实例对象初始化
对象实例化过程其实就是执行类构造函数 对应在字节码文件中的()方法(称之为实例构造器);()方法由 非静态变量、非静态代码块以及对应的构造器组成
()方法可以重载多个,类有几个构造器就有几个()方法
()方法中的代码执行顺序为:父类变量初始化,父类代码块,父类构造器,子类变量初始化,子类代码块,子类构造器。
静态变量,静态代码块,普通变量,普通代码块,构造器的执行顺序
具有父类的子类的实例化顺序如下
类加载器和双亲委派规则,如何打破双亲委派规则
● 类加载器
○ 通过一个类的全限定名来获取 描述此类的二进制字节流 ,实现这个动作的代码模块称为类加载器
○ 任意一个类都需要其加载器和类本身来确定类在JVM的唯一性;每个类加载器都有自己的类名称空间,同一个类class由不同的加载器加载,则被JVM判断为不同的类
● 双亲委派模型
○ 启动类加载器有C++代码实现,是虚拟机的一部分。负责加载lib下的类库
○ 其他的类加载器有java语言实现,独立于JVM,并且继承ClassLoader
○ extention ClassLoader负责加载libext目录下的类库
○ application ClassLoader 负责加载用户路径下(ClassPath)的代码
○ 不同的类加载器加载同一个class文件会导致出现两个类。而java给出解决方法是下层的加载器加委托上级的加载器去加载类,如果父类无法加载(在自己负责的目录找不到对应的类),而交还下层类加载器去加载。如下图
● 打破双亲委派模型
○ 双亲委派模型并不是一个强制的约束模型,而是java设计者推荐给开发者的类加载实现方式
○ 双亲委派模型很好的解决各个类加载基础类的同一问题(越基础的类由越上层的加载器加载),但是基础类总是作为用户代码调用的API,但是如果它的具体实现是下层的代码,此时基础类需要调用下层的代码,则需要打破双亲委派模型
○ 如JNDI服务,JNDI的代码有启动类去加载(rt.jar),它需要调用由独立厂商部署在应用程序classpath下的JNDI的SPI(Service Provider Interface)代码。为了解决SPI代码加载问题,java引入了线程上下文类加载器去加载SPI代码。也就是父类加载器请求子类去完成类的加载动作
○ 线程上下文类加载器,线程创建时会从父线程继承,如果全局范围没有设置过,则默认设置为application Class Loader
完整流程(借鉴)
Java对象实例化全流程