最详细的JVM(Hotspot)介绍
2021-11-16 16:52:47 7 举报
AI智能生成
史上最全JVM详解 超大哦
作者其他创作
大纲/内容
InvokeDynamic
概念
方法句柄:MethodHandler
背景
Dynamic type and static type programming languages. 我们常用的JavaScript, Python, Ruby都可以归为动态语言,而Java, Bytecode都可以认为是静态语言。这两种语言最大的差别是变量和函数的类型是不是在程序运行中确定的。
Compilation & Runtime Optimizations. 用户的程序在在编译 运行时往往伴随着大量的优化,Type 信息在编译器&解释器优化中起了重要的作用。一个比较常见的Runtime optimization是 Inlining Cache,大概的含义就是在函数call site记住函数之前解析出来的具体方法。目的是知道类型后,前后可以关联起来最内联优化。
静态语言中的type信息编译器能够保证,并且这些type信息能够一定程度上保证InliningCache的有效性。
为什么会有这个?
这个指令出发点是让动态语言能够在JVM上更好的运行。比如Ruby, JavaScript. 那问题是动态语言没有类型的信息,在程序运行众type可以随意变化的。
JSR292
与反射的区别
调用模式
1. When JVM sees an invokedynamic instruction, it locates the corresponding bootstrap method in the class, and executes the bootstrap method.
2. After executing the bootstrap method, a CallSite that is linked with a MethodHandle is returned;
3.The invocation on the CallSite later will be transferred to real methods via a number of MethodHandles.
备注: 这里的bootstrap和Methodhandle都是用户提供的,其中bootstrap方法就是用户创建一个CallSite,然后将这个Callsite链接到一个MethodHandle。MethodHandle所指向的方法可以再应用到其它的MethodHandle, 直至最终一个方法或者多个方法
图例
图例
Lambda
原理
Lambda 表达式到函数式接口的转换是通过 invokedynamic 指令来实现的。该 invokedynamic 指令对应的启动方法将通过 ASM 生成一个适配器类
捕获其他变量
对于没有捕获其他变量的 Lambda 表达式,该 invokedynamic 指令始终返回同一个适配器类的实例。对于捕获了其他变量的 Lambda 表达式,每次执行 invokedynamic 指令将新建一个适配器类实例。
性能
不管是捕获型的还是未捕获型的 Lambda 表达式,它们的性能上限皆可以达到直接调用的性能。其中,捕获型 Lambda 表达式借助了即时编译器中的逃逸分析,来避免实际的新建适配器类实例的操作。
代码样例
对象结构
对象初始化模式
构造器模式
new语句
反射机制
对象复制
对象clone
反序列化
内存复制
Unsafe.allocateInstance
对象的内存布局
对象头(Object Header)
MarkWord
Eden/Survivor到Old的升级次数为15, 因为位数是4位, 二进制存储最多支持15次
Klass
Array Length(仅仅数组)
参考:压缩模式
32位
Class pointer is 4 bytes, MarkWord is 4 bytes, and the object header is 8 bytes.
64位
Class pointer is 8 bytes, MarkWord is 8 bytes, and the object header is 16 bytes.
64位(压缩)
Class pointer is 4 bytes, MarkWord is 8 bytes, and the object header is 12 bytes
对象实例数据(Instance data)
对象填充(Align padding)
HotSpot对象模型
OOP-Klass 模型
为什么这么设计?
对象成员重排序
什么是重排序
如果一个字段占据 C 个字节,那么该字段的偏移量需要对齐至 NC。这里偏移量指的是字段地址与对象的起始地址差值。
子类所继承字段的偏移量,需要与父类对应字段的偏移量保持一致。
为啥要重排序
关于伪共享
如何避免伪共享
用字段填充
用Content标记
重排序的规则
重排序的例子
代码
结果
垃圾回收
算法
引用计数法
可达性分析
GC ROOT
STW
Safe Point
使用场景
HotSpot VM定义范围
HotSpot VM 定义原因
操作
Sweep
compact
copy
回收规则
Eden Space (heap)
Survivor Space (heap)
晋升规则
Tenured Generation (heap)
卡表(Card Table)
Permanent Generation (non-heap)
分类
标记 - 复制算法
Serial,Parallel Scavenge 和 Parallel New
标记 - 压缩算法
Serial Old 和 Parallel Old,以及 CMS。Serial Old 和 Parallel Old
标记 - 清除算法
CMS
基于Region
G1(链接的官方教程非常好)
TLAB
几种引用类型
强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
软引用(SoftReference)
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
虚引用(PhantomReference)
虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
内存模型
结构
规则
as-if-serial
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
JSR-133: happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这2个操作之间必须要存在happens-before关系。这里提到的2个操作既可以是一个线程之内,也可以是不同线程之间。
memory barrier
程序在运行时内存实际的访问顺序和程序代码编写的访问顺序不一定一致,这就是内存乱序访问。内存乱序访问行为出现的理由是为了提升程序运行时的性能。内存乱序访问主要发生在两个阶段:
编译时,编译器优化导致内存乱序访问(指令重排)
运行时,多 CPU 间交互引起内存乱序访问
编译时,编译器优化导致内存乱序访问(指令重排)
运行时,多 CPU 间交互引起内存乱序访问
锁
定义
markword
它的最后两位便被用来表示该对象的锁状态。其中,00 代表轻量级锁,01 代表无锁(或偏向锁),10 代表重量级锁,11 则跟垃圾回收算法的标记有关
分类
线程是否要锁住同步资源
锁住
悲观锁
不锁住
乐观锁
锁住同步资源失败,线程是否要阻塞
阻塞
不阻塞
自旋锁
适应性自旋锁
多个线程竞争临界资源的流程细节有没有区别
不锁住资源,多个线程中只有一个
能修改资源成功,其他线程需要重试
能修改资源成功,其他线程需要重试
无锁
同一个线程执行资源时,自动获取同步资源
偏向锁
多个线程获取同步资源时,没有获取到资源的线程自旋等待资源释放
轻量级锁
多个线程获取同步资源时,没有获取资源的线程阻塞等待唤醒
重量级锁
多个线程竞争锁时是否要排队
要排队
公平锁
Ticket Lock
CLH Lock
MCS Lock
先尝试插队,插队失败再排队
非公平锁
Spin Lock
一个线程的多个流程是否能多次获取同一把锁
能
可重入锁
不能
不可重入锁
多个线程能否共享同一把锁
能
共享锁
不能
互斥锁/排他锁
synchronized
锁指令生成
作用于代码块时,字节码会生成monitorenter 和monitorexit指令
作用于方法上时,字节码会生成 ACC_SYNCHRONIZED, 同时有上面的指令
锁对象定义
对于实例方法,锁本身为this
对于静态方法,锁本身为class
锁的用法
进入一次在锁对象上+1
退出一次在锁对象上-1
编译器特性
自动装箱与自动拆箱
Java 语言拥有 8 个基本类型,每个基本类型都有对应的包装(wrapper)类型
例如Integer类型, 调用 Integer.intValue 方法。这是一个实例方法,直接返回 Integer 对象所存储的 int 值。
泛型与类型擦除
原因:兼容 1.5(泛型是在 1.5 引入的) 之前的字节码文件
方法:往 ArrayList<Integer> 中添加元素的 add 方法,所接受的参数类型是 Object;而从 ArrayList 中获取元素的 get 方法,其返回类型同样也是 Object
细节:并不是每一个泛型参数被擦除类型后都会变成 Object 类。对于限定了继承类的泛型参数,经过类型擦除后,所有的泛型参数都将变成所限定的继承类。也就是说,Java 编译器将选取该泛型所能指代的所有类中层次最高的那个,作为替换泛型的类。
桥接方法
为什么要有?
因为泛型擦除导致的类型丢失, 那么只要有重写的方法(返回值或者参数类型发生了变化), 那么就需要桥接方法来链接到擦除后的原方法
如何实现?
字节码编译后会自动生成, flags上会标注为ACC_BRIDGE, ACC_SYNTHETIC模式,内部则进行了类型的转换操作
样例
代码
字节码
变长参数
try-with-resources
foreach 循环
for (Integer item : list) 这种模式等价于while (iterator.hasNext())
由于使用了迭代器的模式, 可能导致堆上内存分配很多, 所以即时编译器会优化掉这部分代码,具体参考内联&标量替换部分
字符串 switch
方法内联
是什么?
在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。方法内联不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化。因此,它可以算是编译优化里最为重要的一环。
优点
内联越多,生成代码的执行效率越高
缺点
编译时间越长,而程序达到峰值性能的时刻也将被推迟
生成的机器码越长,越容易填满 Code Cache
条件
进行内联
自动拆箱总会被内联
由 @ForceInline 注解的方法(仅限于 JDK 内部方法)
由 -XX:CompileCommand 中的 inline 指令指定的方法
更小的且独立的热点方法, 更容易被内联
不进行内联
Throwable 类的方法不能被其他类中的方法所内联
调用字节码对应的符号引用未被解析
目标方法所在的类未被初始化
目标方法是 native 方法
其他
C2 不支持内联超过 9 层的调用(可以通过虚拟机参数 -XX:MaxInlineLevel 调整)
以及 1 层的直接递归调用(可以通过虚拟机参数 -XX:MaxRecursiveInlineLevel 调整)
内联原理
C2 中,方法内联是在解析字节码的过程中完成的。每当碰到方法调用字节码时,C2 将决定是否需要内联该方法调用。 最终即时编译器首先解析字节码,并生成 IR 图,然后在该 IR 图上进行优化。优化是由一个个独立的优化阶段(optimization phase)串联起来的。每个优化阶段都会对 IR 图进行转换。最后即时编译器根据 IR 图的节点以及调度顺序生成机器码。
同 C2 一样,Graal 也会在解析字节码的过程中进行方法调用的内联。此外,Graal 还拥有一个独立的优化阶段,来寻找指代方法调用的 IR 节点,并将之替换为目标方法的 IR 图
最后,即时编译器将根据方法调用指令所在的程序路径的热度,目标方法的调用次数及大小,以及当前 IR 图的大小来决定方法调用能否被内联。
去虚化(devirtualize)
是什么?
动态绑定的虚方法调用来说,即时编译器则需要先对虚方法调用进行去虚化(devirtualize),即转换为一个或多个直接调用,然后才能进行方法内联
完全去虚化
基于类型推导的完全去虚化
基于类型推导的完全去虚化将通过数据流分析推导出调用者的动态类型,从而确定具体的目标方法。
基于类层次分析的完全去虚化
基于类层次分析的完全去虚化通过分析 Java 虚拟机中所有已被加载的类,判断某个抽象方法或者接口方法是否仅有一个实现。如果是,那么对这些方法的调用将只能调用至该具体实现中。
条件去虚化
匹配不到调用者的动态类型
1. 如果类型 Profile 是完整的,也就是说,所有出现过的动态类型都被记录至类型 Profile 之中,那么即时编译器可以让程序进行去优化,重新收集类型 Profile。
2. 如果类型 Profile 是不完整的,也就是说,某些出现过的动态类型并没有记录至类型 Profile 之中,那么重新收集并没有多大作用。此时,即时编译器可以让程序进行原本的虚调用,通过内联缓存进行调用,或者通过方法表进行动态绑定。(仅在 Graal 中使用)
例子
调试
利用虚拟机参数 -XX:+PrintInlining 来打印编译过程中的内联情况
Intrinsic特性
是什么?
在 HotSpot 虚拟机中,所有被该注解标注的方法都是 HotSpot intrinsic。对这些方法的调用,会被 HotSpot 虚拟机替换成高效的指令序列。而原本的方法实现则会被忽略掉。其他虚拟机未必维护了这些 intrinsic 的高效实现,它们可以直接使用原本的较为低效的 JDK 代码
使用方法
从JDK9开始, HotSpot 虚拟机将为标注了@HotSpotIntrinsicCandidate注解的方法额外维护一套高效实现。编译器底层会动态替换掉java的实现, 使用提高性能的本地实现。最新版本的 HotSpot 虚拟机定义了三百多个 intrinsic。
举例
Unsafe的compareAndSwap(compareAndSet)
在 X86_64 体系架构中,对这些方法的调用将被替换为lock cmpxchg指令,也就是原子性更新指令
StringBuilder和StringBuffer构造
HotSpot 虚拟机将优化利用这些方法构造字符串的方式,以尽量减少需要复制内存的情况。
String类、StringLatin1类、StringUTF16类和Arrays类的方法
HotSpot 虚拟机将使用 SIMD 指令(single instruction multiple data,即用一条指令处理多个数据)对这些方法进行优化。
System.arraycopy
通过底层c++的代码, 实现了类似深拷贝和浅拷贝的逻辑, 性能要比java的高。
其他
包装类、Object类、Math类、System类中各个功能性方法,反射 API、MethodHandle类中与调用机制相关的方法,压缩、加密相关方法。
native方法的影响
做法
这些 native 方法的调用需要经过 JNI(Java Native Interface),其性能开销十分巨大。但是,经过即时编译器的 intrinsic 优化之后,这部分 JNI 开销便直接消失不见。简单说,也会从底层进行优化掉, 而不支持的JDK版本, 还是会使用native进行处理;
举例
Thread.currentThread
这是一个 native 方法,同时也是一个 HotSpot intrinsic。在 X86_64 体系架构中,R13 寄存器存放着当前线程的指针。因此,对该方法的调用将被即时编译器替换为一个特殊 IR 节点,并最终生成读取 R13 寄存器指令。
Java 12 的 intrinsic 列表
逃逸分析
是什么?
在 Java 虚拟机的即时编译语境下,逃逸分析将判断新建的对象是否会逃逸。即时编译器判断对象逃逸的依据有两个:一是看对象是否被存入堆中,二是看对象是否作为方法调用的调用者或者参数。
用途
锁消除
synchronized (new Object()) {}会被完全优化掉。这正是因为基于逃逸分析的锁消除。由于其他线程不能获得该锁对象,因此也无法基于该锁对象构造两个线程之间的 happens-before 规则。
synchronized (escapedObject) {}则不然。由于其他线程可能会对逃逸了的对象escapedObject进行加锁操作,从而构造了两个线程之间的 happens-before 关系。因此即时编译器至少需要为这段代码生成一条刷新缓存的内存屏障指令。
例子
栈上分配
如果逃逸分析能够证明某些新建的对象不逃逸,那么 Java 虚拟机完全可以将其分配至栈上,并且在 new 语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象。
由于实现起来需要更改大量假设了“对象只能堆分配”的代码,因此 HotSpot 虚拟机并没有采用栈上分配,而是使用了标量替换这么一项技术。
标量替换
标量替换这项优化技术,可以看成将原本对对象的字段的访问,替换为一个个局部变量的访问。
例子
部分逃逸分析(partial escape analysis)
Graal
支持部分逃逸分析
是什么?
部分逃逸分析将根据控制流信息,判断出新建对象仅在部分分支中逃逸,并且将对象的新建操作推延至对象逃逸的分支中。这将使得原本因对象逃逸而无法避免的新建对象操作,不再出现在只执行 if-else 分支的程序路径之中。
C2
C2 的逃逸分析与控制流无关,相对来说比较简单。
例子
举例子:如果概率统计基本99%不成立,那么Graal将执行类似的优化逻辑
调试
-XX:+DoEscapeAnalysis : 表示开启逃逸分析
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析
从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析
从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis
代码中的循环优化
循环无关代码外提
循环无关代码外提将循环中值不变的表达式,或者循环无关检测外提至循环之前,以避免在循环中重复进行冗余计算。
循环展开
一种在循环中重复多次迭代,并且相应地减少循环次数的优化方式。它是一种以空间换时间的优化方式,通过增大循环体来获取更多的优化机会。循环展开的特殊形式是完全展开,将原本的循环转换成若干个循环体的顺序执行。
在默认情况下,即时编译器仅能将循环展开 60 次(对应虚拟机参数-XX:LoopUnrollLimit)
注解处理器
原理
Java 的注解机制允许开发人员自定义注解。这些自定义注解同样可以为 Java 编译器添加编译规则。不过,这种功能需要由开发人员提供,并且以插件的形式接入 Java 编译器中,这些插件我们称之为注解处理器(annotation processor)
步骤
将源文件解析为抽象语法树;
调用已注册的注解处理器;
生成字节码。
用途
一是定义编译规则,并检查被编译的源文件
二是修改已有源代码
三是生成新的源代码
运行原理
字节码
字节码例子
字节码操作码:固定一个字节
JVM的结构
方法区
堆
PC寄存器
Java方法栈
本地方法栈
编译器
解释执行
即时编译器
C1
C2
Graal
可以通过参数打开, 替换C2
混合编译模式
如何检测热点代码
基于采样
基于计数器
触发的两种规则
方法调用
回边计数器
On-Stack-Replacement(OSR)编译
编译线程数
触发条件
系数
代码编译层次
解释执行;
执行不带 profiling 的 C1 代码;
执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
执行带所有 profiling 的 C1 代码;
执行 C2 代码。
混合编译规则(举例子四种)
事实上更多
事实上更多
编译器优化 - profiling
优化
大量的profiling的代码可以为后续的C2/Graal编译器提供优化依据, 根据计算后的结果可以进行代码的剪枝。类似方法的内联,直接去掉中间层的判断逻辑,导向结果。
去优化(失败)
剪枝后会在不会发发生的路径上加上断言, 如果出现问题则跳转到原来解析执行的代码块上,使用之前的代码执行。
注意点:经过逃逸分析之后,机器码可能并没有实际分配对象,而是在各个寄存器中存储该对象的各个字段(标量替换)。在去优化过程中,Java 虚拟机需要还原出这个对象,以便解释执行时能够使用该对象。另外, 如果代码缓存区达到上限, 编译器也会进行去优化处理.
编译器中间层表达式IR
(Intermediate Representation )
(Intermediate Representation )
概念
我们通常将编译器分为前端和后端。其中,前端会对所输入的程序进行词法分析、语法分析、语义分析,然后生成中间表达形式,也就是 IR(Intermediate Representation )
用途
即时编译器并不需要重新进行词法分析、语法分析以及语义分析,而是直接将 Java 字节码作为一种 IR, 便于将字节码生成机器码
SSA
静态单赋值(Static Single Assignment,SSA)IR。这种 IR 的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用。对于重复的冗余变量, 借助了 SSA IR,编译器则可以通过查找赋值了但是没有使用的变量,来识别冗余赋值
其他: 常量折叠(constant folding)、常量传播(constant propagation)、强度削减(strength reduction)以及死代码删除(dead code elimination)等
Sea-of-nodes
HotSpot 里的 C2 采用的是一种名为 Sea-of-Nodes 的 SSA IR。它的最大特点,便是去除了变量的概念,直接采用变量所指向的值,来进行运算。例如:x =1, x2=x, x3 =x2, 优化后那么x3 = 1; Graal 的 IR 同样也是 Sea-of-Nodes 类型的,并且可以认为是 C2 IR 的精简版本。
Gloval Value Numbering(GVN)
GVN 是一种发现并消除等价计算的优化技术。举例来说,如果一段程序中出现了多次操作数相同的乘法,那么即时编译器可以将这些乘法并为一个,从而降低输出机器码的大小。
可视化工具 IGV
利用 IR 可视化工具Ideal Graph Visualizer(IGV),来展示具体的 IR 图
加载机制
加载器
启动类加载器(bootstrap class loader)
扩展类加载器(extension class loader)
应用类加载器(application class loader)
Java9之后: 平台类加载器(platform class loader)
规则
类的唯一性
双亲委派模型
链接
验证
准备
解析
初始化
静态常量字段
非静态常量部分
初始化规则
例子
异常机制
定义
机制
代码样例
Java 7 的 Supressed 异常
语法糖: try-with-resources,以及多异常捕获
其他
fast fail
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception
场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
fast safe
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
OOM介绍及相关
方法区
运行时常量池溢出
方法区一部分。存放编译期生成的各种字面量和符号引用。 一般异常信息为:java.lang.OutOfMemoryError:PermGen space。可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小
方法区溢出
存储已被JVM加载的类信息,类名、访问修饰符、常量池、字段描述、方法描述等。
一般异常信息为:java.lang.OutOfMemoryError:PermGen space
一般异常信息为:java.lang.OutOfMemoryError:PermGen space
Heap区
分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow), 一般异常为:java.lang.OutOfMemoryError:Java heap spacess
另外当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次也会触发GC overhead limit exceeded异常. 一般异常为: java.lang.OutOfMemoryError:GC overhead limit exceeded
线程创建失败, 线程数超过操作系统最大线程数 ulimit 限制, 或者没有空间的时候, 也会触发该问题, 一般异常为: java.lang.OutOfMemoryError:Unableto createnewnativethread
Java栈(虚拟机栈)
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间(不断创建线程),则抛出OutOfMemoryError异常
如果虚拟机在扩展栈时无法申请到足够的内存空间(不断创建线程),则抛出OutOfMemoryError异常
本地方法栈
为虚拟机使用到的native方法服务,可能底层调用的c或者c++, 类似Java栈的异常
其他情况
Out of swap space
1、地址空间不足;
2、物理内存已耗光;
3、应用程序的本地内存泄漏(native leak),例如不断申请本地内存,却不释放。
2、物理内存已耗光;
3、应用程序的本地内存泄漏(native leak),例如不断申请本地内存,却不释放。
Kill process or sacrifice child
有一种内核作业(Kernel Job)名为 Out of Memory Killer,它会在可用内存极低的情况下“杀死”(kill)某些进程。OOM Killer 会对所有进程进行打分,然后将评分较低的进程“杀死”,具体的评分规则可以参考 Surviving the Linux OOM Killer。
不同于其他的 OOM 错误, Killprocessorsacrifice child 错误不是由 JVM 层面触发的,而是由操作系统层面触发的。
不同于其他的 OOM 错误, Killprocessorsacrifice child 错误不是由 JVM 层面触发的,而是由操作系统层面触发的。
Requested array size exceeds VM limit
JVM 限制了数组的最大长度,该错误表示程序请求创建的数组超过最大长度限制。
JVM 在为数组分配内存前,会检查要分配的数据结构在系统中是否可寻址,通常为 Integer.MAX_VALUE-2
JVM 在为数组分配内存前,会检查要分配的数据结构在系统中是否可寻址,通常为 Integer.MAX_VALUE-2
Direct buffer memory
Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer, 可能超过了最大限额导致错误. 可以通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值, 默认应该是64mb
数据类型
基本类型(primitive types)
Java的八种基本类型
boolean掩码取值
char、byte、short、int、long、float 以及 double内存中默认值都为0
基本类型的大小
栈上内存大小问题
操作数栈加载堆数据问题
引用类型(reference types)
类
JVM通过字节流
方法
JVM通过字节流
数组
虚拟机直接生成
泛型
虚拟机抹除
方法调用
如何定义方法
类名
方法名
方法描述符(method descriptor)
方法重载与重写
重载
规则
在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;
如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。
示例
重写
如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法
如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法。
Java语言重写与JVM重写区别
Java语言重写
JVM重写
方法绑定
静态绑定
invokestatic:用于调用静态方法。
invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
动态绑定(虚方法调用)
invokevirtual:用于调用非私有实例方法。
invokeinterface:用于调用接口方法。
invokedynamic:用于调用动态方法。
区别
方法表(虚方法绑定用)
概念
分类
invokevirtual 所使用的虚方法表(virtual method table,vtable)
invokeinterface 所使用的接口方法表(interface method table,itable)
优化方案
内联缓存(inlining cache)
单态(monomorphic)指的是仅有一种状态的情况。
多态(polymorphic)指的是有限数量种状态的情况。二态(bimorphic)是多态的其中一种。
超多态(megamorphic)指的是更多种状态的情况。通常我们用一个具体数值来区分多态和超多态。在这个数值之下,我们称之为多态。否则,我们称之为超多态。
方法内联(method inlining)
请参考下面的方法内联章节
JVM做法
劣化为超多态状态
JNI方法调用
方法1
通过命名规范自动寻找, native 方法对应的 C 函数都需要以Java_为前缀,之后跟着完整的包名和方法名。由于 C 函数名不支持/字符,因此我们需要将/转换为_,而原本方法名中的_符号,则需要转换为_1
例子: org.example包下Foo类的foo方法,Java 虚拟机会将其自动链接至名为Java_org_example_Foo_foo的 C 函数中。
方法2
由C 代码中主动链接, 通常我们会使用一个名为registerNatives的 native 方法,并按照第一种链接方式定义所能自动链接的 C 函数。在该 C 函数中,我们将手动链接该类的其他 native 方法。
例子:
常用工具
ASMTools
JOL
ASM
Eclipse Memory Analyzer
JMH
Java Microbenchmark Harness
JMH 是一个面向 Java 语言或者其他 Java 虚拟机语言的性能基准测试框架。它针对的是纳秒级别、微秒级别、毫秒级别,以及秒级别的性能测试。
jmc
Java Mission Control使您能够监视和管理Java应用程序,而不会引入通常与这些类型的工具相关联的性能开销。它使用为Java虚拟机(JVM)的常规自适应动态优化收集的数据。
Java内部
javap
javap -p -v Foo
基本信息
常量池
字段区域
方法区域
jps
它将打印所有正在运行的 Java 进程的相关信息
jps -mlv
18331 org.example.Foo Hello World
18332 jdk.jcmd/sun.tools.jps.Jps -mlv -Dapplication.home=/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home -Xms8m -Djdk.module.main=jdk.jcmd
18332 jdk.jcmd/sun.tools.jps.Jps -mlv -Dapplication.home=/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home -Xms8m -Djdk.module.main=jdk.jcmd
jstat
可用来打印目标 Java 进程的性能数据
详细例子
jmap
分析 Java 虚拟机堆中的对象
-clstats,该子命令将打印被加载类的信息。
-finalizerinfo,该子命令将打印所有待 finalize 的对象。
-histo,该子命令将统计各个类的实例数目以及占用内存,并按照内存使用量从多至少的顺序排列。此外,-histo:live只统计堆中的存活对象。
-dump,该子命令将导出 Java 虚拟机堆的快照。同样,-dump:live只保存堆中的存活对象。
-finalizerinfo,该子命令将打印所有待 finalize 的对象。
-histo,该子命令将统计各个类的实例数目以及占用内存,并按照内存使用量从多至少的顺序排列。此外,-histo:live只统计堆中的存活对象。
-dump,该子命令将导出 Java 虚拟机堆的快照。同样,-dump:live只保存堆中的存活对象。
jinfo
可用来查看目标 Java 进程的参数, 并能够改动其中 manageabe 的参数
jstack
可以用来打印目标 Java 进程中各个线程的栈轨迹,以及这些线程所持有的锁
jcmd
可以用来实现前面除了jstat之外所有命令的功能。 在JDK1.7以后,新增一个多功能的工具,可以用它来导出堆、查看Java进程、导出线程信息、执行GC、还可以进行采样分析(jmc 工具的飞行记录器)
VM Flags
java -XX:+PrintFlagsFinal -version | grep ****
反射调用
样例
实现规则
本地实现
动态实现
性能开销
Class.forName
Class.forName 会调用本地方法
Class.getMethod
Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。可想而知,这两个操作都非常费时
Method.invoke
关闭反射调用的 Inflation 机制,从而取消委派实现,并且直接使用动态实现
关闭每次反射调用都会检查目标方法的权限
可以提高 Java 虚拟机关于每个调用能够记录的类型数目(对应虚拟机参数 -XX:TypeProfileWidth,默认值为 2,这里设置为 3)
默认线程
JDK6,7,8
main
这个没什么说的,是你的程序主线程
Reference Handler
清除reference的垃圾回收线程
Finalizer
处理用户的Finalizer方法
Signal Dispatcher
分发处理发送给jvm信号的线程
Attach Listener
由上面这个线程创建的,可能一开始的时候没有。这个线程的作用是为jvm提供一种jvm进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作,比如说我们为了让另外一个jvm进程把线程dump出来,那么我们跑了一个jstack的进程,然后传了个pid的参数,告诉它要哪个进程进行线程dump
代码
public class TestOne {
public static void main(String[] args) {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] allThreads = mxBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : allThreads) {
System.out.println(threadInfo.getThreadId()+"==="+threadInfo.getThreadName());
}
}
public static void main(String[] args) {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] allThreads = mxBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : allThreads) {
System.out.println(threadInfo.getThreadId()+"==="+threadInfo.getThreadName());
}
}
0 条评论
下一页