目录
一、类的生命周期
类的生命周期包括以下7个阶段:
1.1 类加载过程
类加载过程包括:加载、验证、准备、解析和初始化,一共5个阶段。
1.加载
加载过程完成三件事:
- 通过类的完全限定名获取定义该类的二进制字节流。
- 将字节流表示的静态存储结构转变为原空间区的运行时存储结构。
- 在内存中生成一个代表该类的Class对象,作为元空间区中该类的各种数据访问入口。
2.验证
确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3.准备
- 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存。
- 实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。注意:实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
- 初始值一般为 0 值。
4.解析
将常量池的符号引用替换为直接引用的过程。
5.初始化
初始化阶段是虚拟机执行类构造器 <clinit>()方法的过程。
<clinit>()是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。所以,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
二、类加载的时机
2.1 主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了只有下列几种情况必须对类进行加载:
- 当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时。
- 当 jvm 执行 new指令时会加载类。即:当程序创建一个类的实例对象。
- 当 jvm 执行 getstatic指令时会加载类。即:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- 当 jvm 执行 putstatic指令时会加载类。即:程序给类的静态变量赋值。
- 当 jvm 执行 invokestatic指令时会加载类。即:程序调用类的静态方法
- 使用 java.lang.reflect包的方法对类进行反射调用时如 Class.forname("..."), 或newInstance() 等等。如果类没初始化,需要触发类的加载。
- 加载一个类,如果其父类还未加载,则先触发该父类的加载。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main() 方法的类),虚拟机会先加载这个类。
- 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。
2.2 被动引用
除主动引用之外,所有引用类的方式都不会触发加载,称为被动引用。
- 通过子类引用父类的静态字段,不会导致子类加载。
- 通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的加载。
三、类与类加载器
3.1 概述
两个类相等,需要类本身相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
除此之外,还要求两个类使用同一个类加载器进行加载,因为每一个类加载器都拥有一个独立的类名称空间。
3.2 类加载器分类
- 启动类加载器:加载核心类库
- 扩展类加载器:加载扩展类库(ext)
- 应用程序类加载器:加载自定义类或第三方jar包
四、双亲委派模型
应用程序是由三种类加载器互相配合,从而实现类加载,除此之外还可以加入自己定义的类加载器。
类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
4.1 双亲委派工作机制
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
4.2 双亲委派的作用
- 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一,避免冲突。
- 实现热加载,比如Spring Boot DevTools。
五、对象的创建过程
1.类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。内存分配的查找方式有 “指针碰撞” 和 “空闲列表” 两种。
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"。
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5.执行init构造方法
执行 new 指令之后会接着执行 <init> 构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来。