sleep(0)代码经常出入在一些中间件的源码中,但你有没有思考过这个函数的用意呢?
下面是一段RocketMQ的源码,org.apache.rocketmq.store.logfile.DefaultMappedFile#warmMappedFile
for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
byteBuffer.put(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);
}
}
}
可以看到,这里也用到了Thread.sleep(0);
既然是 sleep 0 毫秒,那么他跟去掉这句代码相比,有啥区别么?
正文
操作系统回顾
我们先回顾一下操作系统原理。
操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。在时间片算法中,所有的进程排成一个队列。操作系统按照他们的顺序,给每个进程分配一段时间,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
所谓抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU 。因此可以看出,在抢 占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出 CPU 。
在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过 CPU 了),给他们算出一个总的优先级来。操作系统就会把 CPU 交给总优先级最高的这个进程。当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一 次所有进程的总优先级,然后再挑一个优先级最高的把 CPU 控制权交给他。
Thread.sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。众所周知,GC 线程具有低优先级,因此Thread.sleep(0)
用于帮助 GC 线程尝试竞争 CPU 时间片。但是为什么作者说可以防止long time GC
呢?这就讲到 JVM 的垃圾回收原理了。
安全点
在在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂。
实际上HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint)。有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。因此,安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而长时间执行,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转(末尾)、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。
为了避免安全点过多带来的沉重负担,HotSpot 虚拟机还有一个针对循环的优化措施。如果循环次数少,执行时间不宜过长。因此,默认情况下不会将使用 int 或更小数据类型作为索引值的循环放置在安全点中。这种循环称为可数循环。相应地,使用 long 或更大范围的数据类型作为索引值的循环称为未计数循环,将被放置在安全点。
以HotSpot
虚拟机为例,JVM 并不会在代码指令流的任何位置暂停以启动垃圾回收,而是强制执行必须到达安全点才暂停。换句话说,在到达安全点之前,JVM 不会为 GC STOP THE WORLD
。
安全区域
safepoint可以让运行中的线程主动挂起,而其他状态下的线程(比如正在sleep或者正阻塞在一个锁上)则无法主动运行到safepoint。对于这种情况jvm中设置了安全区(safe region)的概念,当线程处于某些状态时,jvm认为这种情况下这个线程不会对jvm heap做出任何修改,因此不会破坏jvm的“确定”的状态,所以这些线程可以认为是安全的(处于安全区)。当处于安全区的线程要从安全区出来的时候,同样需要检查是否应该主动挂起。jvm中设定以下几种状态的线程就是处于safe region:
- 处于阻塞或者等待中
- 正在执行JNI方法
所以当线程处于以上几个状态时,我们就认为它们就和达到safepoint一样,可以执行特殊的操作了。为了防止线程从safe region返回后对jvm heap进行更改,当STW时线程在从safe region返回时都会主动挂起。
因此,GC 线程必须等到线程执行完毕,才能执行到最近的安全点。但如果使用Thread.sleep(0)
,则可以在代码中放置一个安全点。
总结
Thread.sleep(0)
不是什么无用的代码。sleep
方法可用于在 java 代码中放置一个安全点。可以提前在长循环中触发 GC,避免 GC 线程长时间等待,从而避免达到拉长 GC 时间的目的。
文档信息
- 本文作者:L1Chenxv
- 本文链接:https://l1chenxv.github.io//2023/08/10/sleep(0)/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)