1. 类加载阶段
类加载器 :JVM只会运行二进制文件,类加载器的作用就是将 字节码文件加载到JVM中,从而让Java程序能够启动起来
1.1 加载
- 将类的字节码载到方法区中,使用C++的 instanceKlass 描述java类。关键字段包括:
- _ java_mirror: 即java类镜像,例如对String来说,就是String.class,作用是把klass暴露给java使用
- _super :父类信息
- fields :成员变量
- methods :方法
- constants :常量池
- class_loader :类加载器
- _vtable :方法表
- _itable: 接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
注意
- instanceKlass 这样的元数据存储在方法区(Java 8 之后为元空间),而 _java_mirror 存储在堆中。
- 可通过 HSDB 工具查看类的加载信息。
1.2 连接
- 验证:确保类符合 JVM 规范,进行安全性检查。主要包括以下方面:
(1)文件格式验证
(2)元数据验证
(3)字节码验证
(4)符号引用验证 Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,检查它们是否存在 - 例如,使用支持二进制的编辑器修改 HelloWorld.class 的魔数,然后在控制台运行。
- 准备:为static分配空间,设置默认值
- JDK 7 之前,static 变量存储在 instanceKlass 末尾(方法区);从 JDK 7 开始存储在 _java_mirror 末尾(堆)。
- 分配空间与赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成。
- 如果 static 变量是 final 的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成;
- 引用类型的 final 变量则在初始化阶段赋值。
- 解析:将常量池中的符号引用解析为直接引用。
比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
1.3 初始化
对类的静态变量、静态代码快执行初始化操作
- 初始化通过调用 cinit() 方法实现,JVM 确保线程安全。
- 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
初始化时机:
概括得说,类初始化是[懒惰的]
- main 方法所在的类总是首先初始化。
- 首次访问类的静态变量或静态方法时。
- 子类初始化时,如果父类未初始化,会触发父类初始化。
- 子类访问父类的静态变量,仅触发父类初始化。
- 使用 Class.forName 或 new 关键字。
不会触发初始化的情况:
- 访问类的 static final 常量(基本类型和字符串)。
- 类对象 .class。
- 创建该类的数组。
- 类加载器的 loadClass 方法。
- Class.forName 的参数为 false 时。
2. 类加载器
JDK自带有三个类加载器: bootstrap ClassLoader(启动类加载器)、Extension ClassLoader(扩展类加载器)、Application ClassLoader(应用程序类加载器)。
2.1 启动类加载器
- BootStrapClassLoader是ExtClassLoader的父类加载器,c++写的,不能通过Java代码获取实例,默认负责加载Java的核心库,如 %JAVA_ HOME%/jre/lib 下的jar包和class文件。
2.2 扩展类加载器
- ExtClassLoader是ApplicationClassLoader的父类加载器,负责加载 %JAVA_ HOME%/lib/ext 文件夹下的jar包和class类。
2.3 应用类加载器
- Application ClassLoader是自定义类加载器的父类,负责加载 classpath 下的类文件。
2.4 双亲委派模式
何为双亲委派?
所谓的双亲委派, 就是指优先委派上级类加载器进行加载,如果上级类加载器
①能找到这个类,由上级加载,加载后该类也对下级加载器可见
②找不到这个类,则下级类加载器才有资格执行加载
JVM为什么采用双亲委派机制?
- 通过双亲委派机制可以避免某一个类被重复加载, 当父类已经加载后则无需重复加载,保证唯一性。
- 让类的加载有优先次序, 保证核心类优先加载
- 为了安全,保证类库API不会被修改
- 类加载器调用 loadClass 方法时的查找规则。这里的“双亲”更适合翻译为上级,因为它们没有继承关系。
子类加载器有一个成员变量 parent 指向父加载器,loadClass 方法会调用父类。
## 2.5 自定义类加载器
Q:何时需要自定义类加载器?
- 加载非 classpath 路径中的类文件。
- 通过接口使用实现,常用于框架设计以实现解耦。
- 隔离不同应用中的同名类,避免冲突,常见于 Tomcat 容器。
步骤:
- 继承 ClassLoader。
- 遵循双亲委派机制,重写 findClass 方法(注意不要重写 loadClass)。
- 读取类文件的字节码。
- 调用父类的 defineClass 方法加载类。
- 使用者调用该类加载器的 loadClass 方法。
Q:自己编写类加载器就能加载一个假冒的java.lang.System吗?
不行。
①假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的java.lang.System,自然不会加载假冒的
②假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的java.lang.System时,它需要先加载父类java.lang.Object,而你没有用委派,找不到java.lang.Object所以加载会失败
③以上也仅仅是假设。实际操作你就会发现自定义类加载器加载以java.打头的类时,会抛安全异常,在jdk9以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了
3. 运行期优化
3.1 即时编译
分层编译
原因是什么呢?
JVM将执行状态分为5个层次:
- 0层,解释执行(Interpreter)
- 1层,使用C1即时编译器编译执行(不带profiling)
- 2层,使用C1即时编译器编译执行(带基本的profiling)
- 3层,使用C1即时编译器编译执行(带完全的profiling)
- 4层,使用C2即时编译器编译执行
tip: profiling是指在运行过程中收集一些程序执行状态的数据,例如[方法的调用次数],[循环的回边次数]等
即时编译器(JIT) 与 解释器 的区别 - 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT是将一些字节码编译为机器码,并存入Code Cache, 下次遇到相同的代码,直接执行,无需再编译
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT会根据平台类型,生成平台特定的机器码
PS: 对于不常用的代码,使用解释执行;对于热点代码,使用即时编译以提高性能。
优化方法:
- 逃逸分析:检测新建的对象是否逃逸,可以使用 -XX:-DoEscapeAnalvsis 关闭。
- 方法内联:将热点方法的代码拷贝、粘贴到调用位置。
- 字段优化:优先使用局部变量而非成员变量和静态成员变量。
3.2 反射优化
反射的使用会影响性能,因此应合理使用。