前言
这两天看了篇关于JVM GC的文章,虽然不是很懂但是大为震撼,感觉可以记录一下以后装逼(交流)的时候用得上
原文地址在这juejin.cn/post/713974…
正篇
大概讲的是这么一个事情,原文的大佬在rocketMQ中发现了一段这样的代码
是不是一脸懵逼,循环每隔1000次,线程进入睡眠0秒,看起来没什么用的一段代码体现出了开发的人深厚的底蕴,等我学会了我也来装逼。
好了我来总结一下这里为什么这么写,没什么干货就是做总结,具体的分析看原文。
GC
众所周知JAVA语言在JVM的加持下不需要开发者手动去释放内存,会有一个GC线程作为守护线程去清理内存中的垃圾。
GC线程是什么时候进行垃圾回收的呢,这里是一大堆知识,什么新生代、老生代、永久代、from区、to区、幸存者区之类的,然后根据JVM参数和垃圾回收器的策略去决定要不要GC。先讲上文代码的一个结论Thread.sleep(0)是为了使线程有机会进行GC。
是不是还是有些懵逼,不是说不需要开发者手动去做垃圾回收吗,那为什么还需要调用。
安全点
这里引出一个知识Safe Point(安全点),GC并不是随时随地都可以进行的,需要所有线程都跑到安全点,然后再进行GC,原因是因为决定一个对象是否需要清理是根据可达性分析,而可达性分析需要枚举出所有的GC ROOT,那为什么不能程序边跑边枚举呢,是因为枚举GC ROOT的过程也挺麻烦的,不可能直接排查堆中所有对象去确定,所以提出了一个OOP MAP,方法的执行在JVM中是以栈帧的形式保存的,所谓OOP MAP其实是描述了这样一个范围内出现对象引用的记录,记录一个起始地址和一个偏移量,来代表这样一个位置存在一个对象,我们知道栈中的对象属于GC ROOT,因此可以通过遍历OOP MAP来确定GC ROOT,OOP MAP不是全局唯一的,也不是一个线程一个的,也不是一个栈帧一个的,但是一个线程会存在一个或多个OOP MAP,这里不继续展开,OOP MAP的创建是需要依赖安全点,直接来个总结,OOP MAP描述的是一段范围,那两个安全点之间是否存在引用,存在哪些引用,就是OOP MAP需要记录的东西,因此安全点是GC中不可或缺的。
削峰
基本上都GC的过程都避免不了STW(stop the world),讲一个简单的道理,垃圾越多清理的时间(STW)越久没意见吧,直接来个结论,这里意思是可以提前让线程进入安全点,然后早点GC避免STW时间过长。
安全点的设置
讲完是不是还是一脸懵逼,是不是还是不知道为啥要特别关注这个呢,这里就是关键中的关键了,JVM不会在没行代码之后都设置安全点,JVM也是有原则的,所以我们大概了解下,一般的方法是不会跑太久的,如果跑了很久那基本上是出现了指令复用,所谓的复用就是只方法调用、循环、异常跳出等就是复用,因此在这些地方设置安全点。
不过JVM还是想着优化,所以做了个事情针对于循环这个场景做了一个优化,像是int类型或者更小范围类型的循环被称作可数循环,范围更大的称之为不可数循环,换句话说就是JVM觉得你范围小,不需要每次循环结束都放安全点,就等你跑完再设置安全点。
这就是开头那段代码中为什么要每循环1000次执行一下Thread.sleep(0)为的就是能够设置安全点
为什么Thread.sleep(0)能设置安全点
这就是整篇文章的重点,sleep方法是一个native方法,程序进入native方法之后就相当于进入了JVM管理范围之外,并且不会再对JVM内的数据状态有所影响,因此需要GC的时候可以直接无视正在native方法中的线程,而native方法返回之后是不是就不知道当前JVM所处的状态了嘛(不知道外面现在照常跑着还是在GC中),那么就设置好安全点,直接进去就是了。
这就是为什么手动写Thread.sleep(0)的核心目的,为了能够设置安全点。原文中还有一种方法,设置安全点,就是将循环上的int条件改为long,这样就使可数循环变为了不可数循环,这里我想会不会是因为这样写会让每个循环都设置安全点影响了JVM性能,不过原文大佬也测试了下,好像性能并没有什么差别。都记一下好了,人在江湖飘,技多不压身。