Thread.sleep(0)深入分析
因为我们知道,Java 程序员来说,虚拟机有自己的 GC 机制,我们不需要像写 C 或者 C++ 那样得自己管理内存,只要关注于业务代码即可,并没有特别注意 GC 机制。有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行
1 Thread.sleep(0)
1.1 问题引入
先看看一个来自RocketMQ
(org.apache.rocketmq.store.logfile.DefaultMappedFile#warmMappedFile)代码里面的for
循环,在循环里面,专门有个变量 j,来记录当前循环次数。
第一次循环以及往后每 1000 次循环之后,进入一个 if 逻辑。
在这个 if 逻辑之上,标注了一个注释:prevent gc
:防止GC线程进行垃圾回收。
虽然这是 RocketMQ 的源码,但是这个小技巧和 RocketMQ 框架没有任何关系,完全可以脱离于框架存在。
给出的修改意见是这样的:
把 int
修改为 long
,然后就可以直接把 for
循环里面的 if
逻辑删除掉了。
那么为什么这么做呢,提出这个修改方案的理论立足点是 Java
的安全点相关的知识,也就是 safepoint
1.2 探索
通过调用 Thread.sleep(0)
的目的是为了让 GC
线程有机会被操作系统选中,从而进行垃圾清理的工作。它的副作用是,可能会更频繁地运行 GC
,毕竟每 1000
次迭代就有一次运行 GC
的机会,但是好处是可以防止长时间的垃圾收集。
换句话说,这个代码是想要触发GC
,而不是避免GC
,或者说是“避免”时间很长的 GC。从这个角度来说,程序里面的注释其实是在撒谎或者没写完整。
不是 prevent gc
,而是对 gc 采取了打散运行,削峰填谷
的思想,从而 prevent long time gc
但是仔细想想,我们自己编程的时候,正常情况下从来也没冒出过“这个地方应该触发一下 GC”这样想法吧?
因为我们知道,Java 程序员来说,虚拟机有自己的 GC 机制,我们不需要像写 C 或者 C++ 那样得自己管理内存,只要关注于业务代码即可,并没有特别注意 GC 机制。
那么本文中最关键的一个问题就来了:为什么这里要在代码里面特别注意 GC,想要尝试触发GC
呢
究其原因:safepoint
,安全点。
关于安全点的描述,我们可以看看《深入理解JVM虚拟机(第三版)》的 3.4.2 小节:
注意书里面的描述:
有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停
换言之:没有到安全点,是不能 STW
,从而进行 GC
的,如果在你的认知里面 GC 线程是随时都可以运行的。那么就需要刷新一下认知了
接着,让我们把目光放到书的 5.2.8 小节:由安全点导致长时间停顿。
里面有这样一段话:
我把划线的部分单独拿出来,你仔细读一遍:
但是
HotSpot
虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,所以使用int
类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环(Counted Loop)
,相对应地,使用long
或者范围更大的数据类型作为索引值的循环就被称为不可数循环(Uncounted Loop)
,将会被放置安全点。
意思就是在可数循环(Counted Loop)
的情况下,HotSpot
虚拟机搞了一个优化,就是等循环结束之后,线程才会进入安全点。
反过来说就是:循环如果没有结束,线程不会进入安全点,GC
线程就得等着当前的线程循环结束,进入安全点,才能开始工作。
接着我们再把目光拉回到这里:
这个循环是一个可数循环。
Thread.sleep(0)
这个代码看起来莫名其妙,但是是不是可以大胆的猜测一下:故意写这个代码的人,是不是为了在这里放置一个 Safepoint
呢,以达到避免 GC
线程长时间等待,从而加长 stop the world
的时间的目的呢
所以,接下来只需要找到 sleep
会进入 Safepoint
的证据,就能证明了猜想
在源码的注释里面,直接找到了:
源码地址:https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/tip/src/share/vm/runtime/safepoint.cpp
注释里面说,在程序进入Safepoint
的时候, Java
线程可能正处于框起来的五种不同的状态,针对不同的状态有不同的处理方案。
主要聚焦于和本文相关的第二点:Running in native code
When returning from the native code, a Java thread must check the safepoint _state to see if we must block.
第一句话,就是答案,意思就是一个线程在运行 native
方法后,返回到 Java
线程后,必须进行一次 safepoint
的检测。
同时知乎看到了这个回答,里面有这样一句,也印证了这个点:
地址链接:https://www.zhihu.com/question/29268019/answer/43762165
1.3 实践
前面其实说的都是理论
这一部分我们来拿代码实践跑上一把
public class MainTest {
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable runnable=()->{
for (int i = 0; i < 1000000000; i++) {
num.getAndAdd(1);
}
System.out.println(Thread.currentThread().getName()+"执行结束!");
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("num = " + num);
}
}
按照代码来看,主线程休眠 1000ms
后就会输出结果,但是实际情况却是主线程一直在等待 t1,t2 执行结束才继续执行。
这个循环就属于前面说的可数循环(Counted Loop)
这个程序发生了什么事情呢
- 启动了两个长的、不间断的循环(内部没有安全点检查)。
- 主线程进入睡眠状态 1 秒钟。
- 在1000 ms之后,JVM尝试在Safepoint停止,以便Java线程进行定期清理,但是直到可数循环完成后才能执行此操作。
- 主线程的 Thread.sleep 方法从 native 返回,发现安全点操作正在进行中,于是把自己挂起,直到操作结束。
所以,当我们把 int 修改为 long 后,程序就表现正常了:
1.4 附加
再说一个也是由前面的 RocketMQ
的源码引起的一个思考:
这个方法是在干啥?
预热文件,按照 4K 的大小往 byteBuffer 放 0,对文件进行预热。byteBuffer.put(i, (byte) 0);
更多推荐
所有评论(0)