你好呀,我是喜提七天居家隔离得歪歪。
这篇文章要从一个奇怪的注释说起,就是下面这张图:
无需理会繁杂的代码逻辑精细的规则, 仅仅专注这一个因循特定序规运转的 for 循环装置指示标点。
在循环里面,专门有个变量 j,来记录当前循环次数。
进了一个 if 逻辑是这样子的, 在第一次循环过后, 往后每 1000 次循环之后, 都要进入它。
在这个 if 逻辑之上,标注了一个注释: gc.
,这个单词如果不认识的同学记一下,考试肯定要考的:
这个注释翻译一下就是:防止 GC 线程进行垃圾回收。
具体的实现逻辑是这样的:
核心逻辑其实就是这样一行代码:
.sleep(0);
这样就能实现 gc 了?
懵逼吗?
懵逼就对了,懵逼就说明值得把玩把玩。
这个代码片段,其实是出自 的源码:
org...store..#
需要提前予以说明的是, 并未寻得编写此代码之人去询问其意图究竟是什么, 故而只能凭借自身的理解来对其意图加以推测。要是推测存在偏差, 还望能予以诸多指教。
尽管这属于, 的源码范畴, 然而依据我的理解, 此小技巧跟, 框架并无丝毫关联, 全然能够脱离框架而独立存在。
我给出的修改意见是这样的:
若要将 int 变更为 long, 那么接着就能够直接把 for 循环之中的 if 逻辑予以删除掉了。
这样一看是不是更加懵逼了?
不要慌,接下来,我给你抽丝剥个茧。
另外,在“剥茧”之前,我先说一下结论:
探索
当我知晓这个代码片段归属之际, 我脑海浮现的首个要点便是于代码提交记录里寻觅答案。
看提交者是否在提交代码的时候说明了自己的意图。
于是我把代码拉了下来,一看提交记录是这样的:
我就知道这里不会有答案了。
由于该类在首次提交之际便已涵盖此逻辑, 并且针对此次提交而言代码数量颇为可观, 然而却未对相应功能予以特别阐明。
从提交记录上没有获得什么有用的信息。
于是, 我将目光转向了, 称为的那个issue, 随后, 手持关键词gc, 展开了一番搜索。
除了第一个链接之外,没有找到什么有用的信息:
而第一个链接对应的 是这个:
这个事实是基于我们在针对此问题开展讨论的历经阶段其间所呈涌现而为, 它恰恰就是于先前曾经出现过的那项予以修正的方案, 此方案就是这样的。
也就是说, 我想要借助源码, 或者寻找到这个问题具有权威性的回答, 然而却是找不到了。
紧接着, 我再度前往了此个充满神奇色彩的网站, 于其内部寻觅到了这般提出于二零一八年的问题。
问题和我们的问题一模一样,但是这个问题下面就这一个回答:
这个回答不咋样, 因我感觉没切中要点。不过没关系, 我恰好能将此回答当作着力点, 把欠缺的那一方面贯通对齐, 赋予其能量。
先看这个回答的第一句话:It does not(它没有)。
问题就来了:“它”是谁?“没有”什么?
“它”,指的就是我们前面出现的代码。
“没有”,是说没有防止 GC 线程进行垃圾回收。
该的的回应讲: 使用调用 .sleep(0) 的意图在于给予 GC 线程拥有被操作系统挑选中去展开做好垃圾清理的工作的可能性。其产生的副作用是, 也许会更为高频连贯地运行 GC, 究其原因是你每 1000 次迭代便存在一回运行 GC 的时机, 然而其好处是能够避免出现长时间的垃圾收集情况。
反过来说, 此代码意在“触发”垃圾回收机制, 而非“避免”其发生, 或者确切而言是“避免”耗时冗长的垃圾回收操作。照此层面来讲,程序之中的注释实则存在虚假陈述情况或者表述并不完整。
并非是gc, 而是针对gc采用了“打散运行, 削峰填谷”这样的思想, 此思想进而导致了long time gc。
但是, 你去仔细思索一番, 当我们自身在从事编程工作的时候, 于正常的状况之下, 从来都未曾出现过像“这个地方应当触发一下 GC”这般的思及?
对Java程序员而言, 由于我们晓得, 虚拟机存在自身的GC机制, 以致于我们无需如同编写C或者C++那般自行管理内存, 只需将注意力集中于业务代码就行, 实际上并没有特别专注GC机制。
这样一来, 本文里头特别最为关键的某一个问题便随之产生了, 何以在编码过程当中要在此处格外留意垃圾回收机制, 并且试图去促使垃圾回收机制启动呢?
先说答案:,安全点。
你可以看看《深入理解 JVM 虚拟机(第三种版本)》, 依据, 描述安全点, 去看它的 3.4.2 小部分节:
注意书里面的描述:
有着关于安全点的设定, 这就使得用户程序在执行之际, 并不意味着在代码指令流随便的一处位置都可以停顿下来进而开启垃圾收集, 而是硬性有所要求, 一定要执行抵达安全点之后才能够实施暂停。
换言之:没有到安全点,是不能 STW,从而进行 GC 的。
倘若于你那认知范畴当中, GC线程是能够随时去运行的, 那么就得刷新一回认知了。
随后, 我们将目光投向书本的5.2.8小节, 此节内容乃是因安全点生成长时间停顿。
里面有这样一段话:
我把划线的部分单独拿出来,你仔细读一遍:
对于虚拟机而言, 为避免安全点过多致使其负担过重, 针对循环采取了一项优化举措, 其认为倘若循环次数较少, 那么执行时间想必也不会过长, 故而使用int类型或者范围更小的数据类型作为索引值的循环, 默认不会被放置安全点。这种循环被称作可数循环(Loop), 与之相对应的是, 使用long或者范围更大的数据类型作为索引值的循环就被叫做不可数循环(Loop), 它将会被放置安全点。
便是说, 处于可数循环即Loop的情形下, 虚拟机实施了一项优化, 此优化为, 待循环终结之后, 线程才会步入安全点。
倒过来讲便是: 要是循环尚未结束呢, 那么线程就不会进入安全点, 如此一来, GC 线程就得等待当前的线程结束循环, 进而进入安全点, 之后才能够开始工作。
什么是可数循环( Loop)?
书里面的这个案例来自于这个链接:
HBase实战:记一次导致长时间STW的踩坑之旅
要是你存在着一些可支配的时间, 那我向你提出这样的建议, 你要把这个案例完完全全地去看一下, 而我只是截取了其中问题得以解决的那一部分内容:
在截图里所呈现的while(i < end), 它属于一类可数循环体状况 , 既然用以推行这一循环现象的线程, 是要在循环已然终结后才实行进一步进展。因此, 率先进入的线程, 就得对其予以等待。这样一来, 便会对GC线程后续的运行态势造就影响。
所以,修改方案就是把 int 修改为 long。
其原理在于将其转变为不可数的那种循环, 也就是Loop, 凭借此得以不必等待循环结束, 在其处于循环的这个期间即能够进入。
接着我们再把目光拉回到这里:
这个循环也是一个可数循环。
代码中的.sleep(0), 看起来让人摸不着头脑,然而, 我能不能大胆地进行一番猜测? 这般故意写下此代码的人, 是不是想要在此处放置一个东西? 其目的在于避免GC线程长时间处于等待状态, 进而加长stop the world的时间?
所以, 我接下来, 只需找到, sleep会进入的, 证据, 便能够证明, 我的猜想。
你猜怎么着?
原本是打算去瞅一瞅源码, 然而瞬间就在源码的注释当中, 一下子瞧见并发现了:
注释当中讲道, 当程序开始进入之际, Java线程有可能正处在被框起来的五种不一样的状态, 针对不同状态存有不同的处理办法。
打从一开始, 我是打算逐个进行翻译的, 可是信息的数量规模实在太大了, 致使我在进行消化吸收的时候, 耗费了较多的精力, 因而就不再随意发表言语了。
主要聚焦于和本文相关的第二点: in code。
第一句话, 即为答案, 其意思是, 一个线程于运行方法之后, 在返回到Java线程之际, 必须要进行一次检测。
同一时间, 我于知乎瞧见了R大的这般回答, 其中存在这样一句话, 它也证实了这个要点:
那么接下来,就是见证奇迹的时刻了:
按照R大的相关表述, 将正在对函数予以执行的线程视作“已经进入了”这种状态, 或者把这样的情形称作“处于safe -里”。
sleep 方法就是一个 方法,你说巧不巧?
因此, 于此我们得以明确的是以下事宜, 也就是: 实施调用 sleep 这个方法的线程, 在这种情形下会进而进入到相应的状态之中。
除此以外, 我又寻觅到了一个2013年的, R大针对类似问题展开讨论的帖子。
这里就直接点名道姓的指出了:.sleep(0).
这让我想起以前有个面试题问:.sleep(0) 有什么用。
原来当时我所想的是在那时候觉着这道题目真的是特别难, 现在才发觉原来是自身所具备的能力、水平还不够, 没想到最终像小丑一样的竟然是我自己呀。
还真的是有用。
实践
前面其实说的都是理论。
在这一部分, 我们要借助代码实践来实际操作一番, 所选用的案例是我之前分享过的一篇文章里的, 那篇文章叫《真是绝了!这段被JVM动了手脚的代码!》。
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); } }这个代码,你直接粘到你的 idea 里面去就能跑。
依照代码而言, 主线程作休眠处理, 之后便会将结果输出, 然而实际情形却是, 主线程始终在等待, 等待t1执行完毕, 等待t2执行完毕, 之后才会继续执行。
这个循环就属于前面说的可数循环( Loop)。
这个程序发生了什么事情呢?
那么, 在我们将 int 变更为 long 这一操作之后, 程序显现出了正常的表现:
受到 源码的启示,我们还可以直接把它的代码拿过来:
这样, 哪怕for循环所面对的对象属于int类型, 亦能够依照预定的情况去执行。缘由于我们等同于在循环体内部插入了。
另外,我通过不严谨的方式测试了一下两个方案的耗时:
在我的机器上运行了几次,时间上都差距不大。
然而要是谈及逼格, 那非得是右边的, gc 的那种写法才行。没有长达二十年的功力, 是没法撰写出来这一行, ‘看上去好像没什么用’的代码的啊!
额外提一句
再说一个也是由前面的 的源码引起的一个思考:
这个方法是在干啥?
预热文件,按照 4K 的大小往 放 0,对文件进行预热。
.put(i, (byte) 0);
为什么我会对这个 4k 的预热比较敏感呢?
去年的天池大赛有这样的一个赛道:
其中有两个参赛选大佬都提到了“文件预热”的思路。
我把链接放在下面了,有兴趣的可以去细读一下:
最后,感谢你阅读我的文章,欢迎关注我,谢谢。