类加载阶段
1. 加载
- 加载:将类的字节码载入方法区中,内部采用C++的instanceKlass描述java类。
- 如果这个类的父类还没加载,则先加载父类
- 加载和链接可能是交替运行的
- 通过全限定名获取字节码
- 从文件系统(
.class
文件)、JAR 包、网络、动态代理生成等途径读取二进制数据。
- 从文件系统(
- 将字节码解析为方法区的运行时数据结构
- 在方法区(元空间)存储类的静态结构(如类名、字段、方法、父类、接口等)。
- 在堆中生成
Class
对象- 创建一个
java.lang.Class
实例,作为方法区数据的访问入口。
- 创建一个
2. 链接
- 验证:验证类是否符合JVM规范(安全性检查)
- 准备:为static变量分配空间,设置默认值
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成。
- 如果static变量是final基本类型以及字符串常量:编译阶段就确定了,赋值在准备阶段完成
- 如果static变量是final的,但是属于引用类型,赋值也会在初始化阶段完成
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成。
- 解析:将常量池中的符号引用解析为直接引用。(用符号描述目标转变为用他们在内存中的地址描述他们)
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 ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为Bootstrap |
Application ClassLoader | classpath | 上级为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;}
}
核心作用
- 避免类重复加载:确保一个类在JVM中只存在一份(由最顶层的类加载器优先加载),如果用户自己定义了一个
java.lang.String
,那么这个类并不会被加载,而是由最顶层的Bootstrap加载核心的String类 - 保证安全性:防止核心类被篡改,通过优先委托父类加载器,确保核心类由可信源加载
- 分工明确:Bootstrap(加载JVM核心类)、Extension(加载扩展功能)、Application(加载用户代码)
破坏双亲委派场景
双亲委派并非强制约束,有些情况也会破坏它,否则有些类他是找不到的。
- 核心库(JDBC)需要调用用户实现的驱动(mysql-connector-java)
通过
Thread.currentThread().getContextClassLoader()
获取线程上下文加载器(通常是Application ClassLoader
),直接加载用户类。
- 不同模块可能需要隔离或共享类
自定义类加载器,按照需要选择是否委派父加载器
- 热部署:动态替换已经加载的类
自定义类加载器直接重新加载类,不委派父类加载器
自定义类加载器
使用场景
- 需要加载非classpath路径中的类文件
- 框架设计:都是通过接口来实现,希望解耦
- tomcat容器:这些类有多种版本,不同版本的类希望能隔离。
步骤
- 继承ClassLoader父类
- 要遵守双亲委派机制,重写findClass方法(注意不是重写loadClass方法,否则不会走双亲委派)
- 读取类文件中的字节码
- 调用父类的defineClass方法来加载类
- 使用者调用该类加载器的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倍。
- JVM默认设置
- 方法内联(Inlining):
3. 最终阶段:动态字节码生成(最高效)
- 超过膨胀阈值(如15次后):
JVM为foo.invoke()
生成专用的 字节码访问器(GeneratedMethodAccessor):
// 伪代码:生成的动态类class GeneratedMethodAccessor1 extends MethodAccessor {public Object invoke(Object obj, Object[] args) {Reflect01.foo(); // 直接调用目标方法,绕过反射检查!return null;}}
- 优化点:
- 完全跳过权限检查 和 参数解包(因JVM确认方法签名固定)。
- 通过字节码直接调用
foo()
,性能接近 直接方法调用(纳秒级)。