20260812-笔记.md 47 KB

20260812 课堂笔记 — 类的加载机制(java文件 编译 → 字节码class文件 → 类加载器 → 内存 → JVM解释 → 二进制机器码 → 执行 + 类加载器作用:将 .class 文件加载到内存(也可能是 jar 包:把 Java/class 文件封装成的可执行程序包)+ 类加载的 6 种时机(创建实例对象 / 调用 static 方法 / 访问或赋值 static 变量 / 反射创建 Class 对象 / 初始化子类 / Java.exe 运行主类,懒加载:使用时才加载不使用不加载)+ 类的加载过程(3 大步 5 小步:加载【全限定名获取字节流 → 转运行时数据结构 → 生成 Class 对象】→ 链接【验证:符合虚拟机规范无安全隐患 / 准备:为 static 类变量分配内存设默认值 / 解析:符号引用替换直接引用】→ 初始化【按主观计划初始化变量与资源】)+ Demo01 执行顺序演示(静态代码块 a=10000 → 代码块 a=100 → 构造方法 a=10:static 初始化属于类加载阶段、代码块与构造方法属于实例化阶段)+ 类加载器(三种类加载器:BootstrapClassLoader 虚拟机内置一般表示为 null 无父加载器 / PlatformClassLoader 平台类加载器负责 JDK 特殊模块 / SystemClassLoader 系统类加载器负责用户类路径类库 + 双亲委派层级 SystemClassLoader 继承 PlatformClassLoader 继承 BootstrapClassLoader)+ Demo02 类加载器演示(ClassLoader.getSystemClassLoader() / getParent() 获取父加载器 / bootstrap 表示为 null、getParent() 抛空指针异常)+ 双亲委派模型(Demo03:收到加载请求不自己先加载、先委派给父类加载器、递归至最顶层、顶层无法完成才由子类加载器自己加载 + 为什么需要双亲委派三个原因:避免类被重复加载(同一个类全 JVM 只有一个 Class 对象)/ 保证核心类库安全(java.lang.String 只能由启动类加载器加载、用户自定义同名类永不加载)/ 保证类的唯一性(避免多份不同 String 类型混乱)+ 用类加载器加载资源配置文件(getResourceAsStream("prop.properties") 加载资源文件 + Properties(父类 Hashtable、Map 接口实现、底层数组)load 解析 + getProperty 读取数据库配置 db.driver/db.url/db.username/db.password))

  • 日期:2026-08-12
  • 项目:c260812
  • 包路径:course
  • 作者:WanJL

目录

  1. 类的加载机制概述:从 .java 到执行的完整流程
  2. 类加载器的作用:把 .class 文件加载到内存
  3. 类加载的时机:什么时候会加载类
  4. 类的加载过程:加载 → 链接(验证/准备/解析)→ 初始化
  5. Demo01 执行顺序演示:静态代码块 → 代码块 → 构造方法
  6. 类加载器:三种类加载器与双亲委派(Demo02)
  7. 双亲委派模型:为什么需要双亲委派(Demo03)
  8. 用类加载器加载资源配置文件:getResourceAsStream + Properties(Demo03 + prop.properties)
  9. 知识点全景总结
  10. 随堂练习要点
  11. 拓展阅读

1. 类的加载机制概述:从 .java 到执行的完整流程

概念 —— Java 程序是如何被执行的

我们写的 .java 源文件并不能直接运行,需要经过 编译 → 类加载 → JVM 解释 → 执行 四个阶段。Java 的"一次编写、到处运行"正依赖于这套机制:.java 编译成与平台无关的 .class 字节码,再由不同平台上的 JVM 解释执行。

// 来源:course/Demo01.java(类注释)
/*
    java文件  -- 编译--> 字节码文件.class文件--解释-->二进制机器码--执行-->

    类加载器
    类加载器的作用就是将就是把.class文件加载到内存中

        java文件  -- 编译--> class文件 --类加载器--> 内存 --> JVM(java虚拟机)--解释--> 二进制机器码--->执行
*/
┌─────────┐   编译   ┌────────────┐   类加载器   ┌────────┐   JVM 解释  ┌────────────────┐
│ .java 源文件 │ ─────► │ .class 字节码 │ ──────────► │  内存   │ ──────────► │  二进制机器码执行  │
└─────────┘         └────────────┘             └────────┘            └────────────────┘
阶段 说明
编译 javac 把 .java 源文件编译成 .class 字节码文件(与平台无关)
类加载 类加载器把 .class 文件加载到 JVM 内存中(本课核心)
解释执行 JVM(java 虚拟机)将字节码解释为二进制机器码交给 CPU 执行

要点:

  • .class 字节码是平台无关的——所以同一份字节码可以在 Windows / Linux / Mac 上运行,这就是 Java 跨平台的原理
  • 类加载器是"搬运工"——它的任务就是把磁盘上的 .class 文件搬到 JVM 内存中

2. 类加载器的作用:把 .class 文件加载到内存

概念 —— 谁是"搬运工"

类加载器(ClassLoader) 的作用非常单一而重要:把 .class 文件加载到内存中。没有类加载器,字节码永远只是磁盘上的一堆文件,JVM 无法使用它们。

// 来源:course/Demo01.java(类注释)
/*
    类加载器
    类加载器的作用就是将就是把.class文件加载到内存中

    加载 的时候,加载的不一定是.class文件,也可能是某个jar包,jar包就是把Java文件/class文件封装成可以执行的程序包,让别人使用
*/

加载的不只是 .class 文件,还可能是 jar 包

类加载的输入并不局限于单个 .class 文件——加载的也可能是某个 jar 包。jar 包就是把多个 Java/class 文件封装成的可执行的程序包,方便打包分发、被别人引用使用(第三方框架如 MySQL 驱动、Spring 等都是 jar 包形式)。

要点:

  • 类加载器处理的输入是 .class 字节码文件(或 jar 包),输出是 内存中的 Class 对象
  • jar 包 = 把 Java/class 文件封装成可执行的程序包,让别人使用(项目的依赖、第三方库都是 jar 包)
  • 这个过程发生在"编译之后、解释执行之前"

3. 类加载的时机:什么时候会加载类

概念 —— 懒加载:用到了才加载

Java 的类加载遵循 "使用时才加载,不使用就不加载" 的懒加载原则。只有当你真正用到某个类时,JVM 才会把它加载进内存。类加载的时机(触发点)一共有 6 种:

// 来源:course/Demo01.java(类注释)
/*
    类加载的时机(什么时候会加载类):
        1、创建类的实例对象的时候
        2、调用类的static方法的时候
        3、访问类或接口的static变量或者为该变量赋值的时候。
        4、使用反射的方式强制创建某个类或接口对应的java.lang.Class类型的对象的时候。
        5、初始化某个类的字类对象的时候
        6、直接使用Java.exe命令运行某个主类的时候。

        使用这个类的时候才会加载这个类,不使用的时候就不加载。
*/
触发场景 示例代码
① 创建类的实例对象 new Demo01()
② 调用类的 static 方法 Math.abs(-1)、Arrays.sort(arr)
③ 访问 / 赋值 static 变量 System.out、Student.schoolName = "爱扣钉"
④ 反射创建 Class 对象 Class.forName("course.Demo01")
⑤ 初始化某个类的子类 new SubStudent() 时先加载父类 Student
⑥ Java.exe 运行主类 java course.Demo01(含 main 方法的类)

要点:

  • 懒加载原则:使用这个类的时候才会加载,不使用就不加载——节省内存、提升启动速度
  • 只要触发以上任意一种场景,JVM 就会启动类加载流程
  • 注意第 ⑤ 条:初始化子类会先触发父类的加载(先有父后有子,加载顺序从父到子)

4. 类的加载过程:加载 → 链接(验证/准备/解析)→ 初始化

概念 —— 3 大步 5 小步

类的加载过程大体分为 3 步(也可细分为 5 步):

  • ① 加载(Loading)
  • ② 链接(Linking) —— 由 3 个小子步组成:
    • 验证(Verify)
    • 准备(Prepare)
    • 解析(Resolve)
  • ③ 初始化(Initialization)

    // 来源:course/Demo01.java(类注释)
    /*
    类的加载过程大体分为3步,也有分为5步:
        1、加载
        2、链接
            |- 验证
            |- 准备
            |- 解析
        3、初始化
    
        1、加载
        通过包名+类名(全限定名)获取这个类,准备用流进行传输
        把这个类加载到内存中
        加载完毕后创建一个Class对象
    
        2、链接
            2.1 验证
                确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。
                说白了就是看文件的信息是否有安全隐患,是否符合虚拟机的规范
            2.2 准备
                负责为类的类变量(static修饰的变量)分配内存,并且设置默认初始化值(初始化静态变量)
            2.3 解析
                将类的二进制数据流中的[符号引用]替换为[直接引用],如果本类中使用了其他的类型,那么在这一步就会开始找了,即需要找到使用的那个类。
                没有就报错。
        3、初始化
            根据我们通过程序指定的主观计划去初始化变量和其他资源(给静态变量赋值以及初始化其他资源)
    */
    

三步五小步结构图

类的加载过程
├── 1. 加载(Loading)
│      通过【全限定名】(包名.类名,如 java.lang.Object) 获取二进制字节流
│      → 将字节流代表的静态存储结构转化为运行时数据结构
│      → 在内存中生成代表这个类的 java.lang.Class 对象
├── 2. 链接(Linking)
│      ├── 2.1 验证(Verify):检查字节流信息是否符合虚拟机规范、有无安全隐患
│      ├── 2.2 准备(Prepare):为 static 类变量分配内存并设置默认初始化值(如 int→0)
│      └── 2.3 解析(Resolve):把符号引用替换为直接引用;本类用到其他类型时在此查找,找不到就报错
└── 3. 初始化(Initialization)
        根据程序指定的主观计划初始化变量和其他资源(给静态变量赋值等)

每步细节说明

步骤 干什么 关键点
① 加载 通过全限定名(包名.类名)找到 .class 文件,用流读取字节流 → 转为运行时数据结构 → 在堆中生成 java.lang.Class 对象 任何类被使用时,系统都会为它建立一个 Class 对象
②-1 验证 检查字节流是否符合 JVM 规范、不会危害虚拟机自身安全 防止加载恶意/损坏的字节码
②-2 准备 为 static 类变量分配内存并设置默认值(如 int→0、引用→null) 注意:此时还没执行我们写的赋值语句,只是默认初始化
②-3 解析 将字节流中的符号引用替换为直接引用;如果本类使用了其他类型,在这一步开始查找 找不到使用到的类就报错
③ 初始化 按程序指定的主观计划执行初始化(给静态变量赋值、执行静态代码块等) 真正执行 static int a = 10、static {...} 等代码

要点:

  • 加载 = 把字节码文件变为内存中的 Class 对象(生成"类的模具")
  • 验证 = 安全检查(有没有安全隐患、是否符合规范)
  • 准备 = 给 static 变量"占位"分配内存并设默认值(此时还不是我们写的值)
  • 解析 = 把"符号引用"(类的名字)换成"直接引用"(内存里的实际地址),用到别的类就在这步去找
  • 初始化 = 真正执行静态变量赋值语句和静态代码块(我们写的初始化代码在这里跑)
  • 小结一句:加载"把文件搬进来"、验证"检查安全"、准备"先分个默认内存"、解析"找到引用的类"、初始化"跑我们写的初始化代码"

5. Demo01 执行顺序演示:静态代码块 → 代码块 → 构造方法

概念 —— 类加载阶段与实例化阶段的直观验证

Demo01 用静态变量 + 静态代码块 + 普通代码块 + 构造方法同时给 a 赋值并打印,可以直观看到类的加载(静态初始化)发生在实例化之前,以及类加载"初始化"阶段到底执行了什么:

// 来源:course/Demo01.java
public class Demo01 {
    private static int a;        // 类变量(static),在"准备"阶段分配内存并设默认值 0

    private String str;          // 实例变量
    private Object obj;

    public Demo01() {            // 构造方法:实例化阶段执行(第 3 个执行)
        a = 10;
        System.out.println(a);
        System.out.println("构造方法...");

        str = new String("hello");
        obj = new Object();
    }

    {                            // 普通代码块(构造代码块):实例化阶段执行(第 2 个执行)
        a = 100;
        System.out.println(a);
        System.out.println("代码块....");
    }

    static {                     // 静态代码块:类加载"初始化"阶段执行(第 1 个执行)
        a = 10000;
        System.out.println(a);
        System.out.println("静态代码块");
    }

    public static void main(String[] args) {
        Demo01 demo01;
        demo01 = new Demo01();   // 第一次使用 Demo01 → 触发类加载 → 再实例化
        System.out.println(demo01);
    }
}

预期输出与执行顺序

10000
静态代码块
100
代码块....
10
构造方法...
course.Demo01@15db9742   (对象地址,每次运行不同)
执行顺序 打印内容 所处阶段 说明
第 1 步 10000 / 静态代码块 类加载的"初始化"阶段 main 中 new Demo01() 第一次使用该类 → 触发类加载 → 初始化阶段执行静态代码块,a = 10000
第 2 步 100 / 代码块.... 实例化(创建对象) new 创建对象时,先执行普通代码块(构造代码块),a = 100
第 3 步 10 / 构造方法... 实例化(构造方法) 然后执行构造方法,a = 10;同时给 str、obj 赋值
第 4 步 course.Demo01@... main 收尾 打印对象(默认 Object 的 toString)

要点:

  • 静态代码块只执行一次,发生在类加载的"初始化"阶段——先于任何对象创建
  • 普通代码块 + 构造方法属于实例化阶段——每 new 一次都会执行
  • 三者执行顺序:静态代码块 → 普通代码块 → 构造方法(类加载先于实例化)
  • static 变量在"准备"阶段已分配内存并置默认值 0,初始化阶段再被赋成我们写的值(10000)——印证第 4 节"准备 vs 初始化"的区别

6. 类加载器:三种类加载器与双亲委派(Demo02)

概念 —— 三个"搬运工"和它们的分工

类加载时需要使用类加载器(ClassLoader)。JVM 内置了三种类加载器,分工明确、层级分明:

// 来源:course/Demo02.java(类注释)
/*
        类在加载的时候,是需要使用类加载器进行加载的,大概的分类有三种:
        BootstrapClassLoader:虚拟机内置的类加载器,一般表示为null,并且没有父null。
        PlatformClassLoader:平台类加载,负责加载JDK中一些特殊的模块
        SystemClassLoader:系统类加载器,负责加载用户类路径上所指定的类库。

        SystemClassLoader  继承 PlatformClassLoader 继承 BootstrapClassLoader
*/
类加载器 职责 父加载器
BootstrapClassLoader(启动类加载器) 虚拟机内置类加载器,加载最核心的 JDK 类(如 java.lang.Object) 无,一般表示为 null
PlatformClassLoader(平台类加载器) 加载 JDK 中一些特殊的模块 BootstrapClassLoader
SystemClassLoader(系统/应用类加载器) 加载用户类路径(classpath) 上所指定的类库(我们的代码) PlatformClassLoader
SystemClassLoader(系统类加载器,加载用户类路径的类)
      ↑ 父加载器
PlatformClassLoader(平台类加载器,加载 JDK 特殊模块)
      ↑ 父加载器
BootstrapClassLoader(启动类加载器,虚拟机内置,表示为 null,无父加载器)

要点:

  • 三层结构:系统 → 平台 → 启动,上一层是下一层的"父加载器"(双亲委派模型)
  • 我们的类由 SystemClassLoader 加载——用户在 classpath 上写的类都由它负责
  • BootstrapClassLoader 一般表示为 null(不是 Java 类,是 JVM 内置的),它没有父加载器
  • 代码里写的是"继承",本质是双亲委派:子加载器收到加载请求,先交给父加载器去加载

Demo02 代码演示:获取三种类加载器

// 来源:course/Demo02.java
public class Demo02 {
    public static void main(String[] args) {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        //获取系统类加载器的父类加载器 平台类加载器
        ClassLoader platformClassLoader = systemClassLoader.getParent();
        //获取平台类加载器的父类加载器 虚拟机类加载器
        ClassLoader bootstrapClassLoader = platformClassLoader.getParent();

        System.out.println("系统类加载器:" + systemClassLoader);
        System.out.println("平台类加载器:" + platformClassLoader);
        System.out.println("虚拟机类加载器:" + bootstrapClassLoader);
        //虚拟机类加载器 没有父类加载器,而且不为null,直接就是空指针异常
        System.out.println("虚拟机类加载器还有没有父类加载器?" + bootstrapClassLoader.getParent());
    }
}

预期输出与运行要点

系统类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@...
平台类加载器:jdk.internal.loader.ClassLoaders$PlatformClassLoader@...
虚拟机类加载器:null
虚拟机类加载器还有没有父类加载器?   ← 到这里抛 NullPointerException(空指针异常)
代码 输出/结果 说明
ClassLoader.getSystemClassLoader() AppClassLoader@... 拿到系统类加载器(JDK 中类名为 AppClassLoader)
systemClassLoader.getParent() PlatformClassLoader@... 拿到平台类加载器——系统类加载器的父加载器
platformClassLoader.getParent() null 拿到启动类加载器——但它在 Java 中用 null 表示
bootstrapClassLoader.getParent() 抛空指针异常 因为 bootstrapClassLoader 是 null,再调 getParent() 就空指针了

要点:

  • 获取方式:ClassLoader.getSystemClassLoader() 拿到系统类加载器,再逐级 getParent() 向上取父加载器
  • BootstrapClassLoader 表示为 null:所以打印"虚拟机类加载器"输出是 null
  • null 上没有方法:bootstrapClassLoader.getParent() 对一个 null 调用方法 → 空指针异常(运行到这一行程序报错)
  • JDK 中类名:系统类加载器的类名是 jdk.internal.loader.ClassLoaders$AppClassLoader,平台类加载器是 ...$PlatformClassLoader

7. 双亲委派模型:为什么需要双亲委派(Demo03)

概念 —— 收到加载请求先"向上委派",顶层不行才"自己加载"

双亲委派模型是三种类加载器之间协同工作的核心机制:一个类加载器收到类加载请求时,并不会自己先去加载,而是把请求委派给父类加载器执行;如果父类加载器还有父加载器,就继续向上委派,依次递归,直到最顶层的类加载器。若顶层类加载器能完成加载任务,就成功返回;若顶层无法完成(找不到该类),子类加载器才会尝试自己去加载。

// 来源:course/Demo03.java(类注释)
/*
    如果一个类加载器收到了类加载的请求,它并不会自己先去加载,而是把这个请求委派给父类加载器去执行,如果父类加载器还存在父类加载器就会进一步委派,
    依次递归,请求最终会委派到最顶层的类加载器,如果顶层的类加载器能过完成鸡杂任务,就成功返回。如果无法完成,子类加载器才会尝试自己去加载,这种模式就是双亲委派模型。

    getSystemClassLoader() 获取系统类加载器
    getResourceAsStream() 加载某一个资源文件
*/

双亲委派执行流程

加载请求(如加载 course.Demo01)
      ↓
SystemClassLoader(系统类加载器)—— 不自己先加载,先委派给父加载器
      ↓
PlatformClassLoader(平台类加载器)—— 继续委派给父加载器
      ↓
BootstrapClassLoader(启动类加载器,最顶层)—— 尝试加载
      │
      ├── 能加载(如 java.lang.String 核心类)→ 加载成功,直接返回
      └── 不能加载(如用户写的 course.Demo01)→ 向下回退
              ↓
      PlatformClassLoader 尝试加载 → 不能加载 → 继续向下回退
              ↓
      SystemClassLoader 尝试加载 → 能加载(用户 classpath 上的类)→ 加载成功

要点:

  • 委派方向是"向上":子加载器收到请求先交父加载器,父再交祖父……一路到最顶层的 BootstrapClassLoader
  • 回退方向是"向下":顶层加载不了,才逐级往下回退,由子加载器尝试自己加载
  • 核心类由顶层加载:java.lang.String 等核心类只能由最顶层的 BootstrapClassLoader 加载
  • 我们的类由底层加载:course.Demo01 等用户类最终由 SystemClassLoader(系统类加载器)加载

为什么需要双亲委派模型?—— 三个原因

// 来源:course/Demo03.java(类注释)
/*
    为什么需要双亲委派模型?
        避免类被重复加载:同一个类在整个JVM中只有一个Class对象(类的唯一性是由 类+类加载器 共同决定的),双亲委派机制可以保证任意一个类都只被加载一次。
        保证核心类库的安全:防止用户编写一个假的 核心类 混入到系统中,双亲委派机制会依次把请求交给最顶层的类加载器,
            比如 java.lang.String类只能由顶层的启动类加载器进行加载,用户自定义的同名类永远不会被加载。
        保证类的唯一性:
            即使我们自定义了类加载器,加载了 自定义的 java.lang.String类,但是由于双亲委派机制,实际上加载的还是最顶层的类加载器加载的核心类,
            避免了出现多份不同的String类型的导致的类型混乱。
*/
原因 说明 类比
① 避免类被重复加载 同一个类在整个 JVM 中只有一个 Class 对象(类的唯一性由 类 + 类加载器 共同决定);双亲委派保证任意一个类只被加载一次 全公司只有"一份员工档案",不允许每人各抄一份
② 保证核心类库的安全 防止用户编写一个假的"核心类"混入系统;双亲委派把请求依次交给最顶层加载器,java.lang.String 只能由启动类加载器加载,用户自定义的同名类永远不会被加载 核心系统只有最高层才能签发,防止伪造
③ 保证类的唯一性 即使自定义类加载器加载了自定义的 java.lang.String,由于双亲委派,实际加载的还是顶层加载器加载的核心类——避免出现多份不同的 String 类型导致类型混乱 全 JVM 只认"官方版"String,不会出现多个版本混淆

要点:

  • 类的唯一性 = 类 + 类加载器共同决定——同一个类名由不同加载器加载会产生不同的 Class 对象,双亲委派从机制上避免了这种情况
  • 核心类库安全是双亲委派最重要的价值:任何人无法通过自定义同名类替换/伪造 JDK 核心类(如 java.lang.String)
  • 一个例子记牢:自己写一个 java.lang.String 类,双亲委派机制下它永远不会被加载,JVM 用的始终是启动类加载器加载的官方 String

8. 用类加载器加载资源配置文件:getResourceAsStream + Properties(Demo03 + prop.properties)

概念 —— 类加载器的实际应用:读取配置文件

类加载器除了加载字节码外,还有一个经典的实际应用——用类加载器加载资源配置文件。Demo03 演示了如何通过系统类加载器加载根目录下的 prop.properties 配置文件,再用 Properties 解析其中的数据库连接配置。

// 来源:course/Demo03.java
public class Demo03 {
    public static void main(String[] args) throws IOException {
        // --- 下面代码的过程 ,其实是编写代码加载资源配置文件的过程
        //获取系统类加载器
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();

        //利用类加载器加载一个指定文件
        //参数:文件的路径
        //返回值:字节流
        InputStream inputStream = classLoader.getResourceAsStream("prop.properties");

        //创建解析properties文件的对象 Properties-它的父类是Hashtable 是Map集合接口的另一个实现类 底层是数组
        Properties pro = new Properties();
        //加载输入流对象 -- 其实就是加载我们指定的文件
        pro.load(inputStream);

        //获取指定的配置信息
        String driver = pro.getProperty("db.driver");
        String url = pro.getProperty("db.url");
        String username = pro.getProperty("db.username");
        String password = pro.getProperty("db.password");

        //调用连接数据库的方法,执行连接数据库的操作....
        链接数据库的方法(driver, url, username, password);

        //输出
        System.out.println(pro);
        //关闭流
        inputStream.close();

        // xml  / json / yaml
    }

    public static void 链接数据库的方法(String driver, String url, String username, String password) {

    }
}

配置文件 prop.properties

# 来源:src/prop.properties
# 教学演示示例:以下配置均为占位符,不包含任何真实账号密码信息
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/your_database
db.username=your_username
db.password=********

代码拆解:加载配置文件的全流程

代码 作用 说明
ClassLoader.getSystemClassLoader() 获取系统类加载器 第 6 节 Demo02 已学过的方法
classLoader.getResourceAsStream("prop.properties") 加载指定资源文件 参数是文件路径(相对于 classpath 根目录),返回字节流 InputStream
new Properties() 创建解析 properties 文件的对象 Properties 的父类是 Hashtable,是 Map 集合接口的另一个实现类,底层是数组
pro.load(inputStream) 加载输入流对象 把输入流(即我们指定的文件)解析进 Properties
pro.getProperty("db.driver") 等 获取指定的配置信息 按 key 读取 value,拿到数据库驱动 / 连接地址 / 用户名 / 密码
链接数据库的方法(...) 调用连接数据库的方法 把读到的 4 个配置传参,执行连接数据库的操作(方法体留空为示例)
System.out.println(pro) 输出 Properties 对象 直观看到加载到的全部配置
inputStream.close() 关闭流 资源释放(IO 流使用后必须关闭,回顾 08-06/08-07 IO 流知识)

加载配置文件的意义

配置文件 prop.properties(数据库连接信息:driver/url/username/password)
      ↓ 类加载器加载(getResourceAsStream 返回字节流)
      ↓ Properties.load 解析(Properties 底层是 Map/Hashtable,key=value)
      ↓ getProperty 按 key 取值
      数据库连接参数(driver、url、username、password)
      ↓
      调用连接数据库的方法(执行业务)

要点:

  • 配置与代码分离:数据库连接信息写在 .properties 配置文件里,代码通过类加载器读取——改配置不用改代码、重新编译,这是企业开发的常规做法
  • getResourceAsStream 的路径:参数是类路径(classpath)根目录下的文件路径,所以 prop.properties 放在 src/ 根目录即可被加载
  • Properties:父类是 Hashtable,是 Map 接口的另一个实现类(底层是数组)——键值对形式存储,getProperty(key) 按 key 取 value
  • IO 流要关闭:getResourceAsStream 返回的 InputStream 用完要 close()(衔接 08-06/08-07 的 IO 流知识)
  • 常见的配置文件格式:除了 .properties,还有 xml / json / yaml——不同格式对应不同的解析方式(本课先掌握 properties)
  • 本项目应用场景:数据库连接四要素(驱动 driver / 连接地址 url / 用户名 username / 密码 password)正是 JDBC 连接数据库的标配参数,后续学习 JDBC 时直接用

9. 知识点全景总结

知识点 核心内容 来源
Java 程序执行全流程 .java 源文件 → 编译 → .class 字节码 → 类加载器 → 内存 → JVM 解释 → 二进制机器码 → 执行 course/Demo01 注释
类加载器的作用 把 .class 文件加载到内存中——字节码的"搬运工" course/Demo01 注释
类加载 6 种时机 ①创建实例对象 ②调用 static 方法 ③访问/赋值 static 变量 ④反射创建 Class 对象 ⑤初始化子类 ⑥Java.exe 运行主类 course/Demo01 注释
懒加载原则 使用这个类的时候才会加载,不使用就不加载 course/Demo01 注释
全限定名 包名.类名(如 java.lang.Object),类加载用它定位字节码 course/Demo01 注释
加载(Loading) 通过全限定名获取二进制字节流 → 转为运行时数据结构 → 生成 java.lang.Class 对象 course/Demo01 注释
链接-验证(Verify) 检查字节流符合虚拟机规范、不会危害虚拟机安全 course/Demo01 注释
链接-准备(Prepare) 为 static 类变量分配内存并设置默认初始化值 course/Demo01 注释
链接-解析(Resolve) 符号引用替换为直接引用;用到其他类型在此查找,找不到报错 course/Demo01 注释
初始化(Init) 按主观计划给静态变量赋值、执行静态代码块等 course/Demo01 注释
Demo01 执行顺序 静态代码块(a=10000)→ 代码块(a=100)→ 构造方法(a=10)——类加载先于实例化 course/Demo01 main
三种类加载器 BootstrapClassLoader(虚拟机内置,表示为 null,无父)/ PlatformClassLoader(JDK 特殊模块)/ SystemClassLoader(用户类路径类库) course/Demo02 注释
双亲委派层级 SystemClassLoader 继承 PlatformClassLoader 继承 BootstrapClassLoader course/Demo02 注释
Demo02 获取演示 ClassLoader.getSystemClassLoader() → getParent() 逐级获取;Bootstrap 打印 null,对其调 getParent() 抛空指针 course/Demo02 main
jar 包 加载的不一定是 .class 文件,也可能是 jar 包——把 Java/class 文件封装成可执行的程序包,让别人使用 course/Demo01 注释
双亲委派模型 收到加载请求不自己先加载,先委派给父类加载器、递归至最顶层;顶层能加载就成功返回,顶层无法完成才由子类加载器尝试自己加载 course/Demo03 注释
委派方向与回退方向 委派向上(子→父→祖父→最顶层);回退向下(顶层→……→子),我们的类最终由 SystemClassLoader 加载 course/Demo03 注释
双亲委派原因①:避免重复加载 同一个类全 JVM 只有一个 Class 对象(唯一性由类+类加载器共同决定),双亲委派保证任意一个类只被加载一次 course/Demo03 注释
双亲委派原因②:核心类库安全 防止用户写假的"核心类"混入;java.lang.String 只能由顶层启动类加载器加载,用户自定义同名类永远不会被加载 course/Demo03 注释
双亲委派原因③:保证类的唯一性 即使自定义加载器加载自定义 String,实际加载的还是顶层加载的核心类,避免多份不同 String 类型混乱 course/Demo03 注释
getResourceAsStream classLoader.getResourceAsStream("prop.properties") 用类加载器加载资源文件,参数为 classpath 下文件路径,返回字节流 course/Demo03 main
Properties 解析 properties 文件的对象:父类是 Hashtable、是 Map 接口的另一个实现类、底层是数组;load(InputStream) 加载、getProperty(key) 按 key 取值 course/Demo03 main
prop.properties 配置 数据库连接四要素:db.driver(MySQL 驱动)/ db.url(JDBC 连接地址)/ db.username(用户名,占位符示例)/ db.password(密码,占位符示例) src/prop.properties
配置文件的意义 配置与代码分离:改配置不用改代码;加载后调用连接数据库方法;关闭流;常见格式还有 xml / json / yaml course/Demo03 main

10. 随堂练习要点

  • 执行全流程复述(必做):把 .java 文件 → 编译 → .class 字节码文件 → 类加载器 → 内存 → JVM 解释 → 二进制机器码 → 执行 这条链背下来——这是理解本课所有知识点的总纲
  • 类加载 6 种时机背诵:①创建实例对象 ②调用 static 方法 ③访问/赋值 static 变量 ④反射创建 Class 对象 ⑤初始化子类 ⑥Java.exe 运行主类——每一类都能举一个代码例子(new / Math.abs / Student.name / Class.forName / new 子类 / 运行 main 类)
  • 懒加载理解:思考"为什么 Java 要设计成使用时才加载"——省内存、加快启动速度;对比"启动时把所有类全部加载"的坏处(内存浪费、启动慢)
  • 加载过程三步背诵(必做):加载 → 链接(验证 / 准备 / 解析)→ 初始化;能说出每步一句话(加载=搬文件生成 Class 对象 / 验证=安全检查 / 准备=static 变量分内存设默认值 / 解析=符号引用换直接引用 / 初始化=执行我们写的静态初始化代码)
  • 准备 vs 初始化区分:准备阶段 static int a 只是默认值 0,初始化阶段才执行 a = 10 这类赋值——思考 Demo01 中 static 变量 a 的"默认 0 → 10000"两步
  • Demo01 执行顺序实验(必做):运行 course/Demo01,观察输出顺序为静态代码块(10000) → 代码块(100) → 构造方法(10);再在 main 中连续 new 两个 Demo01 对象,观察静态代码块是否只打印一次(类加载只发生一次)
  • 删静态代码块实验:把 Demo01 的 static 代码块注释掉,重新运行,观察输出变化——体会"静态代码块属于类加载阶段、只执行一次"
  • 三种类加载器背诵:BootstrapClassLoader(虚拟机内置 / 表示为 null / 无父)/ PlatformClassLoader(JDK 特殊模块)/ SystemClassLoader(用户类路径类库)——层级:系统 → 平台 → 启动
  • Demo02 运行验证(必做):运行 course/Demo02,观察三个打印结果——系统类加载器 AppClassLoader、平台类加载器 PlatformClassLoader、启动类加载器 null;最后一行 getParent() 触发空指针异常
  • 空指针原因思考:为什么"虚拟机类加载器还有没有父类加载器?"会空指针——因为 bootstrap 表示为 null,对 null 调方法必然 NPE;这也印证了"启动类加载器没有父加载器"
  • 双亲委派理解:把"SystemClassLoader → 父 PlatformClassLoader → 父 BootstrapClassLoader"的委派链画出来——思考我们的 course.Demo01 是由哪个加载器加载的(系统类加载器 SystemClassLoader)
  • javap 观察字节码(拓展):对 Demo01.class 执行 javap -verbose Demo01,观察字节码中的"符号引用 / 常量池"——为理解第 4 节"解析:符号引用替换为直接引用"提供直观素材
  • 双亲委派流程复述(必做):用"加载 course.Demo01"走一遍双亲委派——系统类加载器收到请求 → 先委派给平台类加载器 → 再委派给启动类加载器 → 顶层加载不了 → 逐级向下回退 → 最终系统类加载器加载成功;记住两个方向:委派向上、回退向下
  • 委派动作实验:把第 6 节 Demo02 的代码扩展——在 main 里打印 System.out.println(Demo01.class.getClassLoader()),观察我们自己写的类由系统类加载器加载;再打印 System.out.println(String.class.getClassLoader()),观察核心类加载器为 null(启动类加载器加载,Java 中表示为 null)——一对比就理解"核心类归顶层、用户类归底层"
  • 双亲委派三个原因背诵(必做):①避免类被重复加载(全 JVM 一个类只有一个 Class 对象)②保证核心类库安全(用户写的 java.lang.String 永不加载)③保证类的唯一性(避免多份 String 类型混乱)——每个原因都能一句话说清
  • 假 String 实验(思考):尝试自己写一个 java.lang.String 类放在 classpath 里并运行,观察程序报错(找不到 main 方法 / 加载的还是官方 String)——直观体会"用户自定义同名核心类永远不会被加载"
  • 类的唯一性理解:记住公式"类的唯一性 = 类 + 类加载器共同决定"——同一个类名由不同类加载器加载,产生的是不同的 Class 对象;双亲委派从机制上避免了同一个类被重复加载
  • 加载配置文件实验(必做):运行 course/Demo03,观察 System.out.println(pro) 输出 {db.password=********, db.username=your_username, db.driver=com.mysql.cj.jdbc.Driver, db.url=jdbc:mysql://...}——4 个数据库配置被成功读取
  • getResourceAsStream 路径实验:把 getResourceAsStream("prop.properties") 的参数改成不存在的路径(如 "abc.properties"),运行观察返回的 inputStream 为 null、后续 pro.load(null) 抛空指针异常——体会"参数必须是 classpath 下真实存在的文件"
  • Properties 回顾 Map 知识:Properties 的父类是 Hashtable、是 Map 接口的实现类、底层是数组——回顾 07-31 学的 Map(HashMap/Hashtable)知识,理解 Properties 就是"key=value 的配置型 Map"
  • getProperty 实验:给 prop.properties 新增一行自定义配置(如 app.name=J260713),在 Demo03 里用 pro.getProperty("app.name") 读取并打印——体会"改配置文件不用改代码"的配置与代码分离思想
  • 关闭流练习:把 inputStream.close() 注释掉再运行,体会资源不关闭的隐患(结合 08-06/08-07 IO 流"用完必须 close"的铁律)——后面学 try-with-resources 可自动关闭
  • 数据库配置串联思考:prop.properties 里的 db.driver(com.mysql.cj.jdbc.Driver)和 db.url(jdbc:mysql://...)正是 JDBC 连接数据库的标配参数——思考后续学 JDBC 时,这套"类加载器读配置文件"的方式如何复用

11. 拓展阅读

  • 类加载机制的地位:本课(类的加载机制 + 类加载器)属于 JVM(Java 虚拟机) 知识领域——面试高频考点,也是理解反射、动态代理、Tomcat 类隔离、Spring 框架等进阶知识的基石
  • 双亲委派模型(Parent Delegation Model):除了 JDK 三种内置加载器外,还可以自定义类加载器(继承 ClassLoader 并重写 findClass());双亲委派模型保证核心类库只由启动类加载器加载,避免用户自定义同名类(如自己写个 java.lang.String)破坏 JDK 核心类
  • 双亲委派的作用:①安全性——核心类由启动加载器加载,防止核心类被篡改;②避免重复加载——父加载器已加载的类,子加载器不会重复加载;③一致性——保证同一个类在全 JVM 中只有一份 Class 对象
  • 打破双亲委派:Tomcat、JDBC(SPI 机制)、热部署等场景需要打破双亲委派(如 Tomcat 的 WebAppClassLoader 先加载自己目录的类),是 JVM 进阶的重要话题
  • 类加载与反射的衔接:第 ④ 条加载时机"反射创建 Class 对象"——回顾 Java 反射:Class.forName() / 类名.class / 对象.getClass() 三种获取 Class 对象的方式,都触发了类加载;反射正是建立在"加载后生成 Class 对象"这一机制之上
  • Class 对象:每个类被使用后,JVM 都会在堆中生成一个 java.lang.Class 类型的对象(类的"元信息":包名、方法、字段、父类等);Class.forName("全限定名") 可拿到它
  • 对象的创建过程:new 一个对象经历了类加载(首次)→ 分配内存 → 实例变量默认初始化 → 普通代码块 → 构造方法的完整链路——本课 Demo01 的"静态代码块 → 代码块 → 构造方法"正是这条链路的前半段直观演示
  • JDK 类加载器源码:JDK9+ 中类加载器在 jdk.internal.loader.ClassLoaders 类中定义——AppClassLoader(系统/应用)、PlatformClassLoader(平台)、BootstrapClassLoader(启动)内部类;通过 ClassLoader.getSystemClassLoader() 即可获取
  • 启动类加载器为何是 null:BootstrapClassLoader 由 C++ 实现、是 JVM 的一部分(不属于 Java 类),因此 Java 代码中"拿不到它",统一用 null 表示——getParent() 返回 null 即代表"已到最顶层,没有父加载器"
  • 链接 vs 加载的时机:加载、验证、准备、解析、初始化这 5 步并非严格串行——解析在某些情况下可以推迟到真正使用符号引用时才进行("惰性解析"),初始化则严格在类被"主动使用"时触发
  • 类加载与 static 的关系:static 变量/静态代码块属于类级(类加载的初始化阶段执行);普通代码块/构造方法属于实例级(每次 new 执行)——这就是 Demo01 执行顺序(静态先于实例)的底层依据
  • 类加载与线程安全:JVM 保证一个类的初始化过程在同一时间只会被一个线程执行(初始化加了同步锁)——多线程同时首次使用一个类时,只有一个线程执行初始化,其余线程阻塞等待
  • 双亲委派的三个价值小结:①安全——核心类只能由启动加载器加载,用户自定义的假核心类(如 java.lang.String)永远不会被加载,防止核心 API 被篡改;②唯一——同一个类全 JVM 只有一个 Class 对象,避免类型混乱;③不重复——父加载器已加载的类子加载器不会重复加载
  • Class.getClassLoader() 验证:Class.getClassLoader() 返回加载该类的类加载器——自己写的类返回系统类加载器(AppClassLoader);核心类(java.lang.String / java.lang.Object)返回 null(BootstrapClassLoader 用 null 表示)——这是验证"哪些类由哪个加载器加载"最直接的手段
  • JDBC 如何打破双亲委派:JDBC(数据库驱动)是一个典型的打破双亲委派场景——DriverManager 是启动类加载器加载的核心类,但它需要加载用户 classpath 下的 MySQL/Oracle 驱动 jar(SystemClassLoader 加载的类);为此 JDK 提供了线程上下文类加载器(Thread Context ClassLoader),让核心类可以反过来使用子加载器加载驱动类——理解 SPI 机制是 Java 进阶的经典话题
  • Tomcat 打破双亲委派:Tomcat 的 WebAppClassLoader 先加载自己 Web 应用的类、加载不到才委派给父加载器(与双亲委派顺序相反)——从而实现多个 Web 应用之间的类隔离(不同应用可用不同版本的 Spring 等),也支持热部署
  • getResourceAsStream 的资源查找原理:ClassLoader.getResourceAsStream("prop.properties") 会在 classpath(类路径) 下按给定路径查找资源文件——src/ 目录编译后就在 classpath 中,所以放在 src/ 根目录的文件能被找到;返回的 InputStream 就是文件的字节流
  • 配置文件格式对比:常用配置文件格式——properties(key=value 简单、Properties 解析)/ xml(标签树形结构、DOM/SAX 解析)/ json(对象嵌套、Jackson/Gson 解析)/ yaml(缩进层级、Spring Boot 默认配置格式)——不同格式各有适用场景,后续框架学习中会接触 xml/json/yaml
  • 配置与代码分离(环境隔离):把数据库连接、接口地址等易变信息放入配置文件,实现配置与代码分离——换数据库/换环境只改配置文件、不改代码不重新编译;企业开发还会按 dev / test / prod 环境准备多套配置文件,通过参数切换
  • JDBC 四要素:连接 MySQL 数据库需要 4 个核心参数——driver(驱动类 com.mysql.cj.jdbc.Driver)、url(jdbc:mysql://主机:端口/库名?参数)、username(用户名)、password(密码)——正是 prop.properties 中配置的 4 项,后续学习 JDBC 时这套"配置文件读取参数"的方式会直接复用
  • try-with-resources 自动关闭:本课 inputStream.close() 是手动关闭流;Java 7 提供了 try-with-resources 语法(try (InputStream in = ...) { }),实现 AutoCloseable 的资源会在 try 结束后自动关闭,无需手动 close——IO 流、网络连接等资源都推荐用它管理(后续课程会正式学习)
  • JDK 文档:java.lang.ClassLoader(类加载器核心 API,含 getResourceAsStream)、java.lang.Class(Class 对象)、java.util.Properties(配置文件解析类,父类 Hashtable)、Class.forName(反射加载类)