JVM类加载与字节码技术(类文件结构、字节码指令、编译期处理、类加载阶段、类加载器、运行期优化)

类文件结构

组成部分 长度 描述
Magic Number 4 字节 0xCAFEBABE,用于标识这是一个 Java Class 文件
Minor / Major Version 4 字节 表示编译该类的 JDK 版本号(52 对应 JDK 8)
Constant Pool Count 2 字节 常量池中入口的数量
Constant Pool 不定 存放字面量和符号引用,是类文件的核心数据区
Access Flags 2 字节 类的访问标志(public、abstract、interface 等)
This / Super Class 4 字节 当前类和父类在常量池中的索引
Interfaces Count / Array 不定 当前类实现的接口信息
Fields Count / Array 不定 成员变量信息(名称、类型、修饰符等)
Methods Count / Array 不定 方法表,包含方法的字节码指令
Attributes Count / Array 不定 附加信息(源码文件名、注解、调试信息等)

字节码指令

自己分析类文件结构太麻烦了,oracle提供了javap工具来反编译class文件

javap -v Helloworld.class

++前置和后置的区别是先++(在slot槽中完成)还是先放操作数栈

编译器会按照从上到下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法<cinit>()V,方法会在类加载的初始阶段被调用

编译器会按从上到下的顺序,收集所有{}代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后

final修饰属性:

基本类型:只能赋值一次,之后不能再修改

应用类型:地址不能变,但内容可以变

final修饰方法:

不能被子类重写

final修饰类:

类不能被继承

除了public方法在运行时确定使用哪个对象(多态),其他均是当前对象

当执行invokevirtual指令时

  • 先通过栈帧中的对象引用找到对象
  • 分析对象头,找到对象的实际class
  • class结构中有vtable,他在类加载的链接阶段就已经根据方法的重写规则生成好了
  • 查表得到方法的具体地址
  • 执行方法的字节码

try中有return语句,finally里面代码也会执行

如果finally中出现了return,会吞掉异常(避免在finally中写return)

方法层面的synchronized不会在字节码层面体现

编译期处理

将.java文件编译为.class字节码过程中,自动生成和转换的代码(语法糖)

  • 默认构造器(前提没有自己写构造器)
  • 自动拆装箱(-128~128直接的对象是复用的,其他是new新对象)
  • 泛型集合取值(泛型擦除的是字节码里的泛型信息,局部变量类型表中还保存了泛型类型信息,但是不能通过反射拿到,只有在方法参数和返回值上的泛型才能反射获得)
  • 可变参数(实质上是根据参数数量,创建对应数组,如果没有参数,创建空数组)
  • foreach循环(数组本质还是for循环,集合本质是迭代器)(所有实现了Iterable接口的集合都可以使用)
  • switch可以使用字符串和枚举类型(String底层用的hashCode(快速判断)配合equals(哈希冲突),枚举底层使用的合成类(根据枚举编号对应一个整数匹配))
  • try-with-resources(就是资源变量在try括号内创建 ,资源对象需要实现AutoCloseable接口,使用后不用使用finally语句块,编译器自动生成资源释放代码)
  • 方法重写时的桥接方法(重写方法的返回值可以是父类的子类)
  • 匿名内部类(其实是生成了新类,并且引用局部变量要是final修饰)

类加载阶段

加载

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

instanceKlass这样的元数据是存储在方法区,_java_morror存储在堆中

链接

  • 验证:验证类是否符合JVM规范,安全性检查
  • 准备:为static变量分配空间,设置默认值
    • static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
    • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
    • 如果static变量是final的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段 完成
    • 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
  • 解析:将常量池中的符号引用解析为直接引用(注意loadClass方法不会导致类的解析和初始化)

初始化

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

类的初始化是懒惰的

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

不会导致类初始化的情况

  • 访问类的static final静态常量(基本类型和字符串)
  • 类对象.class不会触发初始化
  • 创建该类的数组
  • 类加载器的loadClass方法
  • Class.forName的参数2为false时

类加载器

类加载器 层级 是否 Java 实现 加载范围 说明
Bootstrap ClassLoader(启动类加载器) 最顶层 否(C/C++) JDK 核心类库(java.lang、java.util 等) 最核心的加载器,Java 程序的基础
Extension ClassLoader(扩展类加载器) 第二层 扩展类库(原 ext 目录 / 部分模块) 加载 JDK 扩展功能(Java 9 后弱化)
Application ClassLoader(应用类加载器) 第三层 classpath 下的类 平时写的代码基本由它加载
Custom ClassLoader(自定义类加载器) 最底层 用户自定义 用于热部署、插件化、隔离加载等
bootstrap classLoader加载器无法直接访问(C++代码)

Class.forName(className) 加载链接初始化

loader.loadClass(className) 加载

类加载器:负责把 .class 字节码加载进 JVM,并转成 Class 对象的组件

不同类加载器负责"从不同的位置加载类"

双亲委派模型:在调用类加载器的 loadClass() 方法加载类时,类加载器不会先自己加载,而是先将请求委派给父类加载器去完成,只有当父类加载器无法加载时,当前加载器才会尝试加载该类的一种查找规则。

线程上下文类加载器

MySQL 驱动是由 Application ClassLoader 加载的,在 JDBC 中,DriverManager 由 Bootstrap ClassLoader 加载,但它通过线程上下文类加载器获取 Application ClassLoader,从而完成对驱动类的加载。

DriverManager 本身是 Bootstrap 加载的,但它不会直接加载驱动,而是通过线程上下文类加载器间接完成驱动加载。

ServiceLoader :SPI

约定如下:在jar包的META-INF/services包下,一接口全限定名为文件,文件内容是实现类的名称

SPI 的设计是为了让由 Bootstrap ClassLoader 加载的核心类,能够加载并使用由 Application ClassLoader 提供的实现类,从而突破双亲委派的限制,实现框架与实现的解耦。

自定义类加载器

  • 想加载非classpath随意路径中的类文件
  • 都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不用应用的同名类可以加载,不冲突,常见于tomcat容器

步骤:

  • 继承classloader父类
  • 要遵从双亲委派机制,重写findClass方法
    • 注意不是loadClass方法,否则不会走双亲委派机制
  • 读取类文件字节码
  • 调用父类的defineClass方法加载类
  • 使用者调用该类加载器的loadClass方法

判断类完全一致:包名类名一致,类加载器对象也一致

运行期优化

Java先编译成字节码,再由JVM解释执行,并对热点代码进行即时编译优化

JIT 在运行时会对热点代码进行多种优化,主要包括方法内联、逃逸分析以及基于逃逸分析的锁消除和标量替换,同时还会进行循环优化、死代码消除、常量折叠等。此外,JIT 还会通过去虚拟化将虚方法调用转为直接调用,从而进一步提升执行效率。"

即时编译

JVM将执行状态分为5个层次

  • 0层,解释执行
  • 1层,使用C1即使编译器编译执行(不带profiling)
  • 2层,使用C1即使编译器编译执行(带基本profiling)
  • 3层,使用C1即使编译器编译执行(带完全profiling)
  • 4层,使用C2即使编译器编译执行
    profiling是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等

即时编译器(JIT):将热点代码编译成机器码,已达到理想的运行速度

逃逸分析是 JVM 在 JIT 编译阶段的一种优化技术,用来分析对象的作用范围,从而决定是否进行栈上分配、标量替换以及锁消除等优化。

方法内联

如果发现一个方法是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置

还能够进行常量折叠的优化

字段优化

方法内联会进行字段读取优化,将成员变量赋值给局部变量(隐式),加快读取速度

反射优化

反射慢的主要原因不是查找类或方法,而是由于调用是动态的,JVM 在编译期无法确定目标方法,因此难以进行内联等JIT优化。同时反射调用链路更长,并且存在权限检查、参数封装等额外开销。JVM 为此通过 inflation 机制,在高频调用时生成字节码来优化性能

native 方法不一定是和操作系统交互,它只是表示方法由 JVM 外部的语言实现,通常是 C/C++。有些 native 方法确实用于调用操作系统接口,但也有很多是为了操作 JVM 内部结构或提升性能,例如反射底层实现就依赖 native,而不直接涉及操作系统。

反射在初期使用 native 方法的目的,是避免频繁生成字节码带来的开销,同时借助 JVM 内部能力完成动态方法调用。

通过查看RedlectionFactory源码可知

  • sum.reflect.noInflation可以用来禁止膨胀(直接生成GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
  • sum.reflect.inflationThresshold可以修改膨胀阈值
相关推荐
江不清丶2 小时前
生产实战:系统频繁Full GC,如何一步步定位与解决?
java·jvm
吃不胖爹2 小时前
宝塔部署前后端时,配置域名与ssl证书
java·jvm
budingxiaomoli3 小时前
JVM常见面试题总结
jvm
青衫码上行4 小时前
【从零开始学习JVM】内存模型+堆栈的区别
java·jvm·学习·面试
Y4090011 天前
【多线程】Thread 类
java·开发语言·jvm
東雪木1 天前
Java学习——重载 (Overload) 与重写 (Override) 的核心区别、底层实现规则
java·开发语言·jvm·学习·java面试
爱丽_1 天前
JVM GC 调优:内存指标、泄漏排查与线上自救
java·开发语言·jvm
LSL666_1 天前
JVM面试题——垃圾回收GC
java·开发语言·jvm