深入拆解java虚拟机-笔记

前言

来自极客时间上的同名课程

要点摘录

1. java语言的类型

  • 基本类型,8个
  • 引用类型,又分四类
    • 接口
    • 数组类:java虚拟机直接生成,上面2种有对应的字节流
    • 泛型参数,由于会在编译过程中被擦除,所以实际只有3类

2. 类加载流程

加载

查找字节流,借助相应的classLoader加载

  • 启动类加载器
  • 其他类加载器:被启动类加载器加载
  • 双亲委派原则

链接

  • 验证
    确保满足虚拟机的约束条件
  • 准备
    为类的静态字段准备内存,构造与该类想关联的方法表。注意静态字段初始化在后面的初始化阶段
  • 解析
    将符号引用解析成实际引用(如果符号引用指向一个未加载的类,解析将触发这个类的加载,未必触发验证及准备)

初始化

为静态常量赋值(final修饰的除外),执行静态代码块。初始化时线程安全的

3. jvm识别目标方法

  • 静态绑定:在解析时便能识别目标方法的情况
  • 动态绑定:运行时根据调用者类型动态识别目标方法

4. 常用工具

  • javap:查阅java字节码. -p:打印私有,-v:输出附加信息
  • 反汇编器ASMTools

5. 反射

Method.setAccessible作用:不检查方法权限

性能开销:

  • 变长参数导致的Object数组(jvm需要根据传入参数个数new一个数组)
  • 基本类型的自动装箱
  • 方法内联不成功的情况(这条影响最大)

6. java对象内存布局

  • java对象头:每个对象都有,由标记字段和类型指针组成,各占64位(8字节),共16字节
  • 压缩指针:将原本64位的指针压缩到32位
  • 字段重排:虚拟机还会对每个类的字段进行重排序,是的字段也能够内存对齐

7. 垃圾回收

  • 引用计数, 该算法已淘汰
  • 可达性分析算法:从几个gc roots出发,搜索所有能被引用到的对象,该过程为标记。
    • 风险:多线程情况下的误报与漏报问题
  • stop-the-world: 防止在标记过程中堆栈的状态发生改变,采用安全点机制,线程到达安全点后,开始stw。
  • 垃圾回收的方式:
    • sweep清除
      • 缺点:内存碎片,分配效率低(需要逐个访问以找到足够的内存空间)
    • compact压缩
      • 缺点:压缩算法性能开销
    • copy复制: from,to块
  • TLAB(Thread Local Allocation Buffer):每个线程申请一块连续的内存,减少多线程内存分配的竞争
  • 卡表(card table):每个卡512字节,维护一个标志位,代表对应的卡是否可能存在指向新生代的引用。减小minor gc时老年代的全堆空间扫描
  • 虚共享:几个volatile关键字出现在同一缓存行

8. 内存模型

  • 即时编译器重排序
  • happens-before(volatile,锁,构造析构等)
  • 内存屏障memory barrier禁止编译器重排序,对处理器则会导致缓存刷新操作
    • 如volatile关键字,禁止volatile字段写操作之前的内存访问被重排到其之后,也禁止volatile字段读操作之后的内存访问被重排到其前
  • volatile在x86架构的实现,是通过强制刷新写缓存。无法分配到寄存器(register)

9. synchroniezd

synchroniezd锁采用计数的方式,是可重入的
实现方式,按代价由高到低

  • 重量级锁:阻塞唤醒,自适应自旋。monitorenter,monitorexit
  • 轻量级锁:CAS(compare and set),会升级,竞争较少时
  • 偏向锁:第一次请求时采用CAS,会升级,一个线程时

10. 即时编译

通常,代码会被java虚拟机解释执行。而反复执行的热点代码,会被编译成机器码,直接运行在底层硬件。

即时编译以方法为单位,依据调用次数和循环回边执行次数判断热点代码。

profiling:分为跳转指令的分支profiling和类型相关指令的类型profiling,是程序运行时的统计信息

从java8开始默认采用分层编译的方式,将执行分为五个层次
0层:解释执行
1层:没有profiling的C1
2层:部分profiling的C1
3层:全部profiling的C1
4层:C2
C1编译效率较快,C2执行效率较快,但编译慢点。1和4是终止状态

优化的核心:根据已有统计进行假设
去优化:假设失败,从机器码回到解释执行

11. java字节码

java字节码是java虚拟机使用的指令集,它与jvm基于栈的计算模型密不可分。
至于jvm为何基于栈,是因为实现简单,但无法使用底层的寄存器,所以效率不够高。

  • java方法栈帧分为操作数栈和局部变量区
  • 字节码分多种类型,如加载常量指令,操作数专用指令,局部变量区访问指令,方法调用指令,数组相关指令,控制流指令以及计算相关指令等。

12. 方法内联

定义:在编译过程中,遇到方法调用时,将目标方法的方法体纳入编译范围,并取代原方法调用的优化方法。
实现:解析过程中替换方法调用字节码,或者在IR图中替换IR节点。

内联缺点:

  • 内联越多,编译时间越长,程序达到峰值性能的时刻也被推迟
  • 导致生成的机器码变大,容易填满code cache

虚方法调用的内联:去虚化

  • 完全去虚化
  • 条件去虚化

13. 逃逸分析

定义:一种确定指针动态范围的静态分析,它可以分析在程序哪些地方可以访问到指针

判断逃逸依据:

  • 对象是否被存入堆中(个人理解:对象都是分配在堆中,但这里存入,大概指:对象被其他线程引用,才算存入)
  • 对象是否作为方法调用的调用者或者参数

根据逃逸分析结果的优化:

  • 锁消除
  • 标量替换:将原本连续分配的对象拆散为一个个单独的字段,分部在栈上或者寄存器中

部分逃逸分析:附带了控制流信息的逃逸分析

字段访问优化:

  • 沿着控制流缓存字段存储、读取的值,并在接下来直接使用(中间没有方法调用、内存屏障或其他可能存储该字段的节点)
  • 优化冗余的字段存储操作
  • 死代码消除,包括局部变量死存储消除和不可达分支消除

14. 循环优化

  • 循环无关代码外提
  • 循环展开
  • 循环判断外提
  • 循环剥离:即将特殊的循环体(通常是开头或结尾)单独剥离出来,使循环体更一致,更容易触发其他优化

15. 向量化优化

定义:借助CPU的SMID指令,通过单条指令控制多组数据的运算。它被称为CPU指令级别的并行。

16. 注解处理器

用法:

  • 为 Java 编译器添加编译规则
  • 修改源代码
  • 生成新的源代码

Java 源代码的编译过程:


17.jmh

java性能测试的深坑

  • jvm优化
  • os:电源管理,CPU 缓存、分支预测器 ,以及超线程技术
  • 硬件HW(hardware)

jmh便是为了解决这些问题而成立的开源项目。

相关注解:

  • @Fork 启动虚拟机的数目,可以减少因虚拟机的优化会带来不确定性
  • @Warmup 和 @Measurement:预热迭代和测试迭代。建议:保持 5-10 个预热迭代的前提下(这样可以看出是否达到稳定状态)将总的预热时间优化至最少,以便节省性能测试的机器时间
  • @State:允许配置测试程序的状态

18.jvm工具

一些概念:

  • Java Flight Recorder 是 JMC 的其中一个组件,能够以极低的性能开销收集 Java 虚拟机的性能数据
  • mat支配树展示了快照中每个对象所直接支配的对象


    深入拆解java虚拟机-笔记_第1张图片

    深入拆解java虚拟机-笔记_第2张图片

19. java agent与字节码注入

可以通过java agent的类加载拦截功能,在类加载期,修改类对应的byte数组,并通过修改过的字节码完成类的加载

使用:-javaagent:xxx.jar 虚拟机参数

大名鼎鼎的AspectJ,便是通过java agent实现加载期的织入

字节码注入需要注意的问题:

  • 避免无限递归
  • 命名空间

20. Graal与Truffel

Graal是一个用java写就的、能够将java字节码转换成二进制码的即时编译器,通过JVMCI(jvm compile interface)与java虚拟机交互。

Truffel是GraalVM中的语言实现框架,用java写就。解决的问题:实现一门语言时,只需要实现解释执行器,而复用即时编译、垃圾回收等组件。
基于Truffel的语言实现仅需用java实现词法分析器、语法分析器以及针对语法分析所生成的抽象语法树(Abstract Syntax Tree, AST)的解释执行器即可。

实现一门语言需要先实现一个编译器,把该语言编写的程序转换成可以在硬件上直接运行的机器码。通常,编译器分成前端和后端:前端负责词法分析、语法分析、类型检查和中间代码生成。后端负责编译优化和目标代码生成。

21. 结语

jvm学习的一些博客和公号

你可能感兴趣的:(深入拆解java虚拟机-笔记)