JVM类加载入门指南

1. 类加载阶段

在这里插入图片描述

类加载器 :JVM只会运行二进制文件,类加载器的作用就是将 字节码文件加载到JVM中,从而让Java程序能够启动起来

1.1 加载

在这里插入图片描述

  1. 将类的字节码载到方法区中,使用C++的 instanceKlass 描述java类。关键字段包括:
  • _ java_mirror: 即java类镜像,例如对String来说,就是String.class,作用是把klass暴露给java使用
  • _super :父类信息
  • fields :成员变量
  • methods :方法
  • constants :常量池
  • class_loader :类加载器
  • _vtable :方法表
  • _itable: 接口方法表
  1. 如果这个类还有父类没有加载,先加载父类
  2. 加载和链接可能是交替运行的

注意

  • instanceKlass 这样的元数据存储在方法区(Java 8 之后为元空间),而 _java_mirror 存储在堆中。
  • 可通过 HSDB 工具查看类的加载信息。

1.2 连接

  1. 验证:确保类符合 JVM 规范,进行安全性检查。主要包括以下方面:
    (1)文件格式验证
    (2)元数据验证
    (3)字节码验证
    (4)符号引用验证 Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,检查它们是否存在
  2. 例如,使用支持二进制的编辑器修改 HelloWorld.class 的魔数,然后在控制台运行。
    在这里插入图片描述
  3. 准备:为static分配空间,设置默认值
  • JDK 7 之前,static 变量存储在 instanceKlass 末尾(方法区);从 JDK 7 开始存储在 _java_mirror 末尾(堆)。
  • 分配空间与赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成。
  • 如果 static 变量是 final 的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成;
  • 引用类型的 final 变量则在初始化阶段赋值。
  1. 解析:将常量池中的符号引用解析为直接引用
    比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
    在这里插入图片描述

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为什么采用双亲委派机制?

  1. 通过双亲委派机制可以避免某一个类被重复加载, 当父类已经加载后则无需重复加载,保证唯一性。
  2. 让类的加载有优先次序, 保证核心类优先加载
  3. 为了安全,保证类库API不会被修改
  • 类加载器调用 loadClass 方法时的查找规则。这里的“双亲”更适合翻译为上级,因为它们没有继承关系。
    在这里插入图片描述
    子类加载器有一个成员变量 parent 指向父加载器,loadClass 方法会调用父类。

## 2.5 自定义类加载器
Q:何时需要自定义类加载器?

  1. 加载非 classpath 路径中的类文件。
  2. 通过接口使用实现,常用于框架设计以实现解耦。
  3. 隔离不同应用中的同名类,避免冲突,常见于 Tomcat 容器。

步骤:

  1. 继承 ClassLoader。
  2. 遵循双亲委派机制,重写 findClass 方法(注意不要重写 loadClass)。
  3. 读取类文件的字节码。
  4. 调用父类的 defineClass 方法加载类。
  5. 使用者调用该类加载器的 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 反射优化
反射的使用会影响性能,因此应合理使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值