基础
1、运行
基本流程:Java 代码 -> 编译 -> class 字节码 -> load 方法区
内存划分:运行时数据存储堆、栈(Native 栈、线程执行位置的寄存器)
栈帧:存放局部变量和操作数
- Java 栈
- 局部变量区:this 指针、方法参数(数组单元)、普通局部变量
- 字节码操作数:
2、类加载
四大引用类型:
- 类(字节流)
- 接口(字节流)
- 数组类(虚拟机直接生成)
- 泛型参数(泛型擦拭,虚拟机中不存在)
加载 -> 链接 -> 初始化
类加载器:完成查找字节流,创建类
- bootstrap class loader:启动类加载器,C++ 实现,无 Java 对象
- java.lang.ClassLoader:父类(双亲委派机制)
- extension class loader:加载 lib/ext 中的 jar 包的类(平台类加载器)
- application class loader:加载 classpath 指定路径的类
- extension class loader:加载 lib/ext 中的 jar 包的类(平台类加载器)
链接:将创建好的类合并到 Java 虚拟机中
- 验证:确保被加载的类满足虚拟机的约束条件
- 准备:为类的静态字段分配内存,构造方法表(虚方法-动态绑定)
- 解析:编译器生成符号引用,用于定位;将符号引用解析为实际引用(若解析触发到未加载的类的字段或方法,则将触发该类的加载,但不会一定触发链接和初始化)
初始化:
- 声明时赋值: 方法中初始化(此方法由虚拟机加锁,确保仅执行一次)
- 静态代码块赋值: 方法中初始化(此方法由虚拟机加锁,确保仅执行一次)
- final static 基本类型或字符串:编译器将其标记为常量值,有虚拟机完成初始化
JVM 规范何时触发初始化部分场景:
- 虚拟机启动,初始化主类
- new 指令触发
- 静态方法调用
- 静态字段访问
- 子类初始化会触发父类初始化
- 反射调用
- 接口实现类会触发包含 default 方法的接口初始化
类的唯一性:
-
类加载器实例 + 类的全路径一起确定(不同的类加载器加载同一个类,得到的是两个不同的类)
-
方法是如何被执行调用的
- 虚拟机识别方法:类名 + 方法名 + 方法描述符(参数类型 + 返回类型)
- 重载:基于方法描述符(静态绑定/编译时多态;
JVM 认为是解析时能够直接识别目标方法) - 重写:动态绑定(
JVM 认为是运行时能够根据调用者的动态类型识别目标方法) - 方法调用指令:
- invoke static:静态调用【静态绑定】
- invoke special:接口默认实现调用、super 父类调用、私有实例调用【静态绑定】
- invoke virtual:非私有实例调用【动态绑定】
- invoke interface:接口调用【动态绑定】
- invoke dynamic:动态调用
- 方法句柄:
- 参数类型 + 返回类型构成,能够直接执行引用
- 没有反复检查权限的开销
- 间接调用,无法内联
- 第一次动态调用 jvm 将执行所对应的启动方法并绑定,再次执行则直接调用已绑定的调用点
- 方法句柄:
- 方法表:数组-指向当前类及祖先类中的
非私有实例方法- 特征
- 子类包含父类所有方法
- 子类重写方法与父类方法索引相同(动态绑定中
实际引用即是方法表索引值)
- 虚方法表
- 接口方法表
- 特征
3、异常处理
- 抛出异常
- 显式: throw
- 隐式:虚拟机主动抛出异常状态
- 捕获异常
- try:异常监控
- catch:捕获指定类型的异常
- finally:一定执行(或 catch 之后运行)
- Throwable
- Error:终止线程、虚拟机,不可恢复【非检查类型异常】
- Exception:
- RuntimeException:【非检查类型异常】
- 其他异常【检查类型异常】
- 异常表
- 每个方法都有,每一条数据表示一个异常处理器
- 异常处理器:from、to(监控范围)、target(起始位置) 指针和异常类型【指针值为字节码索引】
- 抛出异常时,遍历当前方法异常表,匹配成功则跳转到 target 指针处,最坏情况遍历当前线程所有方法的异常表
4、如何反射
- method.invoke
- 实际由 methodAccessor 实现
- native 实现
- 委派实现【最终还是调用 native 实现】
- 动态生成字节码实现【直接使用 invoke 调用目标方法,免去 java -> c++ -> java 的转换过程,热点代码快,如果是仅调用一次还是 native 实现快】
- 反射调用次数阈值:
Dsun.reflect.inflationThreshold15,调用次数小于阈值则采用 native 实现;大于或等于阈值使用动态生成字节码实现 - 反射开销
-
class.forname 调用 native 方法
-
class.getMethod 遍历当前类和父类的公有方法,得到一份结果的拷贝,消耗堆空间
-
变长参数 object 数组基本类型的装箱和拆箱
-
方法内联
-
- 实际由 methodAccessor 实现
- Method.setAccessible 可绕过 java 访问权限检查
5、内存布局
- 新建对象方式:调用构造器时将优先调用父类构造器直至 object 类
- 反射机制
- 调用构造器初始化实例字段
- new 指令
- 调用构造器初始化实例字段
- object.clone
- 复制已有数据
- 反序列化
- 复制已有数据
- unsafe.allocateInstance
- 没有初始化实例字段
- 反射机制
- java 对象
- 对象头
- 标记字段:存储有关当前对象的运行数据
- 类型指针:指向当前对象所属的类
- 压缩指针
- 字段重排序
- 对象头
6、垃圾回收
- 对象回收分析
- 引用计数:无法解决循环引用造成的泄漏问题
- 可达性分析:解决循环引用问题
- stop-the-world:传统方法,垃圾回收开始时,停止非垃圾回收线程工作,使用
安全点实现 - java 线程工作状态
- 执行 JNI 代码
- 解释执行字节码
- 执行即时编译器生成机器码
- 线程阻塞【安全点】
- 垃圾回收方式
- 清除(跟踪回收):
- 容易产生内存碎片【堆中的对象内存分配是连续的】
- 分配效率低【需要遍历空闲列表查找可分配的内存】
- 压缩(标记清除):
- 把存活对象聚集
- 解决内存碎片问题
- 压缩算法开销大
- 复制(复制收集):
- 内存划分为 from(分配内存)、to(存活对象使用的内存)
- 解决内存碎片问题
- 可使用内存减半
- 清除(跟踪回收):
- 分代回收思想
- 新生代:存储新建对象,minor GC 频繁
- Eden 区
- 2个 Survivor 区【from、to 区】
- 老年代:
- 存储存活时间长的对象
- 会进行全堆性的 GC,耗时长
- 卡表:存在老年代到新生代的引用,避免老年代全局扫描
- 新生代:存储新建对象,minor GC 频繁
编译
1、内存模型
- 多线程赋值乱序可能:
- 编译器重排需
- 处理器乱序执行
- 内存系统重排序
- as-if-serial:
- 单线程下,重排序执行和顺序执行的结果保持一致
- happens-before:线程内和线程外
- 描述两个操作的可见性
- 线程间的操作例子
- 解锁之后的加锁操作
- volatile 字段的写对应的读操作
- 线程启动、线程终止、线程中断
- 构造器的第一个操作
- 内存模型底层实现:内存屏障-禁止重排序
- 锁-强制刷新缓存:使得当前线程所修改的内存对其他线程可见
- volatile:可强制刷新缓存,每次访问均是直接从内存中读写
2、synchronized
- 此代码块编译生成以下指令 锁对象:拥有一个锁计数器和指向该锁的线程的指针
锁计数器:为了允许同一线程可重复获得锁
- 加锁:monitorenter指令
- 执行时检测锁计数器为0,则该锁没有被其他线程持有,那么将线程指针指向当前线程,锁计数器+1
- 执行时检测锁计数器不为0,再检测锁线程指针是否为当前线程,如是当前线程则锁计数器+1,否则等待直到持有锁的线程释放锁
- 解锁:monitorexit指令
- 正常退出或者异常执行都需要解锁
- 执行时锁计数器-1,当值为0时表示锁已经被释放
锁的类型
-
重量锁
- 阻塞加锁失败的线程,锁释放时唤醒被阻塞的线程
- 线程唤醒需要用户态和内核态的切换,消耗大
- 自旋状态:处理器上轮询检查锁是否被释放,若释放锁则直接获取锁,无需进入阻塞状态
- 自适应自旋:根据以往的等待时是否获得锁动态调整自旋时间
- 缺点:不公平的锁机制,导致阻塞线程抢锁机会少
-
轻量锁
- 加锁:
- 先判断对象头中标记字段最后两位是否为重量锁,若不是,则在当前线程栈帧划分锁记录空间并存储锁对象的标记字段
- 然后使用 CAS 替换锁对象的标记字段;CAS 检测当前标记字段最后两位为00,则获得锁;否则,若当前线程持有该锁,则清除锁记录,重新获取锁,若是其他线程持有该锁,则升级为重量锁,当前线程阻塞
- 解锁:
- 若当前锁记录值为 0 ,则直接返回
- 否则进行 CAS 检测比较锁对象的标记记录是否为当前锁记录,若是则替换值并释放锁;若不是则进入重量锁的释放,唤醒被阻塞的线程
- 加锁:
-
偏向锁
- 从始至中只有一个线程来请求该锁,当锁请求时判断锁对象标记字段是否当前线程且epoch值是否相同,满足则持有该锁
- 当 epoch 撤销次数达到阈值,表明不在合适偏向锁,则升级为轻量锁
3、语法糖
自动装箱、拆箱
- 类型缓存值,否则创建一个新的对象
- 缓存最高值可以调整,最低值不能调整
泛型擦拭
- java 程序上的范型信息在 JVM 内部全部丢失,为了兼容范型之前的代码
- 并不是所有范型擦拭之后都变成 object,而是都变成锁限定的继承类
4、即时编译
以方法为单位的即时编译
字节码翻译方式:
- 解释执行(逐条翻译并执行,需要等待翻译完成)
- 即时编译(按照方法为单位,翻译完成后执行)
- 内联缓存优化:加快动态绑定【缓存调用者类型和目标方法】
- 方法内联优化:消除方法调用的固定开销(固定开销:方法执行位置、创建栈帧、栈帧入栈和出栈带来的消耗)
- 混合模式(热点代码采用即时翻译)
分层编译(独立线程执行):
- C1:适合对启动性能有要求的,优化简单,编译时间短(客户端)
- C2:适合对峰值性能有要求的,优化复杂,编译时间长(服务器)
- Graal:实验性
JVM 执行状态:
- 解释执行
- 执行不带 profiling 的 C1 代码
- 执行仅带方法调用次数和循环次数 profiling 的 C1 代码
- 执行带所有 profiling 的 C1 代码
- 执行 C2 代码
profiling:收集能反应程序执行状态的数据
java8 默认开启分层编译,关闭分层编译是直接使用 C2 编译。 循环回边:往回跳转指令
OSR 编译(以循环为单位的编译):
- 判断一个方法是否为热点代码
- 方法的调用次数
- 循环回边的执行次数
5、中间表达形式 IR java源代码到机器码:
- java 编译器将源代码编译成 java 字节码
- 即时编译器将字节码编译成机器码
6、方法内联
编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围内,取代原方法调用的优化
强制内联:@forceinline 注解指定
禁止内联:@dontinline 注解指定
native 方法不会被内联
去虚化:对虚方法调用去虚,转换为一个或者多个直接调用,再进行方法内联
- 完全去虚化
- 通过类型推导和类层次分析,识别虚方法调用的
唯一目标方法 - 推导调用者的动态类型
- 通过分析 JVM 所有已被加载的类,判断某个抽象方法或者接口方法是否只有一个实现,如果是则直接调用其具体实现
- 如果使用使用 final 关键字修饰类,则表示没有子类,可以进行去虚化和方法内联
- 通过类型推导和类层次分析,识别虚方法调用的
- 条件去虚化
- 将虚方法调用转换为若干个类型测试以及直接调用
即时编译器如何决定方法是否被内联:
- 方法调用层数
- 放啊调用指令所在程序的路径热度
- 目标方法调用次数和方法大小
- IR 图大小
注解 @hotspotIntrinsicCandidate 方法虚拟机额外维护一套高效实现,高效实现依赖具体的 CPU 指令
7、逃逸分析
确定指针动态范围的静态分析,可以分析在程序的那些地方可以访问到指针
判断对象是否逃逸的依据:
- 对象是否被放入堆中(静态字段、堆中的实例字段)【堆中的对象能够被其他线程获取其引用】
- 对象是否被传入未知代码【未知代码:方法中未被内联的方法调用】
逃逸分析结果的用处:
- 用于锁消除:若该对象不逃逸,则可以不用给此对象进行加锁、解锁操作
- 用于栈分配:若该对象不逃逸,则新建的对象可以分配到栈空间,并在 new 语句所在的方法退出时,通过弹出当前方法栈帧来自动回收所分配的栈空间【所以为 new 指令创建的对象分配栈空间是有可能的,
但是,hotspot 虚拟机并没有这么做】 - 用于标量替换:将原本对对象的字段的访问替换为一个个局部变量的访问
标量:只能存储一个值【局部变量】
聚合量:可同时存储多个值【对象】
代码优化
1、字段访问优化
即时编译器优化静态字段访问:减少总的内存访问次数
字段读取优化:减少内存访问次数,替换原先内存访问数据读取
字段存储优化:如果一个字段先后连续被存储多次,则消除之前的存储【如果存储字段被 volatile 标记,那么即时编译器不能将冗余的存储操作消除】
死代码消除:【局部变量的冗余存储】
不可达分支消除
2、循环优化
循环无关代码外提:循环体内,没有使用到循环对象的变量或语句应该外提
循环展开:循环体中重复多次循环迭代,并减少循环次数的编译优化
C2 中只有
计数循环能够被展开:1、维护一个循环计数器
2、循环计数器类型为 int\short\char,不能为其他
3、循环计数器增量为常数
4、循环计数器的上限、下限是与循环无关的数值
循环判断外提:将循环中的 if 语句外提
循环剥离:将循环的前或者后几个迭代剥离出循坏
附加
字节码修改工具:
- AsmTools
- ASM:asm.ow2.io/ 类字段排序查看工具:
- JOL javap:查看字节码