类文件结构
| 组成部分 | 长度 | 描述 |
|---|---|---|
| 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可以修改膨胀阈值