前言
来自极客时间上的同名课程
要点摘录
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块
- sweep清除
- 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支配树展示了快照中每个对象所直接支配的对象
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学习的一些博客和公号