JVM 思维导图
2020-07-20 10:54:57 1 举报
AI智能生成
jvm 循序渐进 思维导图
作者其他创作
大纲/内容
概述
基础
基础
讲讲 hello world 是怎么运行的?
这个问题有点大 哈哈
从几个角度来看
java
jvm
cpu
jvm的运行时数据区有哪些?
Run-Time Data Areas
Run-Time Data Areas
线程私有
PC
本地方法栈
虚拟机栈
共享
堆
方法区
参考
解释器栈中的栈帧的组成部分有哪些?
局部变量区
操作数栈
动态连接?
方法返回地址
java reference 的类型有哪些?
参考
class types, array types, and interface types
类加载
类加载的过程?
加载是指什么?
根据字节流创建类的过程
链接的步骤?
验证/准备/解析
验证
准备
为被加载类的静态字段分配内存
部分 Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,
比如说用来实现虚方法的动态绑定的方法表。
比如说用来实现虚方法的动态绑定的方法表。
解析
符号引用解析成为实际引用
如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,
那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
初始化的操作?
final
clinit
class init
标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。
Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。
Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。
类加载器的作用?
加载类
命名空间
什么时候会触发类初始化?
简述
1. jvm 启动时的初始化 main 所在的类
2. new
3. 调用静态方法时/调用静态字段时
4. 子类初始化会导致父类初始化
5. 接口类的初始化
6. 反射/methodHandle
java 中的方法调用
字节码中关于方法调用的5个指令?
参考
方法的静态绑定与动态绑定的关系?
参考
字节码指令与静态/动态绑定
如何解析 符号引用 转为 实际引用?
参考
非接口符号引用
接口符号引用
在执行调用指令前,它所附带的符号引用需要被解析成实际引用。
对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。
对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息。
对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。
对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息。
为什么有桥接方法?
对于 Java 语言中重写而 Java 虚拟机中非重写的情况,
编译器会通过生成桥接方法来实现 Java 中的重写语义。
编译器会通过生成桥接方法来实现 Java 中的重写语义。
举出两种桥接方法的例子?
返回值不同
参数不同
虚方法如何调用?
解释执行时使用方法表
方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。
即时编译中的内联缓存是什么?
内联缓存是一种加快动态绑定的优化技术。
它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法
在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。
在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。
虽然内联缓存附带内联二字,但是它并没有内联目标方法。这里需要明确的是,任何方法调用除非被内联,
否则都会有固定开销。这些开销来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。
否则都会有固定开销。这些开销来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。
异常
非检查异常 与 检查异常的分类 ?
RuntimeException 和 Error 属于 Java 里的非检查异常(unchecked exception)。其他异常则属于检查异常(checked exception)
异常实例 如何构造?
这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。当然,在生成栈轨迹时,Java 虚拟机会忽略掉异常构造器以及填充栈帧的 Java 方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起。此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧。我们在介绍 Lambda 的时候会看到具体的例子。
JVM 如何捕获异常?
使用异常表
异常表的构成?
在编译生成的字节码中,每个方法都附带一个异常表。
异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。
这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。
异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。
这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。
反射
1. 反射的开销来自哪里
4种开销举例
object 数组
拆装箱
生成字节码
权限检查
2. 委派模式的作用
3. 动态实现与本地实现的区别?
4. 为什么 native 实现(本地实现) 反而慢?
跨越native边界对优化有阻碍作用
native 就像黑箱子一样,让虚拟机难以优化分析
invokedynamic
todo
思路
java 的内存布局
垃圾回收
如何判断对象是否存活?
两种方法
引用计数法
可达性分析
简述可达性分析算法?
简述
这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,
并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
GC ROOTS 是什么? 包含哪些?
简单理解, 由堆外指向堆内的引用
包含但不限于
Java 方法栈桢中的局部变量;
已加载类的静态变量;
JNI handles;
已启动且未停止的 Java 线程;
常量池的引用;
已加载类的静态变量;
JNI handles;
已启动且未停止的 Java 线程;
常量池的引用;
什么是 stop the world (stw) ?
为什么 GC 需要 stw ?
安全的GC
为了防止在标记过程中堆栈的状态发生改变,Java 虚拟机采取安全点机制来实现 Stop-the-world 操作,
暂停其他非垃圾回收线程。
暂停其他非垃圾回收线程。
stw 如何实现?
stw 的4个阶段
spin/block/cleanup/vmop
safepoint 是什么?
是什么?
安全点, 线程不会更新引用, java 堆栈不会发生变化
为了java能够安全可靠地完成可达性分析
有哪些地方可以设置安全点检测?
解释器
字节码之间
JNI
调用java 方法时
进入 jni / jni 返回
JIT 编译器
什么时候插入安全点检测?
在生成机器码时,即时编译器需要插入安全点检测,以避免机器码长时间没有安全点检测的情况。
HotSpot 虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测。
HotSpot 虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测。
什么是计数循环? todo test
对于int类型的循环变量i,
如果满足
1) 基于该循环变量的循环出口只有一个,即i < limit,
2) 循环变量随着迭代的增量为常数,例子中i++即增量为1,以及循环变量的上限(当增量为负数时则是下限)为循环无关的,即limit应是循环无关,那么C2会将其判断成计数循环(counted loop),然后默认不插入safepoint。
如果满足
1) 基于该循环变量的循环出口只有一个,即i < limit,
2) 循环变量随着迭代的增量为常数,例子中i++即增量为1,以及循环变量的上限(当增量为负数时则是下限)为循环无关的,即limit应是循环无关,那么C2会将其判断成计数循环(counted loop),然后默认不插入safepoint。
三种基础的垃圾回收方式 ?
三种方式
sweep/compact/copy
讲讲这三种回收方式是怎么做的? 有什么优劣?
jvm 为什么要进行分代回收?
因为大部分对象的存活时间只存活一小段时间
Java 虚拟机可以给不同代使用不同的回收算法。
对于新生代,我们猜测大部分的 Java 对象只存活一小段时间,
那么便可以频繁地采用耗时较短的垃圾回收算法,让大部分的垃圾都能够在新生代被回收掉。
对于新生代,我们猜测大部分的 Java 对象只存活一小段时间,
那么便可以频繁地采用耗时较短的垃圾回收算法,让大部分的垃圾都能够在新生代被回收掉。
怎么分代?
老年代
新生代
eden + survior *2
new Object() 如何向堆申请内存?
TLAB
具体来说,每个线程可以向 Java 虚拟机申请一段连续的内存,比如 2048 字节,作为线程私有的 TLAB。这个操作需要加锁,线程需要维护两个指针(实际上可能更多,但重要也就两个),一个指向 TLAB 中空余内存的起始位置,一个则指向 TLAB 末尾。接下来的 new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。
Minor GC 为什么使用 标记复制算法?
Minor GC 怎么解决老年代对象包含指向新生代对象的引用问题?
卡表
HotSpot 给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。
在进行 Minor GC 的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到 Minor GC 的 GC Roots 里。当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏卡的标识位清零。
高效编译
java内存模型
思维导图
synchronized
思维导图
java 语法糖与 java 编译器
举例
自动拆装箱
泛型
桥接方法
变长参数
try...resources
foreach
switch string
var
即时编译
分层编译的五个层次
解释执行;
执行不带 profiling 的 C1 代码;
执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
执行带所有 profiling 的 C1 代码;
执行 C2 代码。
执行不带 profiling 的 C1 代码;
执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
执行带所有 profiling 的 C1 代码;
执行 C2 代码。
即使编译如何触发?
简述
Java 虚拟机是根据方法的调用次数以及循环回边的执行次数来触发即时编译的
Java 虚拟机在 0 层、2 层和 3 层执行状态时进行 profiling,其中就包含方法的调用次数和循环回边的执行次数。
以方法为单位的即时编译
阈值
OSR 编译是什么?
简述
以循环为单位的即时编译
动态替换java方法栈帧
去优化
详细
Java 虚拟机还存在着另一种以循环为单位的即时编译,叫做 On-Stack-Replacement(OSR)编译。
循环回边计数器便是用来触发这种类型的编译的。
循环回边计数器便是用来触发这种类型的编译的。
OSR 实际上是一种技术,它指的是在程序执行过程中,动态地替换掉 Java 方法栈桢,从而使得程序能够在非方法入口处进行解释执行和编译后的代码之间的切换。事实上,去优化(deoptimization)采用的技术也可以称之为 OSR。
触发 OSR 编译的阈值是由参数 -XX:CompileThreshold 指定的阈值的倍数。
(OnStackReplacePercentage - InterpreterProfilePercentage)/100
其中-XX:InterpreterProfilePercentage的默认值为33,当使用C1时-XX:OnStackReplacePercentage为933,当使用C2时为140。
(OnStackReplacePercentage - InterpreterProfilePercentage)/100
其中-XX:InterpreterProfilePercentage的默认值为33,当使用C1时-XX:OnStackReplacePercentage为933,当使用C2时为140。
工具
如何打印即使编译的信息? 怎么看?
-XX:+PrintCompilation
详细
第一列是时间,第二列是 Java 虚拟机维护的编译 ID。接下来是一系列标识,包括 %(是否 OSR 编译),s(是否 synchronized 方法),!(是否包含异常处理器),b(是否阻塞了应用线程,可了解一下参数 -Xbatch),n(是否为 native 方法)。再接下来则是编译层次,以及方法名。如果是 OSR 编译,那么方法名后面还会跟着 @以及循环所在的字节码。当发生去优化时,你将看到之前出现过的编译,不过被标记了“made not entrant"。它表示该方法不能再被进入。当 Java 虚拟机检测到所有的线程都退出该编译后的“made not entrant”时,会将该方法标记为“made zombie”,此时可以回收这块代码所占据的空间了
profling 的类型有哪两种?
简述
branch
跳转次数/不跳转次数
receiver type
非私有实例方法调用
checkcast
instanceof
aastore
基于分支 profile 的优化
1. 避免编译很有可能用不到的分支
2. "剪枝" 会精简程序的数据流, 从而触发更多的优化
3, 根据概率, 优先处理概率较高的编译请求
基于类型 profile 的优化
即时编译器假设的是对象的动态类型仅为类型 profile 中的那几个。
为了精简控制流和数据流
去优化
详细
在生成的机器码中,即时编译器将在假设失败的位置上插入一个陷阱(trap)。该陷阱实际上是一条 call 指令,调用至 Java 虚拟机里专门负责去优化的方法。
当根据映射关系创建好对应的解释执行栈桢后,Java 虚拟机便会采用 OSR 技术,动态替换栈上的内容,并在目标字节码处开始解释执行。
IR
中间表达形式
中间表达形式
什么是 IR ?
详细
在编译原理课程中,我们通常将编译器分为前端和后端。其中,前端会对所输入的程序进行词法分析、语法分析、语义分析,然后生成中间表达形式,也就是 IR(Intermediate Representation )。后端会对 IR 进行优化,然后生成目标代码。
即时编译器会将 Java 字节码转换成 SSA IR。更确切的说,是一张包含控制流和数据流的 IR 图,每个字节码对应其中的若干个节点(注意,有些字节码并没有对应的 IR 节点)。然后,即时编译器在 IR 图上面进行优化。
静态单赋值 IR
SSA IR
SSA IR
特点?
每个变量只能被赋值一次, 当变量被赋值后才能使用
应用
可以用来识别冗余赋值
C2 中的
sea-of-nodes IR
sea-of-nodes IR
它是一种 SSA IR
特点
去除了变量的概念,直接采用变量所指向的值,来进行运算。
工具
ideal graph visualizer
Global Value Numbering
一种发现并消除等价的优化技术
IR 图上的公共子表达式消除(Common Subexpression Elimination,CSE)。
方法内联
定义
在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。
作用
方法内联不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化.
因此,它可以算是编译优化里最为重要的一环。
因此,它可以算是编译优化里最为重要的一环。
静态方法调用
方法内联的条件
虚方法调用
去虚化两种
完全去虚化
条件去虚化
guarded devirtualization
guarded devirtualization
问题
类加载导致的去优化
intrinsic
简述
逃逸分析
定义
一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针
可通过逃逸分析的结果
进行的优化 3种
进行的优化 3种
锁消除
栈上分配
hotspot 并没有实现
标量替换
将原本对对象的字段的访问,替换为一个个局部变量的访问。
不需要对象头
部分逃逸分析
partial escape analysis
partial escape analysis
作用
解决了所新建的实例仅在部分程序路径中逃逸的情况。
例子
foreach 循环中的 iterator 对象的创建
代码优化
字段访问优化
循环优化
1:循环无关码外提——将循环内的某些无关代码外移,减少某些程序的反复执行
2:循环展开——减少循环条件的判断,针对循环次数少的循环
3:循环判断外提——减少每次循环的都进行判断次数
4:循环剥离——将不通用的处理起来稍微费劲一些的动作,放在循环外处理
2:循环展开——减少循环条件的判断,针对循环次数少的循环
3:循环判断外提——减少每次循环的都进行判断次数
4:循环剥离——将不通用的处理起来稍微费劲一些的动作,放在循环外处理
防止打断CPU的指令流水,提高指令处理的并行度
向量化
自动向量化
向量化优化借助的是 CPU 的 SIMD 指令,即通过单条指令控制多组数据的运算。它被称为 CPU 指令级别的并行。
注解
作用
用来为 类/方法/字段/参数等 java 结构提供额外信息的机制
java 编译的过程
1. 源文件解析为 AST
2. 调用已经注册的注解处理器
3. 生成字节码
应用
jcstress
jmh
lombok
收藏
0 条评论
下一页