1.概述
类是在运⾏期间第⼀次使⽤时,被类加载器动态加载⾄ JVM 。JVM不会⼀次性加载所有 类。因为如果⼀次性加载,那么会占⽤很多的内存。
2.类的生命周期
类的生命周期包括以下 7 个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
2.1类加载过程
类加载过程包含:加载、验证、准备、解析和初始化 ,⼀共包括 5 个阶段。
可以通过⼀句谐⾳来记忆“家宴准备了西式菜” = 家 (加载) 宴 (验证) 准备 (准备) 了⻄ (解析) 式 (初始化) 菜
Step1:加载
加载过程完成以下 3 件事:
- 通过类的完全限定名称获取定义该类的⼆进制字节流。
- 将该字节流表示的静态存储结构转换为 Metaspace 元空间区的运⾏时存储结构。
- 在内存中⽣成⼀个代表该类的 Class 对象,作为元空间区中该类各种数据的访问⼊⼝。
其中⼆进制字节流可以从以下⽅式中获取:
- 从 ZIP 包读取,成为 JAR 、 EAR 、 WAR 格式的基础。
- 从⽹络中获取,最典型的应⽤是 Applet 。
- 运⾏时计算⽣成,例如动态代理技术,在 java.lang.reflect.Proxy 使⽤ Proxy Generator.generateProxyClass 的代理类的⼆进制字节流。
- 由其他⽂件或容器⽣成,例如由 JSP ⽂件⽣成对应的 Class 类。
Step2:验证
确保 Class ⽂件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机⾃身的安 全。
Step3:准备
- 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使⽤的是元空 间区的内存。
- 实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象⼀起被分配在堆中。应该注意 到,实例化不是类加载的⼀个过程,类加载发⽣在所有实例化操作之前,并且类加载只进⾏⼀ 次,实例化可以进⾏多次。
- 初始值⼀般为 0 值。例如:下⾯的类变量 value 被初始化为 0 ⽽不是 123 。
public static int value = 123;
- 如果类变量是常量,那么它将初始化为表达式所定义的值⽽不是 0。
public static final int value = 123;
Step4:解析
将常量池的符号引⽤替换为直接引⽤的过程。其中解析过程在某些情况下可以在初始化阶段之后再开 始,这是为了⽀持 Java 的动态绑定。
Step5:初始化
初始化阶段才真正开始执⾏类中定义的 Java 程序代码。初始化阶段是虚拟机执⾏类构造器 () ⽅法的过程。在准备阶段,类变量已经赋过⼀次系统要求的初始值,⽽在初始化阶 段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
<clinit>() 是由编译器⾃动收集类中所有类变量的赋值动作和静态语句块中的语句合并产 ⽣的,编译器收集的顺序由语句在源⽂件中出现的顺序决定。所以,静态语句块只能访问到定义在它 之前的类变量,定义在它之后的类变量只能赋值,不能访问。
例如:以下代码中静态变量 i 只能赋值,不能访问,因为 i 定义在静态代码块的后⾯。
public class Test {static {i = 0; // 给变量赋值可以正常编译通过System.out.print(i); // 这句编译器会提示“⾮法向前引⽤”}static int i = 1;
}
接⼝中不可以使⽤静态语句块,但仍然有类变量初始化的赋值操作,因此接⼝与类⼀样都会⽣ 成 () ⽅法。但接⼝与类不同的是,执⾏接⼝的 () ⽅法不需要先执⾏⽗ 接⼝的 () ⽅法。只有当⽗接⼝中定义的变量使⽤时,⽗接⼝才会初始化。另外,接⼝ 的实现类在初始化时也⼀样不会执⾏接⼝的 () ⽅法。
虚拟机会保证⼀个类的 () ⽅法在多线程环境下被正确的加锁和同步,如果多个线 程同时初始化⼀个类,只会有⼀个线程执⾏这个类的 () ⽅法,其它线程都会阻塞等 待,直到活动线程执⾏ () ⽅法完毕。如果在⼀个类的 () ⽅法中有耗时 的操作,就可能造成多个线程阻塞,在实际过程中,该阻塞⾮常隐蔽,⼏乎不会被察觉。
3. 类加载的时机
3.1主动引用
虚拟机规范中并没有强制约束何时进⾏加载,但是规范严格规定了只有下列六种情况必须对类进 ⾏加载:
- 当遇到 new 、 getstatic 、 putstatic 或 invokestatic 这 4 条字节码指令 时,⽐如 new ⼀个对象,读取⼀个静态字段(未被 final 修饰)、或调⽤⼀个类的静态⽅法 时。
○ 当 jvm 执⾏ new 指令时会加载类。即:当程序创建⼀个类的实例对象。
○ 当 jvm 执⾏ getstatic 指令时会加载类。即:程序访问类的静态变量(不是静态常量,常量会被加载到运⾏时常量池)。
○ 当 jvm 执⾏ putstatic 指令时会加载类。即:程序给类的静态变量赋值。
○ 当 jvm 执⾏ invokestatic 指令时会加载类。即:程序调⽤类的静态⽅法。
- 使⽤ java.lang.reflect 包的⽅法对类进⾏反射调⽤时如 Class.forname ("...") , 或 newInstance() 等等。如果类没初始化,需要触发类的加载。
- 加载⼀个类,如果其⽗类还未加载,则先触发该⽗类的加载。
- 当虚拟机启动时,⽤户需要定义⼀个要执⾏的主类 (包含 main() ⽅法的类),虚拟机会先加 载这个类。
- 当⼀个接⼝中定义了 JDK8 新加⼊的默认⽅法(被 default 关键字修饰的接⼝⽅法) 时,如果有这个接⼝的实现类发⽣了加载,则该接⼝要在实现类之前被加载。
3.2. 被动引⽤
除主动引⽤之外,所有引⽤类的⽅式都不会触发加载,称为被动引⽤。
被动引⽤的常⻅例⼦包括:
- 通过⼦类引⽤⽗类的静态字段,不会导致⼦类加载。
- 通过数组定义来引⽤类,不会触发此类的加载。该过程会对数组类进⾏加载,数组类是⼀个由虚 拟机⾃动⽣成的、直接继承⾃ Object 的⼦类,其中包含了数组的属性和⽅法。
- 常量在编译阶段会存⼊调⽤类的常量池中,本质上并没有直接引⽤到定义常量的类,因此不会触 发定义常量的类的加载。
4. 类与类加载器
4.1. 概述
两个类相等,需要类本身相等,包括类的 Class 对象的 equals() ⽅法、 isAssignab leFrom() ⽅法、 isInstance() ⽅法的返回结果为 true ,也包括使⽤ instanceof 关 键字做对象所属关系判定结果为 true 。
除此之外,还要求两个类使⽤同⼀个类加载器进⾏加载,因为每⼀个类加载器都拥有⼀个独⽴的 类名称空间。
4.2. 类加载器分类
从 Java 虚拟机的⻆度来讲,只存在以下两种不同的类加载器:
- 启动类加载器( Bootstrap ClassLoader ),使⽤ C++ 实现,是虚拟机的⼀部分;
- 其它类的加载器,使⽤ Java 实现,独⽴于虚拟机,继承⾃抽象类 java.lang.ClassLoader 。
从 Java 开发⼈员的角度看,类加载器可以划分得更细致⼀些:
- 启动类加载器( Bootstrap ClassLoader ),该类加载器负责将存放在 \l ib ⽬录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的 (仅按照⽂件名识别,如 rt.jar ,名字不符合的类库即使放在 lib ⽬录中也不会被加 载)类库加载到虚拟机内存中。例如 java.util.* , java.io.** , java.lang.* 类 等常⽤基础库都是由启动类加载器加载。启动类加载器⽆法被 Java 程序直接引⽤。
- 扩展类加载器(Extension ClassLoader),该类加载器是由 ExtClassLoader ( sun.mi sc.Launcher$ExtClassLoader )实现,负责将 /lib/ext 或者被 jav a.ext.dir 系统变量所指定路径中的所有类库加载到内存中,例如 swing 系列、内置的 j s 引擎、 xml 解析器等以 javax 开头的扩展类库都是由扩展类加载器加载,开发者可以直 接使⽤扩展类加载器。
- 应用程序类加载器( Application ClassLoader ),该类加载器是由 AppClassLoade r ( sun.misc.Launcher$AppClassLoader )实现。由于这个类加载器是 ClassLoad er 中的 getSystemClassLoader() ⽅法的返回值,因此也被称为系统类加载器。它负 责加载⽤户类路径( ClassPath )上所指定的类库,⽐如:我们⾃⼰编写的⾃定义类或第三 ⽅ jar 包。开发者可以直接使⽤这个类加载器,如果应⽤程序中没有⾃定义过⾃⼰的类加载 器,⼀般情况下这个就是程序中默认的类加载器。
5. 双亲委派模型
应⽤程序是由三种类加载器互相配合,从⽽实现类加载,除此之外还可以加⼊⾃⼰定义的类加载 器。
类加载器之间的层次关系,称为双亲委派模型( Parents Delegation Model )。该模型 要求除了顶层的启动类加载器外,其它的类加载器都要有⾃⼰的⽗类加载器。这⾥的⽗⼦关系⼀般通 过组合关系( Composition )来实现,⽽不是继承关系( Inheritance )。
5.1. 双亲委派⼯作机制
⼀个类加载器首先将类加载请求转发到⽗类加载器,只有当⽗类加载器⽆法完成时才尝试⾃⼰加载。
5.2. 双亲委派的作⽤
- 使得 Java 类随着它的类加载器⼀起具有⼀种带有优先级的层次关系,从⽽使得基础类得到 统⼀,避免冲突
例如: java.lang.Object 存放在 rt.jar 中,如果编写另外⼀个 java.lang.Object 并 放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object ⽐在 ClassPath 中的 Object 优先级更⾼,因为 rt.jar 中的 Object 使⽤的是启动 类加载器,⽽ ClassPath 中的 Object 使⽤的是应⽤程序类加载器。 rt.jar 中的 Objec t 优先级更⾼,那么程 序中使⽤的所有的 Object 都是由启动类加载器所加载的 Object 。
- 实现热加载,⽐如 Spring Boot DevTools
6. 对象的创建过程
Step1:类加载检查
虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类 的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须 先执⾏相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成 后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。内存 分配的查找⽅式有 “指针碰撞” 和 “空闲列表” 两种。
选择以上两种⽅式中的哪⼀种,取决于 Java 堆内存是否规整。⽽ Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"。
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操 作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数 据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才 能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
Step5:执行init 构造方法
在上⾯工作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从 Java 程序的 视⻆来看,对象创建才刚开始, 构造⽅法还没有执⾏,⽬前所有的字段都还为零。所以⼀ 般来说,执⾏ new 指令之后会接着执⾏ 构造⽅法,把对象按照程序逻辑的意愿进⾏初 始化,这样⼀个真正可⽤的对象才算完整创建出来。