SafePoint

今天偶然看到的一个 Java 的奇技淫巧

在掘金首页上刷到一个关于 RocketMQ 的小改进,代码在DefaultMappedFile的 591 行, GitHub 提的issue4902。将 for 循环 i 的类型从 int 改为 long,就可以将下面 prevent gc 代码注释掉。thread.sleep(0)主要是让 GC thread 有更多机会被 OS 选中,进行垃圾回收。这样避免超大循环的时候触发很大型的 STW 事件,相当于流量削峰。
为什么 thread.sleep(0)可以进入 safepoint 呢?[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void warmMappedFile(FlushDiskType type, int pages) {
this.mappedByteBufferAccessCountSinceLastSwap++;

long beginTime = System.currentTimeMillis();
ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
long flush = 0;
// long time = System.currentTimeMillis();
for (long i = 0, j = 0; i < this.fileSize; i += DefaultMappedFile.OS_PAGE_SIZE, j++) {
byteBuffer.put((int) i, (byte) 0);
// force flush when flush disk type is sync
if (type == FlushDiskType.SYNC_FLUSH) {
if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
flush = i;
mappedByteBuffer.force();
}
}
// prevent gc
// if (j % 1000 == 0) {
// log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);
// time = System.currentTimeMillis();
// try {
// Thread.sleep(0);
// } catch (InterruptedException e) {
// log.error("Interrupted", e);
// }
// }
}
// force flush when prepare load finished
if (type == FlushDiskType.SYNC_FLUSH) {
log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}",
this.getFileName(), System.currentTimeMillis() - beginTime);
mappedByteBuffer.force();
}
log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(),
System.currentTimeMillis() - beginTime);
this.mlock();
}
  • 提到了一个没听说过的 SafePoint , 于是就去搜了一下

    在 OopMap 的协助下,HotSpot 可以快速准确地完成 GC Roots 枚举,但一个很现实的问题随之而 来:可能导致引用关系变化,或者说导致 OopMap 内容变化的指令非常多,如果为每一条指令都生成 对应的 OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法 忍受的高昂。
    实际上 HotSpot 也的确没有为每条指令都生成 OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint)。有了安全点的设定,也就决定了用户程序执行时 并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。因此,安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过 分增大运行时的内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准 进行选定的,因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而 长时间执行,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转 等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。
    对于安全点,另外一个需要考虑的问题是,如何在垃圾收集发生时让所有线程(这里其实不包括执行 JNI 调用的线程)都跑到最近的安全点,然后停顿下来。这里有两种方案可供选择:抢先式中断 (Preemptive Suspension)和主动式中断(Voluntary Suspension),抢先式中断不需要线程的执行代码 主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地 方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚 拟机实现采用抢先式中断来暂停线程响应 GC 事件。
    ——节选自《深入理解 Java 虚拟机》-3.4.2
    Alexey Ragozin的博客中也有讲解

  • 简而言之, 安全点可以简单理解为它记录了 GC 的引用。所以在 GC 的时候需要从 SafePoint 获取可以回收的对象地址。没有到达 safepoint 的时候是不能进行 gc 的。但是为什么吧 int 改为 long 就可以呢?

    是 HotSpot 虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,所以使用 int 类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环(Counted Loop),相对应地,使用 long 或者范围更大的数据类型作为索引值的循环就被称为不可数循环(Uncounted Loop),将会被放置安全点。
    —–节选自《深入理解 Java 虚拟机》-5.2.8

  • 也就是说,在 int 可数循环的情况下,虚拟机会在循环结束的时候才进行 gc。当改为 long 之后,变为不可数循环(但是这部分没有太懂,还得继续学)

[1] 在safepoint 源码中就有解释,Thread::sleep 就是一个 native 方法


SafePoint
https://polarisink.github.io/20221103/yuque/SafePoint/
作者
Areis
发布于
2022年11月3日
许可协议