【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时,不会触发初始化
java 复制代码
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");
    }
}

懒惰初始化单例模式

java 复制代码
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 ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为Bootstrap
Application ClassLoader classpath 上级为Extension
自定义类加载器 自定义 上级为Applicaiton

启动类加载器

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

java 复制代码
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:<追加路径>

应用程序类加载器

java 复制代码
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方法时,查找类的规则。

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

执行流程

java 复制代码
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方法
java 复制代码
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); // true

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        System.out.println(c1 == c3); // false 
    }
}// 1. 继承ClassLoader父类
class MyClassLoader extends ClassLoader {
    // 2. 重写findClass方法
    @Override
    protected 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)发现这些对象是方法局部作用域且未逃逸(即不会被其他线程或方法访问),因此会直接优化掉对象分配。实际运行时,这些对象可能根本不会在堆上分配内存,而是被替换为标量或直接在寄存器中处理。

java 复制代码
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

java 复制代码
private static int square(final int i) {
	return i * i;
}
System.out.println(square(9));

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

java 复制代码
System.out.println(9 * 9);

例子2

java 复制代码
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()方法,会被优化成:

java 复制代码
public void test() {
	// elements.length首次读取会缓存起来 ==> int[] local
	for(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)
java 复制代码
  // 伪代码:生成的动态类
  class GeneratedMethodAccessor1 extends MethodAccessor {
      public Object invoke(Object obj, Object[] args) {
          Reflect01.foo(); // 直接调用目标方法,绕过反射检查!
          return null;
      }
  }
  • 优化点
    • 完全跳过权限检查参数解包(因JVM确认方法签名固定)。
    • 通过字节码直接调用 foo(),性能接近 直接方法调用(纳秒级)。
相关推荐
风起云涌~19 分钟前
【Java】BlockQueue
java·开发语言
北执南念1 小时前
JDK 动态代理和 Cglib 代理的区别?
java·开发语言
盛夏绽放1 小时前
Python 目录操作详解
java·服务器·python
贰拾wan1 小时前
ArrayList源码分析
java·数据结构
Code季风1 小时前
跨语言RPC:使用Java客户端调用Go服务端的JSON-RPC服务
java·网络协议·rpc·golang·json
豆沙沙包?1 小时前
2025年- H82-Lc190--322.零钱兑换(动态规划)--Java版
java·算法·动态规划
都叫我大帅哥2 小时前
背压(Backpressure):响应式编程的“流量控制艺术”
java·flux
浮游本尊2 小时前
Java学习第5天 - 输入输出与字符串处理
java
阿杰学编程2 小时前
Go 语言中的条件判断和for 循环
java·数据库·golang