黑马JVM解析笔记(六):深入理解JVM类加载机制与运行时优化

发布于:2025-07-03 ⋅ 阅读:(14) ⋅ 点赞:(0)

1.JVM类加载

类加载是Java虚拟机将描述类.class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型的过程。

核心阶段:加载 —> 连接 —>初始化

1.1 加载,以jdk1.8为例

在这里插入图片描述

  • 类加载器先把Person.class字节码解析为InstanceKlass(底层是c++)结构,存放一些关键信息和对象的引用,生命周期与类加载器相同(类卸载时才释放)
  • 然后就是把新对象和实例信息放在堆内存,由CG管理
    • 首先,如果存在父类会先加载父类元数据
    • 元空间的java_mirror指向堆中Person.class,这个是存放静态变量和类方法指针
    • 所有的实例化对象又都指向Person.class(不同指针指向不同实例化),这样java才可以通过Person.class访问到不同的实例
    • Object对象,作用就是所有实例对象共享里面东西
1.2 连接

连接分为3个阶段:验证,准备和解析

1.2.1 验证

验证这里就不过多说明,主要的作用就是保证加载的字节码符合JVM规范

1.2.2 准备

为类的变量(静态变量)在方法区分配内存并设置初始值(0)

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于_java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
1.2.3 解析

将常量池内的符号引用替换成直接引用的过程,通俗的说就是类和接口的全限定名、字段的名称和描述符和方法的名称和描述符转换为具体的指令和相对偏移量

1.3 初始化

执行类的<clinit>(),真正开始执行类中定义的java程序代码

<clinit>() 方法:

  • 说明:由编译器自动收集类中静态变量和静态语句块中语句合并生成
  • 顺序:按照原文件的顺序
  • 父类优先,父类的**<clinit>()** 方法在子类先执行
  • 线程安全:同一个类加载器下只能一个线程进行初始化

何时触发初始化?(主动调用的):

  1. 创建实例:new,getstatic,putstatic,invokestatic
  2. 反射调用
  3. 初始化子类
  4. 主类:main
  5. 默认方法(1.8版本)

被动引用不会触发初始化:

  • 通过子类引用父类的静态字段,不会导致子类初始化。
  • 通过数组定义来引用类(如 SuperClass[] sca = new SuperClass[10];),不会触发该类的初始化。
  • 引用类的常量——基本数据类型(static final 修饰,且在编译期把结果放入常量池的字段),不会触发该类的初始化。
1.4 相关面试题
1.4.1 从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化
public class Load4 {
    public static void main(String[] args) {
            System.out.println(E.a);
            System.out.println(E.b);
            System.out.println(E.c);
        }
 }
class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;
}

a,b不会,引用 static final 和基本数据类型,不会导致初始化,由于已经在编译的时候放入常量池中,c是包装类,底层会通过语法糖调用Integer.valueOf方法,会推迟到初始化阶段

1.4.2 典型应用 - 完成懒惰初始化单例模式
public final class Singleton {
    private Singleton() { }
    // 内部类中保存单例
    private static class LazyHolder {
    	static final Singleton INSTANCE = new Singleton();
    }
    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
    public static Singleton getInstance() {
    	return LazyHolder.INSTANCE;
    }
}

就是懒惰初始化单例模式的实现,实现原理:

  • 构造方法私有化
    • 禁止外部直接 new Singleton(),确保实例只能通过 getInstance() 获取。
  • 静态内部类 LazyHolder 托管实例
    • 使用 static final 修饰单例实例 INSTANCE,保证:
      • 线程安全(JVM 类加载机制确保 static final 变量只初始化一次)。
      • 不可变性(防止运行时被修改)。
  • 懒加载触发时机
    • 只有首次调用 getInstance() 时,才会加载 LazyHolder 类并初始化 INSTANCE,避免类加载时直接创建实例(饿汉式的缺点)。

2.类加载器

2.1 以JDK8 为例:四种加载器类型
类加载器名称 实现方式 父加载器 加载路径 加载范围 是否可获取 特点
启动类加载器 (Bootstrap ClassLoader) JVM 内建(C/C++ 实现) 无(顶级加载器) JAVA_HOME/jre/lib 目录 (如 rt.jar, resources.jar, charsets.jar 等) Java 核心库 (java.*, javax.*, sun.* 等) (返回 null) 唯一无父加载器;安全机制禁止加载非核心类
扩展类加载器 (Extension ClassLoader) Java (sun.misc.Launcher$ExtClassLoader) 启动类加载器 JAVA_HOME/jre/lib/ext 目录 或 java.ext.dirs 系统变量指定路径 Java 扩展库 (javax.* 的部分实现) 加载 JDK 扩展功能(如加密、XML 解析等)
应用程序类加载器 (Application ClassLoader) Java (sun.misc.Launcher$AppClassLoader) 扩展类加载器 用户类路径(ClassPath) (-classpath-cp 参数指定) 用户程序类 (项目代码 + 第三方库) 默认类加载器 ClassLoader.getSystemClassLoader() 返回此实例
自定义类加载器 (Custom ClassLoader) Java (用户继承 ClassLoader 实现) 默认应用程序类加载器 用户自定义路径 (网络、数据库、加密文件等) 用户指定类 (如动态生成类、隔离模块等) 需重写 findClass() 方法;支持热部署、类隔离、代码加密等扩展场景
2.2 双亲委派模式源码分析
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查是否已加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 2. 如果父加载器不为null,委派给父加载器
                if (parent != null) {
                    c = parent.loadClass(name, false); // 注意 resolve=false,父加载器只负责加载,连接由子加载器触发(可选)
                } else {
                    // 3. 父加载器为null(即Bootstrap),尝试用Bootstrap加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器找不到,不是错误,继续往下走
            }
            // 4. 如果父加载器(包括Bootstrap)都没找到
            if (c == null) {
                // 调用自身的findClass()方法尝试加载
                c = findClass(name);
            }
        }
        // 5. 如果需要连接(解析),则进行连接阶段(Linking)
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派机制流程是(递归实现):

  1. 先看自己本地缓存是否加载过该类
  2. 如果没有就委派上级—应用程序类加载器 (Application ClassLoader)
  3. 再查看是否加载过的类,如果没有,继续委派上级—扩展类加载器 (Extension ClassLoader)
  4. 再查看是否加载过的类,如果没有,继续委派上级—启动类加载器 (Bootstrap ClassLoader),然后findClass方法区查找,显然是没有的
  5. 再查看是否加载过的类,如果没有,就调用下一级的(扩展类加载器)去执行findClass方法区查找,显然是没有的
  6. 继续调用下一级(应用程序类加载器),去执行findClass方法区查找,然后到类路径找到,就进行加载

双亲委派机制流程目的是什么:

  • **保护核心类库安全:**防止用户写的java核心类覆盖掉已经定义好的类,因此是由最高级的加载器先进行创建,这样就不会出现覆盖的情况
  • 避免重复加载: 确保一个类在 JVM 中只被加载一次(由同一个加载器加载)。父加载器加载过的类,子加载器就不会再加载。这保证了类的唯一性。
  • 保证基础类型一致性: 例如,无论哪个加载器加载 java.lang.Object,最终都由 Bootstrap 加载器加载,保证了所有环境中 Object 都是同一个类。
2.3 线程上下文类加载器

线程上下文类加载器(TCCL)是 Java 为解决双亲委派模型的局限性而引入的重要机制。每个线程都拥有一个独立的上下文类加载器,可通过线程对象进行访问和修改。

线程上下文类加载器案例—SPI(Service Provider Interface)机制:

  • 核心接口(如 JDBC 的 java.sql.Driver)在核心类库(rt.jar)中,由 Bootstrap 加载器加载。
  • 具体实现(如 mysql-connector-java.jar)在 ClassPath 下,应由 AppClassLoader 加载。
  • 问题:Bootstrap 加载器无法“看见” AppClassLoader 加载的类。按照双亲委派,DriverManager(由 Bootstrap 加载)加载 Driver 接口的实现时,会委派给 Bootstrap,但 Bootstrap 找不到第三方驱动类。
  • 解决方案: 使用线程上下文类加载器(Thread Context ClassLoader, TCCL)
    • Thread.currentThread().setContextClassLoader(ClassLoader cl) 可以设置当前线程的上下文类加载器(通常设置为 AppClassLoader 或自定义加载器)。
    • DriverManager 在加载驱动时,使用 Thread.currentThread().getContextClassLoader() 获取上下文类加载器(通常是能加载到第三方驱动的加载器)来加载 Driver 实现类。这相当于父加载器(Bootstrap)反过来请求子加载器(AppClassLoader)去加载,打破了自底向上的委派。这是 Java 基础库主动打破双亲委派的典型案例。
2.4 自定义类加载器

开发者可以继承 java.lang.ClassLoader 类并重写 findClass(String name) 方法(强烈推荐只重写此方法,而不是覆盖破坏双亲委派的 loadClass 方法)来创建自定义的类加载器。

自定义类加载器关键步骤

  1. 继承 ClassLoader
  2. 重写 findClass(String name) 方法:
    • 根据类名(name)定位并读取 .class 文件的二进制数据(byte[])。
    • 可在此处进行解密、解压缩等处理。
    • 调用父类的 defineClass(String name, byte[] b, int off, int len) 方法(或 defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain))将字节数组转换为 Class<?> 对象。
    • 如果找不到类,抛出 ClassNotFoundException
  3. (可选) 在构造函数中指定父加载器(默认父加载器是系统类加载器)。

重要原则: 尽量遵循双亲委派。只在 findClass 中实现自定义加载逻辑,让 loadClass 方法维持其委派机制。除非有特殊需求(如 SPI 服务加载),否则避免覆盖 loadClass

3.JVM运行期的优化

JVM在运行期会做大量的优化工作,目的就是提升Java程序的执行效率,让解释执行的字节码接近甚至超过C++等编译语言

3.1 即时编译(分层编译、方法内联、字段优化)

即时编译的类型

编译器 名称 特点 使用场景
C1 Client 编译器 编译速度快,优化少 小程序、冷代码
C2 Server 编译器 编译慢,优化激进(更快机器码) 热代码、性能关键路径

上面两种即时编译器的区别是,前者更多目的是想让编译速度更快,后者编译慢是因为花了更多时间在优化字节码上面,从而生成一种执行更快的机器码,保存在缓存中

3.1.2 分层编译:将C1和C2编译器结合使用,可以根据代码热点分层升级优化
层级 执行方式 说明
0 解释执行 初始所有方法都解释执行,便于启动快
1 C1 编译 + profiling 快速编译 + 收集运行数据(类型、分支命中率等)
2 C1 编译 + 最少优化 次热代码快速响应
3 C1 编译 + 全优化 热度更高,继续深入优化
4 C2 编译 用 C1 收集 profile 后交给 C2 进行最高级别机器码优化

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等

JVM 参数控制:

-XX:+TieredCompilation (默认开启)
-XX:+PrintCompilation (打印编译日志)
3.1.3 **方法内联:**将方法调用替换为方法体实际内容,避免运行时方法调用带来的开销

举例:

int add(int a, int b) {
    return a + b;
}

int x = add(1, 2); // → int x = 1 + 2; // 可内联

不过是有条件限制:

  1. 方法体太大时(默认限制 35 字节)不会内联;
  2. 递归方法不可内联;
  3. 多态调用太复杂时难以判断目标 → 可配合去虚拟化解决;
3.1.4 字段优化:

字段优化是 JVM 在运行时对类中的 成员变量(字段)访问和存储做的各种性能提升处理。目的是:

  • 加快字段读取和写入速度
  • 减少内存开销
  • 提高程序执行效率

常见字段优化方式:

1.常量字段折叠(Constant Folding)

如果字段是 static final 常量,JVM 编译时会直接用字面量替换字段引用

public static final int SIZE = 10;
int x = SIZE + 2;
// 实际变成 int x = 12;

优点:不再访问字段,提高运行速度。

2.final 字段内联

final 修饰的实例字段,在对象构造后不会再变,JVM 可以直接把字段值内联到使用处

final String name = "Tom";
// → 直接替换为 "Tom",避免字段访问

2.字段偏移优化(Field Offset Optimization)

JVM 会在类加载时为每个字段分配 固定偏移地址,然后在运行时通过偏移量直接访问内存。

优点:

  • 不用每次查字段名 → 类结构 → 字段位置;
  • 类似“看坐标读数据”,效率更高。
3.2 反射优化

反射虽然灵活但性能低(因为:涉及方法查找、涉及安全检查、涉及动态类型转换和方法调用不是直接执行,而是通过一层“跳板”),JVM 通过 内联、MethodHandle、Lambda表达式、缓存、权限控制等手段,让反射更快、更像普通调用。


网站公告

今日签到

点亮在社区的每一天
去签到