系统学习(四):JVM 笔记

196 阅读13分钟

基础

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 指定路径的类

链接:将创建好的类合并到 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.inflationThreshold 15,调用次数小于阈值则采用 native 实现;大于或等于阈值使用动态生成字节码实现
    • 反射开销
      • class.forname 调用 native 方法

      • class.getMethod 遍历当前类和父类的公有方法,得到一份结果的拷贝,消耗堆空间

      • 变长参数 object 数组基本类型的装箱和拆箱

      • 方法内联

  • 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,耗时长
      • 卡表:存在老年代到新生代的引用,避免老年代全局扫描

编译

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:查看字节码