当我们运行Java程序时,Java虚拟机(JVM)需要加载各种类文件,以执行程序中的代码。Java的类加载机制是Java语言的一个关键特性,它负责在运行时将类加载到内存中,并确保类的正确性。
类是在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
类的生命周期包括以下 7 个阶段:
● 加载(Loading)
● 验证(Verification)
● 准备(Preparation)
● 解析(Resolution)
● 初始化(Initialization)
● 使用(Using)
● 卸载(Unloading)
其中,前五个阶段是最重要的,它们也是类加载过程,可以通过一句谐音来记忆“家宴准备了西式菜” = 家 (加载) 宴 (验证) 准备 (准备) 了西 (解析) 式 (初始化) 菜
加载是类加载的第一个阶段,加载过程完成以下3件事:
1. 通过类的完全限定名称获取定义该类的二进制字节流。
2. 将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。
3. 在内存中生成一个代表该类的 Class 对象,作为元空间区中该类各种数据的访问入口。
public static int value = 123;
public static final int value = 123;
接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 clinit() 方法。但接口与类不同的是,执行接口的 clinit() 方法不需要先执行父接口的 clinit() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 clinit() 方法。
虚拟机会保证一个类的 clinit() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 clinit() 方法,其它线程都会阻塞等待,直到活动线程执行 clinit() 方法完毕。如果在一个类的 clinit() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中,该阻塞非常隐蔽,几乎不会被察觉。
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了只有下列几种情况必须对类进行加载:
public class Demo01 {
public static void main(String[] args){
//执行以下字节代码,类会被加载
//1.new指令
Parent parent = new Parent();
//getStatic指令
System.out.println(Parent.i);
//putStatic指令
Parent.i=4;
//invokeStatic 指令
Parent.dosth();
//如果是访问常量,会到常量池中获取,不会触发类加载
System.out.println(Parent.k);
}
}
class Parent{
static int i = 3;
static final int k = 3;
static {
System.out.println("Parent类被加载");
}
public static void dosth() {
}
}
//1.Class.forname("...")
try {
Class.forName("com.apesource.demo3.Parent");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//仅获取Class对象,不会被加载
Class cls = Parent.class;//
//使用newInstance() ,会被加载
cls.newInstance();
public class Demo02 {
public static void main(String[] args){
Son son = new Son();
}
}
class Father{
static{
System.out.println("father类被加载");
}
}
class Son extends Father{
static {
System.out.println("son类被加载");
}
}
interface A{
default void dosth() {
System.out.println("实现了该接口的实现类被加载之前会先加载该接口");
}
}
public class Demo02 {
static{
System.out.println("Demo02被加载");
}
public static void main(String[] args){
}
}
除主动引用之外,所有引用类的方式都不会触发加载,称为被动引用。
通过子类引用父类的静态字段,不会导致子类加载。
通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的加载。
//被动引用,不会被加载
public class Demo02 {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
//1.通过子类调用父类的静态变量,只会触发父类的加载,不会触发子类的类加载
System.out.println(Son.value);
//2.通过类定义的数组,不会触发类加载
Father[] array = new Father[16];
System.out.println(array);
//3.访问类的静态常量,不会触发类加载
System.out.println(Father.i);
}
}
class Father{
static final int i = 3;
static int value = 3;
static{
System.out.println("father类被加载");
}
}
class Son extends Father{
static {
System.out.println("son类被加载");
}
}
在Java中,类加载机制与类加载器(ClassLoader)密切相关。类加载器负责加载类文件并构建类的表示。
启动类加载器(Bootstrap ClassLoader)
该类加载器负责将存放在
扩展类加载器(Extension ClassLoader)
该类加载器负责将存放在
应用程序类加载器(Application ClassLoader)
该类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现,负责加载自定义类或第三方jar包。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序是由三种类加载器互相配合,从而实现类加载,除此之外还可以加入自己定义的类加载器。
类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
例如:java.lang.Object 存放在 rt.jar 中,如果我们在类路径ClassPath下也编写一个java.lang.Object,程序可以编译通过,但是由于双亲委派模型的存在,在 rt.jar 中被启动类加载器加载的 Object 比在 ClassPath 中被应用程序类加载器加载的 Object 优先级更高,那么程序中使用的所有的 Object 都是由启动类加载器所加载的 Object。
以上是Java对一个类加载的过程,到现在位置,我们只是将Class类加载的流程熟悉了一边,但是仅仅将类加载到初始化的完成,也只是类声明周期的前五步,在类使用的过程出,除了静态资源访问、反射操作之外,还有一个关键的使用,就是对象的实例化,也就是对象的创建过程:
虚拟机遇到一条 new 指令时,首先检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。内存分配的查找方式有 “指针碰撞” 和 “空闲列表” 两种。
指针碰撞:
空闲列表:
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java 程序的视角来看,对象创建才刚开始,init 构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 init构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来。
Java的类加载机制是Java虚拟机的一个重要组成部分,它负责将类加载到内存中,并确保类的正确性。了解这个机制有助于开发者更好地理解Java程序的运行方式,并能够更好地应对类加载相关的问题。