文章目录
- JVM类加载机制
-
- 一、类加载的核心定义(通俗理解)
-
- [1. 基本概念](#1. 基本概念)
- [2. 通俗比喻](#2. 通俗比喻)
- [3. 与JVM运行时数据区域的关联(核心衔接)](#3. 与JVM运行时数据区域的关联(核心衔接))
- 二、类加载的时机:何时触发类加载?
-
- [1. 触发类加载(初始化)的6种主动引用场景(JVM规范强制要求)](#1. 触发类加载(初始化)的6种主动引用场景(JVM规范强制要求))
- [2. 不触发类初始化的3种常见被动引用场景(新手易混)](#2. 不触发类初始化的3种常见被动引用场景(新手易混))
- [3. 代码示例:验证主动/被动引用(直观理解)](#3. 代码示例:验证主动/被动引用(直观理解))
- 三、类加载的核心过程:加载→链接→初始化(三步五阶段)
-
- 总览:三步五阶段核心职责
- [1. 加载(Load):读取文件,生成Class对象](#1. 加载(Load):读取文件,生成Class对象)
- [2. 链接(Link):验证+准备+解析,让类"可用"](#2. 链接(Link):验证+准备+解析,让类“可用”)
- [3. 初始化(Initialize):执行静态代码,赋实际值](#3. 初始化(Initialize):执行静态代码,赋实际值)
- 四、类加载器(ClassLoader):类加载的"执行者"
-
- [1. 类加载器的核心特性](#1. 类加载器的核心特性)
- [2. JVM的三层内置类加载器(核心)](#2. JVM的三层内置类加载器(核心))
- [3. 自定义类加载器](#3. 自定义类加载器)
- [4. 代码示例:获取类的加载器](#4. 代码示例:获取类的加载器)
- 五、双亲委派模型:类加载器的核心设计原则
-
- [1. 双亲委派模型的核心定义](#1. 双亲委派模型的核心定义)
- [2. 通俗比喻:公司的"审批流程"](#2. 通俗比喻:公司的“审批流程”)
- [3. 双亲委派模型的执行流程(图文版)](#3. 双亲委派模型的执行流程(图文版))
- [4. 双亲委派模型的核心优势](#4. 双亲委派模型的核心优势)
- [5. 双亲委派模型的"破坏"场景](#5. 双亲委派模型的“破坏”场景)
- 六、类的完整生命周期
- 七、面试高频考点与新手易混点
-
- [1. 面试高频考点(必背)](#1. 面试高频考点(必背))
- [2. 新手易混点(核心区分)](#2. 新手易混点(核心区分))
-
- [(1)类加载 vs 对象实例化](#(1)类加载 vs 对象实例化)
- [(2)准备阶段 vs 初始化阶段](#(2)准备阶段 vs 初始化阶段)
- [(3)类加载器的"父子关系" vs 继承关系](#(3)类加载器的“父子关系” vs 继承关系)
- [(4)启动类加载器 vs 其他类加载器](#(4)启动类加载器 vs 其他类加载器)
- 八、核心总结
重复了,上次已经学过了,这里主要是去看看书。
JVM类加载机制
类加载是JVM的核心底层机制,也是衔接磁盘.class字节码文件 与JVM运行时数据区域 的关键桥梁------简单说,类加载就是JVM将磁盘上的.class文件(二进制字节码)加载到内存,完成类元数据解析、存储,并生成可被程序使用的Class对象的全过程。其最终结果是:类的元数据存入方法区(元空间),类的Class对象存入堆内存,这也是后续创建对象、调用静态方法/变量的基础,和之前讲的JVM运行时数据区域强关联。
一、类加载的核心定义(通俗理解)
1. 基本概念
类加载机制:JVM通过类加载器,将编译后的.class二进制字节码文件(磁盘/网络/内存)加载到JVM内存,经过验证、准备、解析、初始化等步骤,最终在方法区存储类的元数据,在堆中生成对应java.lang.Class对象的过程。
- 加载主体:类加载器(ClassLoader),是JVM实现类加载的具体组件;
- 核心结果:① 类元数据(类名、父类、方法/字段定义、静态变量等)→ 方法区(元空间);② 代表该类的
Class对象 → 堆内存(作为程序访问方法区类元数据的"入口"); - 核心特点:懒加载(延迟加载) ,JVM不会在启动时加载所有类,而是在程序首次主动使用该类时,才触发类加载(节省内存)。
2. 通俗比喻
把类加载比作**"图书馆引入新书的全过程"**:
- 磁盘
.class文件 → 图书馆未入库的新书; - 类加载器 → 图书馆管理员;
- 方法区(元空间) → 图书馆的藏书目录(存储书籍基本信息:书名、作者、分类、页码等);
- 堆中
Class对象 → 这本书的借阅索引卡(程序通过索引卡,就能查到藏书目录的详细信息,进而"使用"这本书); - 类加载的各个步骤 → 管理员验书(验证)、贴书号(准备)、录入目录(解析)、盖章入库(初始化)。
3. 与JVM运行时数据区域的关联(核心衔接)
类加载是之前讲的方法区、堆的"数据来源"之一,核心关联如下:
- 类加载的准备阶段 :为静态变量分配内存并赋默认值 → 内存位置是方法区(元空间);
- 类加载的初始化阶段:为静态变量赋实际值、执行静态代码块 → 操作的是方法区中的静态变量;
- 类加载完成后:类元数据永久存储在方法区,直到类被卸载;堆中的
Class对象作为访问入口,被程序引用。
一句话总结 :没有类加载,方法区就没有类元数据,堆也无法创建该类的对象实例,程序所有的new操作、静态方法调用都会失效。
二、类加载的时机:何时触发类加载?
JVM规范严格定义了6种主动引用场景 ,只有当程序首次主动使用 某个类时,才会触发类的加载+链接+初始化;除此之外的被动引用,不会触发类的初始化(可能触发加载/链接,但不会执行初始化)。
1. 触发类加载(初始化)的6种主动引用场景(JVM规范强制要求)
这是面试高频考点,必须熟记,核心是"首次、主动、使用"三个关键词:
- 当通过
new关键字创建类的实例时(如new User()); - 调用类的静态变量 (非final常量)或静态方法 时(如
User.count、User.sayHello()); - 通过反射机制访问类时(如
Class.forName("com.example.User")); - 初始化某个类的子类时(子类首次主动使用,会先触发父类的初始化);
- JVM启动时,执行主类(包含main方法的类) (如
java Demo,会先初始化Demo类); - 当使用JDK 1.7及以上的动态语言支持时,方法句柄对应的类首次被使用时。
核心规则 :父类优先于子类初始化,接口不遵循此规则(接口初始化时,不会触发父接口的初始化,只有当使用父接口的静态变量时,才会初始化)。
2. 不触发类初始化的3种常见被动引用场景(新手易混)
被动引用仅可能触发类的加载/链接 ,但不会执行初始化(即不会执行静态代码块、不会给静态变量赋实际值),常见场景:
- 子类引用父类的静态变量:仅初始化父类,子类不会被初始化;
- 通过数组创建类的引用 :如
User[] users = new User[10],仅创建数组对象,不会初始化User类; - 引用类的final常量:编译期常量会被存入运行时常量池,引用时直接取常量池的值,不会初始化类。
3. 代码示例:验证主动/被动引用(直观理解)
java
// 父类:用于验证父类优先初始化
class Parent {
// 静态变量:准备阶段赋默认值0,初始化阶段赋实际值10
public static int parentNum = 10;
// 静态代码块:初始化阶段执行,用于标记是否初始化
static {
System.out.println("Parent类 初始化");
}
}
// 子类
class Child extends Parent {
public static int childNum = 20;
static {
System.out.println("Child类 初始化");
}
}
// 测试类:主类
public class ClassLoadTriggerDemo {
// 编译期final常量:存入运行时常量池
public static final String CONST_STR = "hello";
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("===== 场景1:子类引用父类静态变量(被动引用) =====");
System.out.println(Child.parentNum); // 仅初始化Parent,不初始化Child
System.out.println("===== 场景2:数组创建类引用(被动引用) =====");
Parent[] parents = new Parent[5]; // 不初始化Parent
System.out.println("===== 场景3:引用final常量(被动引用) =====");
System.out.println(ClassLoadTriggerDemo.CONST_STR); // 不初始化当前类
System.out.println("===== 场景4:new创建实例(主动引用) =====");
Child child = new Child(); // 触发Child初始化(先初始化Parent)
System.out.println("===== 场景5:反射访问(主动引用) =====");
Class.forName("com.example.Child"); // 若未初始化,触发初始化
}
}
运行结果:
===== 场景1:子类引用父类静态变量(被动引用) =====
Parent类 初始化
10
===== 场景2:数组创建类引用(被动引用) =====
===== 场景3:引用final常量(被动引用) =====
hello
===== 场景4:new创建实例(主动引用) =====
Child类 初始化
20
===== 场景5:反射访问(主动引用) =====
(无输出,因为Child已在场景4初始化)
核心结论 :只有主动引用才会触发类的初始化,被动引用不会;且子类初始化时,父类一定会先完成初始化。
三、类加载的核心过程:加载→链接→初始化(三步五阶段)
JVM将类从加载到可用,分为三大步骤 ,其中链接步骤 又细分为验证、准备、解析 三个阶段,合称为类加载的"三步五阶段" 。这五个阶段按固定顺序执行(解析阶段可在初始化阶段后执行,为了支持动态绑定),全程由类加载器协调完成。
整体流程:加载(Load)→ 验证(Verify)→ 准备(Prepare)→ 解析(Resolve)→ 初始化(Initialize)
总览:三步五阶段核心职责
| 大步骤 | 子阶段 | 核心职责(通俗解释) | 操作的内存区域 |
|---|---|---|---|
| 加载 | - | 读取.class文件,生成Class对象 | 磁盘→堆(Class对象)、方法区(临时元数据) |
| 链接 | 验证 | 校验.class文件的合法性(防止篡改、格式错误) | 方法区 |
| 链接 | 准备 | 为静态变量分配内存,赋默认值 | 方法区(静态变量) |
| 链接 | 解析 | 将符号引用转为直接引用(找实际内存地址) | 方法区(运行时常量池) |
| 初始化 | - | 执行静态代码块,为静态变量赋实际值 | 方法区(静态变量) |
1. 加载(Load):读取文件,生成Class对象
核心工作 :类加载器根据类的全限定名 (如com.example.User),找到对应的.class二进制字节码文件(来源:磁盘、网络、内存、动态生成),将其读取到JVM内存,然后在堆中生成一个代表该类的java.lang.Class对象,并将类的初步元数据存入方法区。
- 关键操作1:查找.class文件:不同类加载器有不同的查找范围(后续讲类加载器时详细说);
- 关键操作2:生成Class对象 :这是程序访问类元数据的唯一入口,每个类在JVM中只有一个Class对象(保证类的唯一性);
- 核心特点:加载阶段是类加载器的核心工作阶段,后续的链接、初始化阶段由JVM统一完成,与类加载器无关。
2. 链接(Link):验证+准备+解析,让类"可用"
链接阶段的核心目标:将加载阶段读取的类元数据,进行合法性校验、内存分配、引用解析,确保该类能被JVM正确使用,分为三个子阶段。
(1)验证(Verify):.class文件的"安检"
核心工作 :JVM对加载的.class文件进行严格的合法性校验,防止恶意篡改、格式错误的.class文件被加载,保证JVM的运行安全。
- 校验内容:文件格式验证(是否符合.class文件规范)、元数据验证(类的继承关系、字段/方法定义是否合法)、字节码验证(字节码指令是否合法,防止无限循环、栈溢出)、符号引用验证(符号引用的目标是否存在);
- 核心作用:保证JVM安全 ,若验证失败,会抛出
java.lang.VerifyError异常,类加载终止。
(2)准备(Prepare):为静态变量分配内存,赋默认值
核心工作 :JVM在方法区 为类的静态变量(static修饰) 分配内存空间,并为其赋JVM默认值(而非代码中定义的实际值)。
- 关键要点1:仅处理静态变量,实例变量不会在该阶段处理(实例变量在创建对象时,在堆中分配内存);
- 关键要点2:赋默认值,而非实际值:默认值是JVM为各数据类型定义的初始值(如int→0、String→null、boolean→false);
- 关键要点3:final静态常量特殊处理 :被
static final修饰的常量,在编译期 就会被赋实际值,存入类文件常量池,准备阶段会直接赋实际值(而非默认值)。
代码示例:准备阶段的默认值
java
class PrepareDemo {
public static int a = 10; // 准备阶段:分配内存,赋默认值0;初始化阶段:赋实际值10
public static String str; // 准备阶段:分配内存,赋默认值null;初始化阶段:若有赋值则赋实际值
public static final int b = 20; // 编译期常量,准备阶段直接赋实际值20(非0)
}
(3)解析(Resolve):符号引用→直接引用
核心工作 :JVM将方法区运行时常量池 中的符号引用 ,转换为直接引用(即目标的实际内存地址)。
- 符号引用:之前讲运行时常量池时提到过,是类/方法/字段的"间接标识"(如方法名
add(int,int)、类名com.example.User),存储在运行时常量池; - 直接引用:是类/方法/字段在JVM内存中的实际内存地址(或指向地址的指针);
- 解析对象:类、接口、字段、方法、方法句柄等;
- 核心特点:解析阶段可延迟执行 :JVM规范允许解析阶段在初始化阶段之后 执行(为了支持动态绑定,如多态中的方法调用,运行时才知道实际调用的子类方法)。
3. 初始化(Initialize):执行静态代码,赋实际值
核心工作 :JVM执行类的静态代码块(static{}) ,并为静态变量赋代码中定义的实际值 ,这是类加载过程中唯一由程序员编写的代码执行阶段。
- 执行顺序:① 先执行父类的初始化(递归执行,直到Object类);② 再执行当前类的静态变量赋值语句;③ 最后执行当前类的静态代码块;
- 核心规则:初始化阶段仅执行一次 :每个类在JVM的生命周期中,只会被初始化一次(保证静态变量只被赋值一次,静态代码块只执行一次);
- 触发条件:只有主动引用才会触发初始化(之前讲的6种场景);
- 异常处理:若初始化阶段执行静态代码块/赋值语句时抛出异常,类加载会终止,该类变为不可用状态 ,后续任何使用该类的操作都会抛出
java.lang.NoClassDefFoundError异常。
代码示例:初始化阶段的执行顺序
java
// 父类
class ParentInit {
public static int parentA = 10;
static {
System.out.println("ParentInit 静态代码块执行");
parentA = 20;
}
}
// 子类
class ChildInit extends ParentInit {
public static int childA = parentA + 10;
static {
System.out.println("ChildInit 静态代码块执行");
childA = 40;
}
}
// 测试类
public class InitializeDemo {
public static void main(String[] args) {
// 主动引用:调用子类静态变量,触发初始化
System.out.println(ChildInit.childA);
}
}
运行结果:
ParentInit 静态代码块执行
ChildInit 静态代码块执行
40
执行流程解析:
- 调用
ChildInit.childA,触发子类初始化,先递归初始化父类ParentInit; - 父类初始化:先为
parentA赋实际值10 → 执行父类静态代码块,将parentA改为20; - 子类初始化:先为
childA赋实际值20+10=30→ 执行子类静态代码块,将childA改为40; - 最终输出
childA的实际值40。
四、类加载器(ClassLoader):类加载的"执行者"
类加载器是实现加载阶段 的具体组件,其核心职责是:根据类的全限定名 查找并读取.class文件,生成Class对象。JVM提供了三层内置类加载器 ,并支持开发者实现自定义类加载器,共同构成类加载器的体系结构。
1. 类加载器的核心特性
- 双亲委派模型:这是JVM类加载器的核心设计原则(后续详细讲),保证类的唯一性;
- 类的唯一性 :同一个类,由不同的类加载器加载,在JVM中会被视为两个不同的类(即使.class文件完全相同);
- 沙箱安全 :通过双亲委派模型,防止自定义类加载器篡改JVM核心类(如
java.lang.String); - 可扩展性 :开发者可通过继承
java.lang.ClassLoader,实现自定义类加载器,加载非磁盘来源的.class文件(如网络、内存、加密文件)。
2. JVM的三层内置类加载器(核心)
JVM默认提供三层内置类加载器,按加载范围从核心到应用 划分,每层加载器有固定的加载路径,互不重叠。
| 类加载器名称 | 英文名称 | 核心职责(加载范围) | 实现类 | 是否继承ClassLoader |
|---|---|---|---|---|
| 启动类加载器 | Bootstrap ClassLoader | 加载JVM核心类库(JAVA_HOME/jre/lib下的rt.jar、charsets.jar等,如java.lang、java.util包) |
C/C++实现(JVM内部) | 否(不是Java类) |
| 扩展类加载器 | Extension ClassLoader | 加载JVM扩展类库(JAVA_HOME/jre/lib/ext下的jar包,或java.ext.dirs系统属性指定的路径) |
sun.misc.Launcher$ExtClassLoader |
是 |
| 应用程序类加载器(系统类加载器) | Application ClassLoader | 加载应用程序的类 (项目src/main/java下的类、第三方依赖包(maven/gradle)、classpath环境变量指定的路径) |
sun.misc.Launcher$AppClassLoader |
是 |
3. 自定义类加载器
开发者通过继承java.lang.ClassLoader类 ,重写其findClass()方法(推荐)或loadClass()方法(不推荐,会破坏双亲委派模型),实现自定义的类加载逻辑。
- 核心使用场景:加载非磁盘来源的.class文件(如网络下载的加密.class文件、内存中动态生成的.class文件)、实现类的热部署(如Tomcat的WebAppClassLoader)、隔离不同模块的类(如微服务中的类隔离);
- 核心原则:重写
findClass()方法,而非loadClass()方法(loadClass()是实现双亲委派模型的核心方法,重写会破坏模型)。
4. 代码示例:获取类的加载器
java
public class ClassLoaderDemo {
public static void main(String[] args) {
// 1. 获取应用程序类加载器(加载当前自定义类)
ClassLoader appClassLoader = ClassLoaderDemo.class.getClassLoader();
System.out.println("当前类的类加载器:" + appClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 2. 获取扩展类加载器(应用程序类加载器的父类)
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("扩展类加载器:" + extClassLoader); // sun.misc.Launcher$ExtClassLoader@1b6d3586
// 3. 获取启动类加载器(扩展类加载器的父类)
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("启动类加载器:" + bootstrapClassLoader); // null(C/C++实现,无Java对象)
// 4. 启动类加载器加载的核心类(如String),getClassLoader()返回null
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println("String类的类加载器:" + stringClassLoader); // null
}
}
核心结论:
- 自定义类由应用程序类加载器加载;
- JVM核心类(如String)由启动类加载器 加载,其
getClassLoader()返回null(因为启动类加载器不是Java类); - 类加载器的父子关系 :应用程序类加载器 → 扩展类加载器 → 启动类加载器(注意:是逻辑上的父子关系 ,并非继承关系,而是通过
getParent()方法关联)。
五、双亲委派模型:类加载器的核心设计原则
双亲委派模型 是JVM类加载器的核心设计原则 ,也是面试最高频的考点之一。它定义了类加载器在加载类时的查找顺序 :先向上委托,再向下查找 ,简单说就是"孩子找爹,爹找爷爷,爷爷找不到,再自己找"。
1. 双亲委派模型的核心定义
当某个类加载器收到类加载的请求时,它不会立即自己去查找加载 ,而是先将该请求委托给其父类加载器 去执行;父类加载器收到请求后,也会继续向上委托,直到委托给顶层的启动类加载器 ;只有当父类加载器在自己的加载范围内找不到该类 时,才会将请求返回给子类加载器,由子类加载器自己去查找加载。
2. 通俗比喻:公司的"审批流程"
把类加载请求比作员工申请经费 ,类加载器比作员工→部门经理→总经理→董事长(父子关系):
- 员工(应用程序类加载器)收到申请,先交给部门经理(扩展类加载器)审批;
- 部门经理交给总经理(启动类加载器)审批;
- 董事长(启动类加载器)在自己的权限范围内(核心类库)查看,若能审批(找到类),则直接处理;若不能,返回给总经理;
- 总经理(扩展类加载器)在自己的权限范围内(扩展类库)查看,若能处理则处理,否则返回给部门经理;
- 部门经理(应用程序类加载器)在自己的权限范围内(应用类)查看,若能处理则处理,否则抛出
ClassNotFoundException异常。
3. 双亲委派模型的执行流程(图文版)
以加载自定义类com.example.User为例,流程如下:
1. 应用程序类加载器收到加载请求 → 委托给父类(扩展类加载器)
2. 扩展类加载器收到请求 → 委托给父类(启动类加载器)
3. 启动类加载器:在JRE/lib下查找,未找到User类 → 返回给扩展类加载器
4. 扩展类加载器:在JRE/lib/ext下查找,未找到User类 → 返回给应用程序类加载器
5. 应用程序类加载器:在classpath下查找,找到User.class → 加载该类,生成Class对象
4. 双亲委派模型的核心优势
(1)保证类的唯一性
同一个类,在JVM中只会被加载一次。因为所有类加载请求都会先委托给顶层的启动类加载器,只有父类加载器找不到时,子类加载器才会自己加载,避免了同一个类被多个类加载器重复加载。
- 示例:若自定义了一个
java.lang.String类,应用程序类加载器收到加载请求后,会委托给启动类加载器,而启动类加载器已经加载了核心的java.lang.String类,因此自定义的String类不会被加载,保证了核心类的唯一性。
(2)保证JVM的沙箱安全
防止恶意代码篡改JVM核心类库。比如,开发者无法自定义一个java.lang.String类来替代JVM的核心String类,因为双亲委派模型会让启动类加载器先加载核心的String类,自定义的String类永远不会被加载。
(3)简化类加载器的设计
每个类加载器只需关注自己的加载范围,无需关心其他加载器的逻辑,降低了类加载器的实现复杂度。
5. 双亲委派模型的"破坏"场景
双亲委派模型是JVM的推荐设计原则 ,并非强制要求,在某些场景下,需要破坏双亲委派模型才能实现特定功能,常见场景:
- SPI机制(服务提供者接口) :如JDBC、JNDI等,核心类由启动类加载器加载,而实现类由应用程序类加载器加载,启动类加载器无法加载实现类,因此需要通过线程上下文类加载器(Thread Context ClassLoader)打破双亲委派;
- 类的热部署:如Tomcat的WebAppClassLoader,每个Web应用有自己的类加载器,需要加载当前应用的类,而不委托给父类加载器,否则多个Web应用的类会冲突;
- 自定义类加载器 :若开发者重写了类加载器的
loadClass()方法,会直接破坏双亲委派模型的执行流程。
六、类的完整生命周期
类加载的"三步五阶段"只是类生命周期的前半部分 ,一个类从被加载到被卸载,完整的生命周期分为7个阶段 ,其中加载、链接、初始化、使用、卸载 是按顺序执行的,解析阶段可在初始化阶段后执行(动态绑定)。
类的完整生命周期 :加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
- 加载/链接/初始化:前面已详细讲解,是类的"初始化阶段";
- 使用:程序通过创建类的实例、调用静态方法/变量、反射访问等方式,使用该类;
- 卸载 :当该类满足卸载条件时,JVM会将其从方法区中移除(类元数据被回收),堆中的Class对象也会被GC回收,该类的生命周期结束。
类的卸载条件(严格,一般很少触发)
JVM对类的卸载有严格的要求,只有当以下三个条件同时满足时,类才会被卸载:
- 该类的所有实例对象都已被GC回收(堆中无该类的任何实例);
- 该类的Class对象已被GC回收(无任何程序引用该Class对象);
- 加载该类的类加载器 已被GC回收(自定义类加载器才会满足,内置类加载器随JVM启动而存在,随JVM关闭而销毁,因此内置类加载器加载的类永远不会被卸载)。
核心结论 :JVM核心类库(如java.lang.String)由启动类加载器加载,永远不会被卸载;应用程序类加载器/扩展类加载器加载的类,一般也不会被卸载;只有自定义类加载器加载的类,才有可能满足卸载条件,被JVM卸载。
七、面试高频考点与新手易混点
1. 面试高频考点(必背)
- 类加载的三步五阶段分别是什么?每个阶段的核心工作?
- 触发类初始化的6种主动引用场景?哪些被动引用不会触发初始化?
- 双亲委派模型的核心原理?优势?哪些场景会破坏双亲委派模型?
- JVM的三层内置类加载器分别是什么?各自的加载范围?
- 准备阶段和初始化阶段的区别?static final常量在哪个阶段赋实际值?
- Class对象存在哪个内存区域?类的元数据存在哪个内存区域?
- 为什么同一个类由不同类加载器加载,会被视为不同的类?
- 类的卸载条件是什么?JVM核心类会被卸载吗?
2. 新手易混点(核心区分)
(1)类加载 vs 对象实例化
- 类加载:加载的是类本身 ,生成Class对象,存储类元数据,是针对类的操作,只执行一次;
- 对象实例化:通过
new关键字,根据类的元数据,在堆中创建对象实例 ,是针对对象的操作,可执行多次; - 关系:先有类加载,后有对象实例化,没有类加载,就无法创建对象实例。
(2)准备阶段 vs 初始化阶段
- 准备阶段:赋默认值,由JVM自动执行,无代码参与;
- 初始化阶段:赋实际值,执行静态代码块,由程序员编写的代码决定;
- 关键区别:static final常量在准备阶段 赋实际值,普通静态变量在初始化阶段赋实际值。
(3)类加载器的"父子关系" vs 继承关系
- 类加载器的父子关系:是逻辑上的关联关系 ,通过
getParent()方法实现,并非继承关系; - 示例:应用程序类加载器的父类是扩展类加载器,但应用程序类加载器并不继承扩展类加载器,而是通过
getParent()方法指向扩展类加载器。
(4)启动类加载器 vs 其他类加载器
- 启动类加载器:C/C++实现,是JVM内部组件,不是Java类,因此
getParent()返回null; - 扩展/应用程序类加载器:Java类,继承
java.lang.ClassLoader,有对应的Java对象。
八、核心总结
- 类加载是JVM将
.class文件加载到内存的过程,最终结果是类元数据存入方法区,Class对象存入堆,是衔接磁盘字节码和JVM运行时数据区域的关键; - 类加载遵循懒加载 ,仅在6种主动引用场景下触发初始化,被动引用不会触发;
- 类加载的核心流程是三步五阶段 ,其中准备阶段 为静态变量分配内存赋默认值,初始化阶段执行静态代码块赋实际值,是唯一执行程序员代码的阶段;
- JVM提供三层内置类加载器 ,遵循双亲委派模型 (先向上委托,再向下查找),核心优势是保证类的唯一性和JVM安全;
- 类的完整生命周期包含7个阶段,卸载条件严格,只有自定义类加载器加载的类才有可能被卸载,JVM核心类永远不会被卸载;
- 类加载是JVM底层的核心机制,也是理解内存区域、GC、反射、动态代理的基础,和之前讲的方法区、堆、运行时常量池强关联,需结合理解。