当前位置: 首页 > ops >正文

【JVM】- 类加载与字节码结构3

类加载阶段

1. 加载

  • 加载:将类的字节码载入方法区中,内部采用C++的instanceKlass描述java类。
  • 如果这个类的父类还没加载,则先加载父类
  • 加载和链接可能是交替运行的

在这里插入图片描述

  1. 通过全限定名获取字节码
    • 从文件系统(.class 文件)、JAR 包、网络、动态代理生成等途径读取二进制数据。
  2. 将字节码解析为方法区的运行时数据结构
    • 在方法区(元空间)存储类的静态结构(如类名、字段、方法、父类、接口等)。
  3. 在堆中生成 Class 对象
    • 创建一个 java.lang.Class 实例,作为方法区数据的访问入口。

2. 链接

  1. 验证:验证类是否符合JVM规范(安全性检查)
  2. 准备:为static变量分配空间,设置默认值
    • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成。
      • 如果static变量是final基本类型以及字符串常量:编译阶段就确定了,赋值在准备阶段完成
      • 如果static变量是final的,但是属于引用类型,赋值也会在初始化阶段完成
  3. 解析:将常量池中的符号引用解析为直接引用。(用符号描述目标转变为用他们在内存中的地址描述他们)

3. 初始化

<cint>()V方法

初始化即调用 <cint>()V方法,虚拟机会保证这个类的构造方法的线程安全

发生的时机

类的初始化是懒惰的。

  • main方法所在的类,优先被初始化
  • 首次访问这个类的静态变量或静态方法
  • 子类初始化时,如果父类还没初始化,会先初始化父类
  • 子类访问父类的静态变量,只会触发父类的初始化。
  • 执行Class.forName
  • new会导致初始化

不会导致初始化:

  • 访问类的static final静态常量(基本类型和字符串),不会触发初始化
  • 类对象.class不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的loadClass方法,不会触发初始化
  • Class.forName的参数2为false时,不会触发初始化
public class Load01 {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;static {System.out.println("init E");}
}

懒惰初始化单例模式

public class Load02 {public static void main(String[] args) {Singleton.test();System.out.println(Singleton.getInstance()); // 懒汉式,只有调用getInstance()方法时,才会加载内部的LazyHolder}
}
class Singleton {// 私有构造方法private Singleton(){}public static void test() {System.out.println("test");}private static class LazyHolder {private static Singleton SINGLETON = new Singleton();static {System.out.println("LazyHolder init");}}public static Singleton getInstance() {return LazyHolder.SINGLETON;}
}

类加载器

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为Bootstrap
Application ClassLoaderclasspath上级为Extension
自定义类加载器自定义上级为Applicaiton

启动类加载器

启动类加载器是由C++程序编写的,不能直接通过java代码访问,如果打印出来的是null,说明是启动类加载器。

public class Load03 {public static void main(String[] args) throws ClassNotFoundException {Class<?> aClass = Class.forName("pers.xiaolin.jvm.load.F");System.out.println(aClass.getClassLoader()); // null}
}
public class F {static {System.out.println("bootstarp F init");}
}

使用java -Xbootclasspath/a:. pers.xiaolin.jvm.load.Load03将这个类加入bootclasspath之后,输出null,说明是启动类加载器加载的这个类

  • java -Xbootclasspath:<new bootclasspath>
  • java -Xbootclasspath/a:<追加路径>
  • java -Xbootclasspath/p:<追加路径>

应用程序类加载器

public class Load04 {public static void main(String[] args) throws ClassNotFoundException {Class<?> aClass = Class.forName("pers.xiaolin.jvm.load.G");System.out.println(aClass.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2(应用程序类加载器)}
}public class G {static {System.out.println("G init");}
}

双亲委派模式

双亲委派:调用类加载器loadClass方法时,查找类的规则。

每次都去上级类加载器中找,如果找到了就加载,如果上级没找到,才由本级的类加载器进行加载。

执行流程

protected Class<?> loadClass(String name, boolean resolve) {synchronized (getClassLoadingLock(name)) {// 1. 检查类是否已加载Class<?> c = findLoadedClass(name);if (c == null) {try {// 2. 委托父加载器加载if (parent != null) {c = parent.loadClass(name, false);} else { // parent == null,说明到了启动类加载器c = findBootstrapClassOrNull(name); // 父加载器是 Bootstrap}} catch (ClassNotFoundException e) {}// 3. 父加载器未找到,则自行加载if (c == null) {c = findClass(name);}}return c;}
}

核心作用

  1. 避免类重复加载:确保一个类在JVM中只存在一份(由最顶层的类加载器优先加载),如果用户自己定义了一个java.lang.String,那么这个类并不会被加载,而是由最顶层的Bootstrap加载核心的String类
  2. 保证安全性:防止核心类被篡改,通过优先委托父类加载器,确保核心类由可信源加载
  3. 分工明确:Bootstrap(加载JVM核心类)、Extension(加载扩展功能)、Application(加载用户代码)

破坏双亲委派场景

双亲委派并非强制约束,有些情况也会破坏它,否则有些类他是找不到的。

  1. 核心库(JDBC)需要调用用户实现的驱动(mysql-connector-java)

通过Thread.currentThread().getContextClassLoader()获取线程上下文加载器(通常是Application ClassLoader),直接加载用户类。

  1. 不同模块可能需要隔离或共享类

自定义类加载器,按照需要选择是否委派父加载器

  1. 热部署:动态替换已经加载的类

自定义类加载器直接重新加载类,不委派父类加载器

自定义类加载器

使用场景

  1. 需要加载非classpath路径中的类文件
  2. 框架设计:都是通过接口来实现,希望解耦
  3. tomcat容器:这些类有多种版本,不同版本的类希望能隔离。

步骤

  1. 继承ClassLoader父类
  2. 要遵守双亲委派机制,重写findClass方法(注意不是重写loadClass方法,否则不会走双亲委派)
  3. 读取类文件中的字节码
  4. 调用父类的defineClass方法来加载类
  5. 使用者调用该类加载器的loadClass方法
public class Load05 {public static void main(String[] args) throws ClassNotFoundException {MyClassLoader classLoader = new MyClassLoader();// 5. 使用者调用该类加载器的loadClass方法Class<?> c1 = classLoader.loadClass("MapImpl1");Class<?> c2 = classLoader.loadClass("MapImpl1");System.out.println(c1 == c2); // trueMyClassLoader classLoader2 = new MyClassLoader();Class<?> c3 = classLoader2.loadClass("MapImpl1");System.out.println(c1 == c3); // false }
}// 1. 继承ClassLoader父类
class MyClassLoader extends ClassLoader {// 2. 重写findClass方法@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException { // name就是类名称String path = "d:\\myclasspath" + name + ".class";try {ByteArrayOutputStream os = new ByteArrayOutputStream();Files.copy(Paths.get(path), os);// 3. 读取类文件中的字节码byte[] bytes = os.toByteArray();// 4. 调用父类的defineClass方法来加载类return defineClass(name, bytes, 0, bytes.length); // byte[] -> *.class} catch (IOException e) {e.printStackTrace();throw new ClassNotFoundException("类文件未找到", e);}}
}

唯一确定类的方式应该是:包名类名类加载器相同

运行期优化

逃逸分析

现象】:循环内创建了1000个Object对象,但未被外部引用。
JIT优化】:JIT编译器(尤其是C2编译器)会通过逃逸分析(Escape Analysis)发现这些对象是方法局部作用域且未逃逸(即不会被其他线程或方法访问),因此会直接优化掉对象分配。实际运行时,这些对象可能根本不会在堆上分配内存,而是被替换为标量或直接在寄存器中处理。

public class JIT01 {public static void main(String[] args) {for(int i = 0; i < 200; ++i) {long start = System.nanoTime();for(int j = 0; j < 1000; ++j) {new Object();}long end = System.nanoTime();System.out.printf("%d\t%d\n", i, (end - start));}}
}

在运行期间,虚拟机会对这段代码进行优化。
JVM将执行状态分为5个层次:

  • 0层:解释执行
  • 1层:使用C1即时编译器编译执行(不带profiling)
  • 2层:使用C1即时编译器编译执行(带基本的profiling)
  • 3层:使用C1即时编译器编译执行(带完全的profiling)
  • 4层:使用C2即时编译器编译执行

profiling是在运行过程中收集一些程序执行状态的数据(方法的调用次数、循环次数…)
解释器:将字节码解释成机器码,下次遇到相同的字节码,仍然会执行重复的解释
即时编译器(JIT):就是把反复执行的代码编译成机器码,存储在Code Cache,下次再遇到相同的代码,直接执行,无需编译。
解释器是将字节码解释为争对所有平台都通用的机器码;JIT会根据平台类型,生成平台特定的机器码。
对于占据大部分不常用的代码,无需耗费时间将其编译成机器码,直接采取解释执行的方式;对于仅占用小部分的热点代码, 可以将其编译成机器码。(运行效率:Iterpreter < C1 < C2

方法内联

例子1

private static int square(final int i) {return i * i;
}
System.out.println(square(9));

如果发现square是热点方法,并且长度不会太长时,就会进行内联(把方法内的代码拷贝到调用位置)

System.out.println(9 * 9);

例子2

public class JIT02 {int[] elements = randomInts(1_000);int sum = 0;void doSum(int x) {sum += x;}public void test() {for(int i = 0; i < elements.length(); ++i) {doSum(elements[i]);}}
}

方法内联也会导致成员变量读取时的优化操作。

上边的test()方法,会被优化成:

public void test() {// elements.length首次读取会缓存起来 ==> int[] localfor(int i = 0; i < elements.length(); ++i) { // 后续999次,求长度(不需要访问成员变量,直接从loca中取)sum += elements; // 后续1000次,取下标(不需要访问成员变量,直接从loca中取)}
}

反射优化

1. 初始阶段:解释执行(未优化)

  • 前几次调用(约0~5次)
    • Method.invoke 会走完整的 Java反射逻辑,包括:
      • 方法权限检查(AccessibleObject)。
      • 参数解包(Object[] 转原始类型)。
      • 动态方法解析(通过JNI调用底层方法)。
    • 性能极差:单次调用耗时可能是直接调用的 20~100倍(微秒级 vs 纳秒级)。

2. 中间阶段:JIT初步优化(方法内联+膨胀阈值)

  • 调用次数达到阈值(约5~15次)
    JIT编译器(C2)开始介入优化:
    • 方法内联(Inlining)
      • 如果 foo() 是简单方法(如本例的 System.out.println),JIT会尝试内联它。
      • Method.invoke 本身 无法直接内联(因反射调用是动态的)。
    • 膨胀阈值(Inflation Threshold)
      • JVM默认设置 -XX:InflationThreshold=N(通常N=15),当反射调用超过此阈值时,JVM会生成 动态字节码存根(Native Method Accessor),替代原始反射逻辑。
      • 优化效果
        调用从JNI方式转为直接调用生成的存根代码,性能提升约 5~10倍

3. 最终阶段:动态字节码生成(最高效)

  • 超过膨胀阈值(如15次后)
    JVM为 foo.invoke() 生成专用的 字节码访问器(GeneratedMethodAccessor)
  // 伪代码:生成的动态类class GeneratedMethodAccessor1 extends MethodAccessor {public Object invoke(Object obj, Object[] args) {Reflect01.foo(); // 直接调用目标方法,绕过反射检查!return null;}}
  • 优化点
    • 完全跳过权限检查参数解包(因JVM确认方法签名固定)。
    • 通过字节码直接调用 foo(),性能接近 直接方法调用(纳秒级)。
http://www.xdnf.cn/news/14301.html

相关文章:

  • 性能优化 - 高级进阶:JVM 常见优化参数
  • Linux内核网络协议的双重注册机制:inet_add_protocol与inet_register_protosw深度解析
  • Python小酷库系列:Python中的JSON工具库(3)
  • 行为设计模式之State(状态)设计模式
  • java中常见的排序算法设计介绍
  • IDEA21中文乱码解决办法
  • ubuntu 22.04设置时区和24小时制显示——筑梦之路
  • 【详细】CUDA开发学习教程清单
  • 【深度解析】Java高级并发模式与实践:从ThreadLocal到无锁编程,全面避坑指南!
  • Arcgis中,toolbox工具箱中工具莫名报错的解决方法
  • 【速写】policy与reward分词器冲突问题(附XAI阅读推荐)
  • LeetCode--31.下一个排列
  • 行为设计模式之Strategy(策略)
  • 网络编程(HTTP协议)
  • ShenNiusModularity项目源码学习(34:总结)
  • C/C++数据结构之漫谈
  • React-router、React-router-dom、React-router-native之间的区别
  • 基于深度强化学习的智能机器人路径规划系统:技术与实践
  • Flutter 本地存储全面指南:从基础到高级实践
  • CMake实战:qmake转cmake神器 - pro2cmake.py
  • 【图像处理入门】7. 特征描述子:从LBP到HOG的特征提取之道
  • 智慧金融——解读DeepSeek在银行业务场景的应用【附全文阅读】
  • Kotlin实现文件上传进度监听:RequestBody封装详解
  • Vue 性能优化
  • Flink与Kubernetes集成
  • 数据库相关操作
  • [windows工具]OCR提取文字软件1.1使用教程及注意事项
  • Java—— ArrayList 和 LinkedList 详解
  • 【橘子的AI | 每日一课】Day4!机器学习 (ML) 基础
  • /etc/profile.d/conda.sh: No such file or directory : numeric argument required