首页 > 基础资料 博客日记
好多分钟了解下java虚拟机--03
2025-06-18 23:30:02基础资料围观8次
垃圾回收
引用计数法和可达性分析
- 引用计数法
即记录对象的 reference count
若≠0则保留
a, b对象相互引用, 不可回收, 造成内存泄露
- 可达性分析(JVM主流使用)
从GC Root出发的树状结构
若对象不可达则回收
- 存在的问题
在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)
Stop-the-world 以及安全点
当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作。(安全词🤣🤣🤣)
垃圾回收的三种方式
- 清除(sweep)
- 压缩(compact)
- 复制(copy)
Java虚拟机的堆划分
- new 指令执行时将对象存储在Eden区
- Eden区满进行一次Minor GC
Minor GC收集新生代的垃圾, 同时扫描老年代的对象以确保所有被引用的年轻代对象都被正确标记为存活对象。
- 仍存活于JVM eden区的对象转移到from区
- Survivors区通过
copy
进行垃圾回收 - 在Survivors区长期存活对象转移到老年代
TLAB(acquire lock())
每个线程可以向 Java 虚拟机申请一段连续的内存
线程维护内存头尾指针
卡表
维护每张卡的dirty位
卡中存在写入, 则dirty位置1
卡: 将堆划分为多个512字节的卡
if (CARD_TABLE [this address >> 9] != DIRTY) //减少重复写的不必要开销
CARD_TABLE [this address >> 9] = DIRTY;
为避免扫描所有的老年代对象 ,导致新生代的对象重复扫描,导致的重复扫描对象堆
GC通过寻找dirty卡扫描, 扫描完dirty清零
Java内存模型
在单线程中由于 as-if-serial 原则 不会改变程序重排序的运行结果
多线程就要涉及到 happen-before
happens-before (java 5)
描述两个操作的内存可见性
如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见
Java 内存模型的底层实现
对于即时编译器来说,它会针对前面提到的每一个 happens-before 关系,向正在编译的目标方法中插入相应的读读、读写、写读以及写写内存屏障。
内存屏障
即时编译器将根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令
如6.S081的__sync_synchronize()
以我们日常接触的 X86_64 架构来说,读读、读写以及写写内存屏障是空操作(no-op),只有写读内存屏障会被替换成具体指令
volatile字段与安全发布
Java虚拟机实现sync
当声明 synchronized 代码块时,编译而成的字节码将包含 monitorenter
和 monitorexit
指令
public void foo(Object lock) {
synchronized (lock) {
lock.hashCode();
}
}
// 上面的 Java 代码将编译为下面的字节码
public void foo(java.lang.Object);
Code:
0: aload_1
1: dup
2: astore_2 //复制lock对象
3: monitorenter
4: aload_1
5: invokevirtual java/lang/Object.hashCode:()I
8: pop
9: aload_2
10: monitorexit
11: goto 19
14: astore_3
15: aload_2
16: monitorexit
17: aload_3
18: athrow
19: return
Exception table:
from to target type
4 11 14 any
14 17 14 any
- ACC_SYNCHRONIZED
public synchronized void foo(Object lock) {
lock.hashCode();
}
// 上面的 Java 代码将编译为下面的字节码
public synchronized void foo(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: (0x0021) ACC_PUBLIC, **ACC_SYNCHRONIZED**
Code:
stack=1, locals=2, args_size=2
0: aload_1
1: invokevirtual java/lang/Object.hashCode:()I
4: pop
5: return
该标记表示在进入该方法时,Java 虚拟机需要进行 monitorenter
操作。而在退出该方法时,不管是正常返回,还是向调用者抛异常,Java 虚拟机均需要进行 monitorexit
操作
锁
|-----------------------------------------------------------|------------------|
| Thread ID (偏向锁) / HashCode / GC Age (其他状态) | 锁标志 | epoch |
|-----------------------------------------------------------|------------------|
| 54 bits | 3 bits | 2 bits |
- 重量级锁
Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程
在被阻塞前, 线程先进入自旋状态
自旋 在处理器上空跑并且轮询锁是否被释放
Java 虚拟机给出的方案是自适应自旋,根据以往自旋等待时是否能够获得锁,来动态调整自旋的时间(循环数目)
- 轻量级锁
通过CAS判断锁的标记字段是否为01
- 01 → 替换为刚分布过锁记录ID(持有线程) 并替换锁标志00
- else →
- 该线程重复获取同一把锁。此时,Java 虚拟机会将锁记录清零,以代表该锁被重复获取
- 其他线程持有该锁。此时,Java 虚拟机会将这把锁膨胀为重量级锁,并且阻塞当前线程
偏向锁
超级无敌乐观锁 偏好从始至终只有一个线程请求某一把锁
- 锁对象初始化
JVM通过 CAS 操作,将当前线程的地址记录在锁对象的标记字段之中,并且将锁标志设置为 101(偏向锁状态), 同时设置锁对象的epoch = 全局epoch
- 请求锁
- 判断锁指向的对应线程地址是否为该线程,若不是, 全局epoch++ 撤销偏向锁
- 判断 epoch 值是否和锁对象的类的 epoch 值相同 若不是 撤销偏向锁
- 若一切正常
- 撤销偏向锁
- 锁对象可以重新偏向其他线程(若无竞争)
- 悲观化 turn blue
- 撤销需在安全点执行,成本较高,因此JVM通过epoch优化管理
即时编译(JIT)
HotSpot 虚拟机包含多个即时编译器 C1、C2 和 Graal
分层编译
- 初始阶段:
- 代码首先由解释器执行,以快速启动应用程序。
- 热点检测:
- JVM通过热点检测(HotSpot)技术识别出频繁执行的代码块(热点代码)。
- C1编译:
- 一旦检测到热点代码,JVM会使用C1编译器将其编译为本地代码。
- 此时,代码的执行效率有所提高,但优化程度有限。
- C2编译:
- 如果热点代码继续被频繁执行,JVM会进一步使用C2编译器对其进行编译。
- C2编译器会进行更高级的优化,生成高效的本地代码。
- 动态优化:
- 在运行过程中,JVM可能会根据运行时信息动态调整编译策略,甚至重新编译某些代码。
热点代码: JVM通过识别代码块的循环回边数(循环体调用次数)和调用次数两者取和
OSR编译
(on- stack - replacement)
顾名思义就是在程序运行过程中(暂停线程)将通过JIT编译后的本地代码栈帧替换字节码栈帧
Profiling(性能分析)
收集能够反映程序执行状态的数据
- 分支profile
- 数据收集:
- JIT编译器会记录每次分支指令的执行情况,包括分支是否被取和分支的目标地址。
- 收集的数据包括分支的取向(taken或not taken)和分支的执行频率。
- 数据分析:
- 通过分析分支的执行历史,JIT编译器可以预测分支的未来行为。
- 如果某个分支总是被取或总是被忽略,JIT编译器可以调整分支预测逻辑,减少分支预测错误的开销。
- 数据收集:
- 类型profile
- 数据收集:
- JIT编译器会记录每次对象创建和方法调用时的类型信息。
- 收集的数据包括对象的实际类型、方法的接收者类型等。
- 数据分析:
- 通过分析类型使用历史,JIT编译器可以推断出对象类型的常见模式。
- 如果某个方法总是被特定类型的对象调用,JIT编译器可以进行类型专用化(type specialization),生成针对该类型的优化代码。
- 数据收集:
其中,最为基础的便是方法的调用次数以及循环回边的执行次数。它们被用于触发即时编译
去优化
当发现profile优化后的代码没有按预期执行, 线程进入trap
暂停线程通过OSR进行执行代码栈帧转换
当JVM调用去优化方法时,它会根据去优化的原因来决定对即时编译器生成的机器码采取什么行动。具体来说,有三种可能的行动:
- Action_None
示例:假设某个方法的去优化是因为出现了异常,但这个异常与优化无关,重新编译也不会改变生成的机器码。在这种情况下,JVM可以选择保留当前的机器码,下次调用该方法时直接使用。
- Action_Recompile
示例:假设某个方法的去优化是因为类层次分析的结果发生了变化,例如新加载了一个子类,导致之前的优化假设不再成立。在这种情况下,JVM可以选择不保留当前的机器码,但直接重新编译该方法。
- Action_Reinterpret
示例:假设某个方法的去优化是因为基于性能分析的激进优化失败了,例如某个假设的执行路径不再成立。在这种情况下,JVM需要重新收集性能数据,以更好地反映程序的新的执行状态。
public void method() {
try {
// 可能引发异常的代码
} catch (Exception e) {
// 处理异常
}
}
public void method() {
for (int i = 0; i < 1000000; i++){
// 热点代码
}
}
public class Parent {
public void method() {
// 方法体
}
}
public class Child extends Parent {
@Override
public void method() {
// 重写的方法体
}
}public class Main {
public void caller(Parent p) {
p.method(); // 假设这里被内联了
// Parent的method
}
}
即时编译器的中间表达形式
中间表达形式(Intermediate Representation)
如果不考虑解释执行的话,从 Java 源代码到最终的机器码实际上经过了两轮编译:Java 编译器将 Java 源代码编译成 Java 字节码,而即时编译器则将 Java 字节码编译成机器码。
Java 字节码本身并不适合直接作为可供优化的 IR
现代编译器一般采用静态单赋值(Static Single Assignment,SSA)IR
SSA
每个变量只能被赋值一次,而 且只有当变量被赋值之后才能使用
y = 1; SSA伪代码 y1 = 1;
y = 2; - - - - -> y2 = 2;
x = y; x1 = y2;
int x = 0;
if (condition) {
x = 1;
} else {
x = 2;
}
// 使用x
int x_1 = 0;
if (condition) {
x_1 = 1; // 重用x_1
} else {
x_1 = 2; // 重用x_1
}
// 使用x_1
int x_1 = 0;
if (condition){
x_2 = 1;
}else{
x_3 = 2;
}x_4 = φ(x_2, x_3);
// 使用x4
Sea-of-nodes
去除了变量的概念,直接采用变量所指向的值,来进行运算
节点调度需根据节点间的依赖关系进行
GVN优化
Global Value Numbering
发现并消除等价计算的优化技术
public static int foo(int count) {
int sum = 0;
for (int i = 0; i < count; i++) {
sum += i;
}
return sum;
}
public static int foo(int a, int b) {
int sum = a * b;
if (a > 0) {
sum += a * b;
}
if (b > 0) {
sum += a * b;
}
return sum;
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:
上一篇:SpringBoot读取Resources下的文件
下一篇:没有了