背景
哈喽艾瑞宝迪,看完这篇文章,你将会详细掌握Java类加载的过程中。某个类首次调用后,执行的详细流程。
不是教你背八股,而是教你背诵呸,掌握熟悉之后的,学以致用。
ps: 为啥不说Kotlin?Kotlin 代码在编译后会生成与 Java 兼容的中间字节码,遵循和Java一样的类加载流程。
抛出问题1,以下代码每一行的执行顺序是?
这里有个小坑哈,想知道答案,除了要理解类加载,还需要理解对象实例化,被标题类加载骗进来,带你多学一个应该不会介意吧。
public static void main(String[] args) {
System.out.printf("执行 Num.a \n");
Num.funB();
// String str = Num.a;
}
static class Num {
static String a = "11";
static Num instance = new Num();
{
System.out.println("执行非静态代码块");
}
int v = 10;
static {
System.out.println("执行静态代码块");
}
Num(){
System.out.println("构造器方法");
}
public static void funB(){
System.out.printf("执行funB");
}
}
前置知识
你需要对Java类加载,有一个大致的了解。比如在他加载、验证、准备、解析、初始化、使用和卸载的各个流程中大概会干什么事。
如果你对这些不太了解,可以先去看一下某大佬的这篇文章Java类加载器 — classloader 的原理及应用
不用全懂,啃个脸熟,就可以回来了哈。
类加载规则
-
要理解,静态类加载他是首次调用到该类的时候触发的。因为静态类全局只有一个。 非静态类,new 100个可以有100个。
-
类加载流程会经历多个阶段,其中在准备阶段,会为所有的静态变量设置类型的默认值,例如int 都是0,String ,Object 都是null;
-
静态代码块,只在初始化阶段中执行。
-
在初始化阶段,静态变量和静态代码块是同级的。按照写代码顺序谁在前先执行谁。
-
类加载流程中(初始化阶段),会为静态变量赋值,例如 statis int a = 3; 这个阶段a 才从默认值0变成3。
-
静态调用的方式,例如Num.funB(); 和 Num.a; 会触发类加载流程。
以上规则不是静态变量就是静态代码块,那我要问了,非静态变量(实例变量)和实例代码块什么时候执行,什么时候赋值?
上面已经有暗示了,new Object() ,创建对象的时候。
那有的宝子又要问了,为啥实例对象,不是在首次调用的时候捏?
因为非静态方法和对象的调用,不能直接Object.funA() , Object.value; 要先创建对象Object o = new Object(); 才能调用哇。 那么继续看一下,创建对象的规则,也就是实例化规则
实例化规则
-
new Object() 会触发实例化,调用一次触发一次。
-
实例化和类加载类似也有,各个阶段。
-
先为新的对象分配内存空间,然后 为所有实例变量设置默认值。(此时,实例代码块是不参与的)
-
按代码顺序执行,实例变量的赋值和实例代码块
-
执行构造器。
问题1执行顺序
掌握了,以上规则那问题1的执行就很清晰了。
-
main 函数中Num被首次调用,触发类加载流程
-
准备阶段:为所有静态变量设置默认值,也就是 a = null,instance = null;
-
初始化阶段:按照代码顺序执行静态变量的赋值和静态代码块;
-
static String a = "11"; 给a 赋值为”11“
-
然后执行 static Num instance = new Num(); 要给 instance 赋值,从而触发了new Num()实例化流程。
-
实例化流程,会为实例变量也就是int v = 10; 先设置默认值v = 0;
-
按照代码顺序执行,实例代码块打印日志 {System.out.println("执行非静态代码块");}
-
按照代码顺序执行 v 赋值 10;
-
最后执行构造器方法 Num(){};
-
-
实例化结束后,instance 变成了 new Num()对象。
-
继续顺序执行其他静态变量和静态代码块,也就是 static {System.out.println("执行静态代码块");}
-
最后执行匹配方法 funB()
-
执行main后日志打印如下
执行 Num.a
执行非静态代码块
构造器方法
执行静态代码块
执行funB
扩展
类加载 和 实例化,过程如果有父类,会按照上述规则先执行父亲,在执行自己。
例子栗子🌰
public static void main(String[] args) {
System.out.printf("执行 Num.a \n");
Num.funB();
// String str = Num.a;
}
static class NumParent{
static {
System.out.println("父亲的静态代码块");
}
static String aParent = "父亲A";
int vParent = 9;
NumParent(){
System.out.println("父亲的构造方法");
}
{
System.out.println("父亲的实例代码块");
}
}
static class Num extends NumParent {
static String a = "11";
static Num instance = new Num();
{
System.out.println("执行非静态代码块");
}
int v = 10;
static {
System.out.println("执行静态代码块");
}
Num(){
System.out.println("构造器方法");
}
public static void funB(){
System.out.printf("执行funB");
}
}
以上代码执行顺序如下:
-
Num.funB();首次调用触发类加载,流程。
-
Num存在父NumParent,先执行NumParent类加载。
-
为NumParent所有静态变量设置默认值 aParent = null;
-
顺序执行静态代码块和静态变量的赋值 aParent = "父亲A"。
-
NumParent类加载结束,开始执行Num类加载。
- 为Num所有静态变量设置默认值。
- 顺序执行静态变量和静态代码块,
-
String a 赋值了 “11”
-
顺序执行 static Num instance = new Num(); 开始。触发了实例化,因为Num存在父类,所以优先执行父类实例化
-
开始父类实例化流程。
-
所有实例变量设置默认值。
-
按照代码顺序执行,实例变量的赋值,和实例代码块
-
int vParent 被赋值 9; 然后 打印实例代码块日志。
-
最后执行父类实例化方法。
-
父类实例化流程结束
-
开始当前类(Num)实例化流程
-
所有实例变量设置默认值
-
按代码顺序执行实例变量和实例代码块
-
打印了实例代码块日志,v 被赋值了 10;
-
执行Num 构造器方法。
-
创建了新的对象赋值给了instance
-
-
static Num instance = new Num();结束,继续按照代码顺序执行静态变量和静态代码块
-
执行静态代码块,打印日志 static {System.out.println("执行静态代码块");}
-
-
类加载流程结束,执行最后的匹配方法 funB();
图解
sequenceDiagram
participant M as main()
participant NP as NumParent Class
participant N as Num Class
participant Obj as Num Instance
M->>N: 调用 Num.funB()
Note over N: 触发 Num 类初始化<br/>需先初始化父类
%% 父类初始化
N->>NP: 初始化父类 NumParent
NP->>NP: 准备阶段:静态变量默认值<br/>aParent = null
NP->>NP: 初始化阶段开始
NP->>NP: 执行静态代码块<br/>输出"父亲的静态代码块"
NP->>NP: 静态变量赋值<br/>aParent = "父亲A"
Note over NP: 父类静态初始化完成
%% 子类初始化
NP-->>N: 父类初始化完成
N->>N: 准备阶段:静态变量默认值<br/>a = null, instance = null
N->>N: 初始化阶段开始
N->>N: 静态变量赋值<br/>a = "11"
N->>N: 执行 static Num instance = new Num()<br/>触发实例化
%% 实例化过程
N->>Obj: 创建实例
Note over Obj, NP: 实例化父类部分
Obj->>NP: 父类实例化
NP->>NP: 默认初始化<br/>vParent = 0
NP->>NP: 执行实例代码块<br/>输出"父亲的实例代码块"
NP->>NP: 实例变量赋值<br/>vParent = 9
NP->>NP: 执行构造方法<br/>输出"父亲的构造方法"
Note over NP: 父类实例化完成
Note over Obj, N: 实例化子类部分
Obj->>N: 子类实例化
N->>N: 默认初始化<br/>v = 0
N->>N: 执行实例代码块<br/>输出"执行非静态代码块"
N->>N: 实例变量赋值<br/>v = 10
N->>N: 执行构造方法<br/>输出"构造器方法"
Note over Obj: 实例化完成
%% 继续类初始化
N->>N: 返回实例赋值给 instance
N->>N: 执行静态代码块<br/>输出"执行静态代码块"
Note over N: 类初始化完成
%% 执行静态方法
N-->>M: 类初始化完成
M->>N: 执行 funB()<br/>输出"执行funB"
好的有宝子又要问了
神马我new 一个Num();对象也会触发他父类的初始化方法?那难道创建一个对象占用两个内存吗,一个Num的内存一个,NumParent的内存?
new 一个对象一个内存且是连续的 类似这样
graph LR
A[子类对象内存块] --> B[对象头]
A --> C[父类字段]
A --> D[子类特有字段]
C --> E[Object类字段]
C --> F[直接父类字段]
C --> G[...其他祖先类字段]
D --> H[子类新增字段]
D --> I[子类新增字段]
style A fill:#e6f7ff,stroke:#1890ff,stroke-width:2px
style B fill:#f9f0ff,stroke:#722ed1
style C fill:#f6ffed,stroke:#52c41a
style D fill:#fff2e8,stroke:#fa8c16
你说一个就一个?我要查你手机(>_<)。两个类的构造器方法,加点日志,打印一下你会发现
public static void main(String[] args) {
System.out.println("地址 " + Num.instance);
System.out.println("地址 父 " + (NumParent) Num.instance);
}
\\\省略无关代码
Num(){
System.out.println("构造器方法 地址: " + this);
}
NumParent(){
System.out.println("父亲的构造方法 地址: " + this);
}
日志如下 指向同一个地址50134894,包是一个。
父亲的构造方法 地址: com.example.demo.Test$Num@50134894
构造器方法 地址: com.example.demo.Test$Num@50134894
地址 com.example.demo.Test$Num@50134894
地址 父 com.example.demo.Test$Num@50134894
灵魂拷问,我知道这些有什么用
写hello world 那肯定是不需要知道这些的。但是我们工作的历史长河中,有一天你可能会遇到类似这样的问题。
fun main() {
// 定义商品类
data class Product(val name: String, val price: Double)
abstract class Discount {
var discountRate = 1.0
init {
// 父类初始化时尝试计算折扣
calcDiscount()
}
// 子类实现
abstract fun calcDiscount()
}
class NationalDiscount : Discount() {
private var subsidyRate = 0.85 // 85折
init {
println("设置国补折扣率: 0.85")
}
override fun calcDiscount() {
// 漏洞点:父类初始化时调用,此时 实例变量 subsidyRate尚未初始化 为0
discountRate = subsidyRate
println("折扣计算: $discountRate")
}
}
val iPhone = Product("iPhone 15 Pro", 9.9)
println("购买 ${iPhone.name},应享85折国补...")
// 应用国补折扣
val discount = NationalDiscount()
val finalPrice = iPhone.price * discount.discountRate
println(" 原价: ${iPhone.price} 元")
println(" 实付: $finalPrice 元")
}
运行日志如下 小黑本来想九块九交个朋友盆友的iPhone 15 变成 0元购。
购买 iPhone 15 Pro,应享85折国补...
折扣计算: 0.0
设置国补折扣率: 0.85
原价: 9.9 元
实付: 0.0 元
其他用于,热更新,插件化,动态加载,安全加固。不一一列举了(等我先学一下)
以上内容,若有纰漏,错误,请大佬大刀扶正