在 Java 编程中,当使用new关键字创建子类对象时,静态方法和构造方法的执行顺序常常让开发者感到困惑。而这一执行顺序,又与 Java 的类加载机制紧密相连。深入理解这些内容,对于写出高效、稳定的 Java 代码至关重要。接下来,我们就一同揭开其中的奥秘。
一、类加载器详解
1.1 类加载器是什么
类加载器是 Java 虚拟机中负责将.class文件加载到内存中,并对类进行初始化的组件。它如同 Java 程序的 “搬运工”,在程序运行过程中,将存储在磁盘上的字节码文件加载到 JVM 内存中,使 Java 程序能够访问和使用这些类。
1.2 类加载时机
类加载并不是在程序启动时就一次性完成所有类的加载,而是遵循 “用到的时候即创建、调用、访问的时候” 这一原则。具体来说,当出现以下几种情况时,会触发类的加载:
- 当创建类的实例(使用new关键字)时;
- 当调用类的静态方法或访问类的静态变量时;
- 当子类被加载时,其父类也会被加载;
- 当使用反射机制对类进行操作时;
- 当虚拟机启动时,会先加载包含main方法的类。
1.3 类加载过程
类加载过程主要分为三个阶段:加载、链接和初始化。
【加载】
- 定位类:通过包名和类名获取类,在文件系统或其他资源位置中找到对应的.class文件,准备使用输入流进行读取。
- 加载到内存:将.class文件的字节码数据读取到 JVM 内存中。
- 创建 Class 对象:加载完毕后,在内存中创建一个Class对象,该对象作为程序访问类的各种信息(如类的字段、方法等)的入口。
【链接】(验证、准备、解析)
- 验证:确保加载的字节码文件符合 Java 虚拟机规范,检查文件格式是否正确、字节码指令是否合法、是否存在安全隐患等。例如,验证类的继承关系是否合理,方法的访问权限是否正确等。如果验证不通过,JVM 会抛出相应的错误,阻止类的进一步加载。
- 准备:为类的静态变量分配内存,并赋默认值。需要注意的是,这里只是分配内存和赋默认值,如int类型的静态变量默认值为0,boolean类型默认值为false,而不是执行变量的初始化语句。例如,对于static int num = 10;,在准备阶段,num的值为0,而不是10。
- 解析:将编译过程中使用的符号引用替换为直接引用。符号引用是在编译时,对于类、方法、字段等的引用使用的一种符号表示,而直接引用是指向目标的指针、偏移量或句柄等。例如,在编译时,类中对其他类的方法调用使用的是符号引用,在解析阶段,会根据类加载器找到对应的类,并将符号引用替换为实际的方法地址,以便在运行时能够正确调用方法。
【初始化】
在初始化阶段,按照程序的需要对类的静态变量进行个性化赋值,执行静态代码块。静态代码块和静态变量的赋值语句会按照在代码中出现的顺序依次执行。例如:
public class Example {
static int num = 10;
static {
num = 20;
}
}
在上述代码中,先执行num = 10,再执行num = 20,最终num的值为20。
1.4 类加载器分类
Java 中的类加载器主要分为以下几类:
- 启动类加载器(Bootstrap ClassLoader):虚拟机内置的加载器,是所有类加载器的祖先。它负责加载 Java 核心类库,如java.lang、java.util等包下的类,这些类位于%JAVA_HOME%/lib目录下,或者被-Xbootclasspath参数指定的路径中。由于它是由 C++ 实现的,在 Java 代码中无法直接获取。
- 平台类加载器(Platform ClassLoader,在 Java 9 + 中):用于加载 JDK 模块,这些模块包含了 Java 平台的一些扩展功能。它是系统类加载器的父类,可以通过ClassLoader.getPlatformClassLoader()方法获取。
- 系统类加载器(System ClassLoader 或 AppClassLoader):负责加载用户指定的类库,即应用程序类路径(classpath)下的类。在 Java 程序中,可以通过ClassLoader.getSystemClassLoader()方法获取系统类加载器。一般我们编写的自定义类和引用的第三方 jar 包中的类,都是由系统类加载器加载的。
1.5 双亲委派模型
双亲委派模型是 Java 类加载器的工作模式,其工作流程如下:当一个类加载器收到类加载请求时,它不会立即自己去尝试加载这个类,而是先把请求委托给父类加载器,父类加载器再委托给它的父类加载器,以此类推,直到委托给顶层的启动类加载器。只有当父类加载器无法完成加载任务时(即在它的加载路径中找不到对应的类),子加载器才会尝试自己去加载。这种模型保证了 Java 核心类库的安全性和唯一性,避免了不同类加载器重复加载相同的核心类。例如,无论在多少个应用程序中,java.lang.Object类始终是由启动类加载器加载的,不会出现多个不同版本的Object类。
1.6 获取类加载器常用方法
在 Java 中,可以通过以下方法获取不同的类加载器:
- 获取平台类加载器:ClassLoader platformClassLoader = ClassLoader.getPlatformClassLoader();
- 获取系统类加载器:ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
- 获取系统类加载器的父类(即平台类加载器):ClassLoader parentClassLoader = systemClassLoader.getParent();
- 由于启动类加载器是由 C++ 实现的,在 Java 代码中无法直接获取,但是可以通过systemClassLoader.getParent().getParent()尝试获取,不过返回值通常为null。
二、new 创建子类对象时静态方法和构造方法的执行顺序
2.1 父类静态代码块执行
当使用new关键字创建子类对象时,首先会触发类的加载过程。如果父类尚未被加载,JVM 会先加载父类。在父类加载的初始化阶段,父类的静态代码块会被执行,并且仅在父类第一次加载时执行一次。静态代码块常用于对类的静态资源进行初始化,例如初始化静态集合、加载配置文件等。
class Parent {
static {
System.out.println("父类静态代码块执行");
}
}
2.2 子类静态代码块执行
父类加载完成后,接着会加载子类。在子类的初始化阶段,子类的静态代码块会被执行,同样仅在子类第一次加载时执行一次。
class Child extends Parent {
static {
System.out.println("子类静态代码块执行");
}
}
2.3 父类构造方法执行
在子类对象创建过程中,会先调用父类的构造方法。这是因为子类继承了父类的属性和方法,需要先完成父类对象的初始化,才能正确初始化子类对象。父类构造方法的执行顺序遵循构造方法的调用规则,从父类的构造方法开始,逐层向上调用,直到最顶层的父类构造方法执行完毕。
class Parent {
public Parent() {
System.out.println("父类构造方法执行");
}
}
2.4 子类构造方法执行
当父类构造方法执行完毕后,才会执行子类的构造方法,完成子类对象的实例化过程。子类构造方法会对自身特有的属性进行初始化,从而创建出完整的子类对象。
class Child extends Parent {
public Child() {
System.out.println("子类构造方法执行");
}
}
通过以下代码进行测试:
public class Main {
public static void main(String[] args) {
new Child();
}
}
执行结果为:
父类静态代码块执行
子类静态代码块执行
父类构造方法执行
子类构造方法执行
三、博主总结
- 类加载器(是什么?什么时候用?过程?分类?模型?方法?)
- 类加载器是什么:负责将.calss文件加载到内存中并进行初始化。
- 类加载时机:用到的时候即创建、调用、访问的时候。
- 类加载过程:
- 【加载】
- 通过包名和类名获取类,准备用流进行调用。
- 把这个类加载到内存中。
- 加载完毕创建一个calss对象。
- 【链接】(验证、准备、解析)
- 验证:查看是否符合虚拟机规范,是否有安全隐患。
- 准备:对静态变量赋默认值。
- 解析:解析在编译过程中引用的其他类,并将获取到的信息替换之前用符号代替的类信息。
- 【初始化】
- 按照成需要需要对所有变量进行个性化赋值。
- 【加载】
- 类加载器分类:
- 启动类加载器:虚拟机内置的加载器。
- 平台类加载器:加载JDK模块。
- 系统类加载器:加载用户指定的类库。
- 双亲委派模型:自定义启动器》系统类加载器》平台类加载器》启动类加载器
- 获取类加载器常用方法:
- 启动类加载器 == 系统类加载器.getParent().getParent()。
-
- 平台类加载器 == 系统类加载器.getParent() 。
- new创建子类对象时静态方法和构造方法的执行顺序?(关于类加载器)
- 首先执行父类的静态代码块(仅在类第一次加载时执行)。
- 接着执行子类的静态代码块(仅在类第一次加载时执行)。
- 再执行父类的构造方法。
- 最后执行子类的构造方法。