共计 6233 个字符,预计需要花费 16 分钟才能阅读完成。
你好呀,我是歪歪。
前几天在一个开源我的项目的 github 外面看到这样的一个 pr:
光是看这个名字,外面有个 MemorySafe,我就有点陷进去了。
我先给你看看这个货色:
这个必定很眼生吧?我是从阿里巴巴开发标准中截的图。
为什么不倡议应用 FixedThreadPool 和 SingleThreadPool 呢?
因为队列太长了,申请会沉积,申请一沉积,容易造成 OOM。
那么问题又来了:后面提到的线程池用的队列是什么队列呢?
用的是没有指定长度的 LinkedBlockingQueue。
没有指定长度,默认长度是 Integer.MAX_VALUE,能够了解为无界队列了:
所以,在我的认知外面,应用 LinkedBlockingQueue 是可能会导致 OOM 的。
如果想防止这个 OOM 就须要在初始化的时候指定一个正当的值。
“正当的值”,听起来轻描淡写的四个字,然而这个值到底是多少呢,你说的准吗?
基本上说不准。
所以,当我看到 pr 上的 MemorySafeLinkedBlockingQueue 这个名字的时候,我就陷进去了。
在 LinkedBlockingQueue 后面加上了 MemorySafe 这个限定词。
示意这是一个内存平安的 LinkedBlockingQueue。
于是,我想要钻研一下到底是怎么样来实现“平安”的,所以啪的一下就点进去了,很快啊。
MemorySafeLBQ
在这个 pr 外面咱们看一下它次要是想干个什么事儿:
https://github.com/apache/dub…
提供代码的哥们是这样形容它的性能的:
能够齐全解决因为 LinkedBlockingQueue 造成的 OOM 问题,而且不依赖 instrumentation,比 MemoryLimitedLinkedBlockingQueue 更好用。
而后能够看到这次提交波及到 7 个文件。
实际上真正外围的代码是这两个:
然而不要慌,先眼生一下这两个类,而后我先按下不表。先追根溯源,从源头上讲。
这两个类的名字太长了,所以先约定一下,在本文中, 我用 MemoryLimitedLBQ 来代替 MemoryLimitedLinkedBlockingQueue。用 MemorySafeLBQ 来代替 MemorySafeLinkedBlockingQueue。
能够看到,在 pr 外面它还提到了“比 MemoryLimitedLBQ 更好用”。
也就是说,它是用来代替 MemoryLimitedLBQ 这个类的。
这个类从命名上也看得出来,也是一个 LinkedBlockingQueue,然而它的限定词是 MemoryLimited,能够限度内存的。
我找了一下,这个类对应的 pr 是这个:
https://github.com/apache/dub…
在这个 pr 外面,有大佬问他:
你这个新队列实现的意义或目标是什么?你能不能说出以后版本库中须要被这个队列取代的队列?这样咱们才好决定是否应用这个队列。
也就是说他只是提交了一个新的队列,然而并没有说到利用场景是什么,导致官网不晓得该不该承受这个 pr。
于是,他补充了一个回复:
就是拿的 FixedThreadPool 做的示例。
在这个外面,就应用了无参的 LinkedBlockingQueue,所以会有 OOM 的危险。
那么就能够应用 MemoryLimitedLBQ 来代替这个队列。
比方,我能够限度这个队列能够应用的最大内存为 100M,通过限度内存的形式来达到防止 OOM 的目标。
好,到这里我先给你梳理一下。
首先应该是有一个叫 MemoryLimitedLBQ 的队列,它能够限度这个队列最大能够占用的内存。
而后,因为某些起因,又呈现了一个叫做 MemorySafeLBQ 的队列,声称比它更好用,所以来取代它。
所以,接下来我就要梳理分明三个问题:
- MemoryLimitedLBQ 的实现原理是什么?
- MemorySafeLBQ 的实现原理是什么?
- MemorySafeLBQ 为什么比 MemoryLimitedLBQ 更好用?
MemoryLimitedLBQ
别看这个玩意我是在 Dubbo 的 pr 外面看到的,然而它实质上是一个队列的实现形式。
所以,齐全能够脱离于框架而存在。
也就是说,你关上上面这个链接,而后间接把相干的两个类粘进去,就能够跑起来,为你所用:
https://github.com/apache/dub…
我先给你看看 MemoryLimitedLBQ 这个类,它就是继承自 LinkedBlockingQueue,而后重写了它的几个外围办法。
只是自定义了一个 memoryLimiter 的对象,而后每个外围办法外面都操作了 memoryLimiter 对象:
所以真正的机密就藏在 memoryLimiter 对象外面。
比方,我带你看看这个 put 办法:
这外面调用了 memoryLimiter 对象的 acquireInterruptibly 办法。
在解读 acquireInterruptibly 办法之前,咱们先关注一下它的几个成员变量:
- memoryLimit 就是示意这个队列最大所能包容的大小。
- memory 是 LongAdder 类型,示意的是以后曾经应用的大小。
- acquireLock、notLimited、releaseLock、notEmpty 是锁相干的参数,从名字上能够晓得,往队列外面放元素和开释队列外面的元素都须要获取对应的锁。
- inst 这个参数是 Instrumentation 类型的。
后面几个参数至多我还很眼生的,然而这个 inst 就有点奇怪了。
这玩意日常开发中基本上用不上,然而用好了,这就是个黑科技了。很多工具都是基于这个玩意来实现的,比方赫赫有名的 Arthas。
它能够更加不便的做字节码加强操作,容许咱们对曾经加载甚至还没有被加载的类进行批改的操作,实现相似于性能监控的性能。
能够说 Instrumentation 就是 memoryLimiter 的关键点:
比方在 memoryLimiter 的 acquireInterruptibly 办法外面,它是这样的用的:
看办法名称你也晓得了,get 这个 object 的 size,这个 object 就是办法的入参,也就是要放入到队列外面的元素。
为了证实我没有乱说,我带你看看这个办法上的正文:
an implementation-specific approximation of the amount of storage consumed by the specified object
留神这个单词:approximation.
这可是正儿八经的四级词汇,还是 a 结尾的,你要是不眼生的话可是要挨板子的。
整句话翻译过去就是:返回指定对象所耗费的存储量的一个特定实现的近似值。
再说的直白点就是你传进来的这个对象,在内存外面到底占用了多长的长度,这个长度不是一个十分准确的值。
所以,了解了 inst.getObjectSize(e) 这行代码,咱们再认真看看 acquireInterruptibly 是怎么样的:
首先,两个标号为 ① 的中央,示意操作这个办法是要上锁的,整个 try 外面的办法是线程平安的。
而后标号为 ② 的外面干了什么事儿?
就是计算 memory 这个 LongAdder 类型的 sum 值加上以后这个对象的值之后,是不是大于或者等于 memoryLimit。
如果计算后的值真的超过了 memoryLimit,那么阐明须要阻塞一下下了,调用 notLimited.await() 办法。
如果没有超过 memoryLimit,阐明还能往队列外面放货色,那么就更新 memory 的值。
接着到了标号为 ③ 的中央。
来到这里,再次判断一下以后曾经应用的值是否没有超过 memoryLimit,如果是的话,就调用 notLimited.signal() 办法,唤醒一下之前因为 memoryLimit 参数限度导致不能放入的对象。
整个逻辑十分的清晰。
而整个逻辑外面的外围逻辑就是调用 Instrumentation 类型的 getObjectSize 办法取得以后放入对象的一个 size,并判断以后曾经应用的值加上这个 size 之后,是否大于了咱们设置的最大值。
所以,你用脚趾头猜也能猜到了,在 release 办法外面,必定也是计算以后对象的 size,而后再从 memory 外面减进来:
说穿了,也就这么屁大点事儿。
而后,你再次扫视一下这个 acquireInterruptibly 办法的 try 代码块外面的逻辑,你有没有发现什么 BUG:
如果你没反映过去,那我再提个醒:你认真的剖析一下 sum 这个局部变量是不是有点不妥?
你要是还没反馈过去,那我间接给你上个代码。前面有一次提交,是把 sum 批改为了 memory.sum():
为什么这样改呢?
我给你说个场景,假如咱们的 memoryLimit 是 1000,以后曾经应用的 memory 是 800,也就是 sum 是 800。这个时候我要放的元素计算出来的 size 是 300,也就是 objectSize 是 300。
sum+objectSize=1100,比 memoryLimit 的值大,是不是在这个 while 判断的时候被拦挡住了:
之后,假如队列外面又开释了一个 size 为 600 的对象。
这个时候执行 memory.add(-objectSize) 办法,memory 变为 200:
那么会调用 signalNotLimited 办法,唤醒这个被拦挡的这个哥们:
这个哥们一被唤醒,一看代码:
while (sum + objectSize >= memoryLimit) {notLimited.await();
}
心里想:我这里的 sum 是 800,objectSize 是 300,还是大于 memoryLimit 啊,把我唤醒干啥玩意,傻逼吗?
那么你说,它骂的是谁?
这个中央的代码必定得这样,每次都查看最新的 memory 值才行:
while (memory.sum() + objectSize >= memoryLimit) {notLimited.await();
}
所以,这个中央是个 BUG,还是个死循环的 BUG。
后面代码截图中还呈现了一个链接,就是说的这个 BUG:
https://github.com/apache/inc…
另外,你能够看到链接中的项目名称是 incubator-shenyu,这是一个开源的 API 网关:
本文中的 MemoryLimitedLBQ 和 MemorySafeLBQ 最先都是出自这个开源我的项目。
MemorySafeLBQ
后面理解了 MemoryLimitedLBQ 的基本原理。
接下来我带你看看 MemorySafeLBQ 这个玩意。
它的源码能够通过这个链接间接获取到:
https://github.com/apache/dub…
也是拿进去就能够放到本人的我的项目跑,把文件作者批改为本人的名字的那种。
让咱们回到最开始的中央:
这个 pr 外面说了,我搞 MemorySafeLBQ 进去,就是为了代替 MemoryLimitedLBQ 的,因为我比它好用,而且我还不依赖于 Instrumentation。
然而看了源码之后,会发现其实思路都是差不多的。只不过 MemorySafeLBQ 属于是反其道而行之。
怎么个“反其道”法呢?
看一下源码:
MemorySafeLBQ 还是继承自 LinkedBlockingQueue,只是多了一个自定义的成员变量,叫做 maxFreeMemory,初始值是 256 1024 1024。
这个变量的名字就十分值得注意,你再细细品品。maxFreeMemory,最大的残余内存,默认是 256M。
后面一节讲的 MemoryLimitedLBQ 限度的是这个队列最多能应用多少空间,是站在队列的角度。
而 MemorySafeLBQ 限度的是 JVM 外面的残余空间。比方默认就是当整个 JVM 只剩下 256M 可用内存的时候,再往队列外面加元素我就不让你加了。
因为整个内存都比拟吃紧了,队列就不能无限度的持续增加了,从这个角度来躲避了 OOM 的危险。
这样的一个反其道而行之。
另外,它说它不依赖 Instrumentation 了,那么它怎么检测内存的应用状况呢?
应用的是 ManagementFactory 外面的 MemoryMXBean。
这个 MemoryMXBean 其实你一点也不生疏。
JConsole 你用过吧?
上面这个界面进去过吧?
这些信息就是从 ManagementFactory 外面拿进去的:
所以,的确它没有应用 Instrumentation,然而它应用了 ManagementFactory。
目标都是为了获取内存的运行状态。
那么怎么看进去它比 MemoryLimitedLBQ 更好用呢?
我看了,要害办法就是这个 hasRemainedMemory,在调用 put、offer 办法之前就要先调用这个办法:
而且你看 MemorySafeLBQ 只是重写了放入元素的 put、offer 办法,并不关注移除元素。
为什么呢?
因为它的设计理念是只关怀增加元素时候的残余空间大小,它甚至都不会去关注以后这个元素的大小。
而还记得后面讲的 MemoryLimitedLBQ 吗?它外面还计算了每个元素的大小,而后搞了一个变量来累加。
MemoryLimitedLBQ 的 hasRemainedMemory 办法外面也只有一行代码,其中 maxFreeMemory 是类初始化的时候就指定好了。那么要害的代码就是 MemoryLimitCalculator.maxAvailable()。
所以咱们看看 MemoryLimitCalculator 的源码。
这个类的源码写的十分的简略,我全副截完都只有这么一点内容,全副加起来也就是 20 多行代码:
而整个办法的外围就是我框起来的 static 代码块,外面一共有三行代码。
第一行是调用 refresh 办法,也就是对 maxAvilable 这个参数进行从新赋值,这个参数代表的意思是以后还能够应用的 JVM 内存。
第二行是注入了一个每 50ms 运行一次的定时工作。到点了,就触发一下 refresh 办法,保障 maxAvilable 参数的准实时性。
第三行是退出了 JVM 的 ShutdownHook,停服务的时候须要把这个定时工作给停了,达到优雅停机的目标。
外围逻辑就这么点。
从我的角度来说,的确是比 MemoryLimitedLBQ 应用起来更简略,更好用。
最初,再看看作者提供的 MemorySafeLBQ 测试用例,我补充了一点正文,很好了解,本人去品,不再多说:
它是你的了
文章外面提到的 MemoryLimitedLBQ 和 MemorySafeLBQ,我说了,这两个玩意是齐全独立于框架的,代码间接粘过去就能够用。
代码也没几行,不论是用 Instrumentation 还是 ManagementFactory,核心思想都是限度内存。
思路扩大一下,比方咱们有的我的项目外面用 Map 来做本地缓存,就会放很多元素进去,也会有 OOM 的危险,那么通过后面说的思路,是不是就找到了一个问题的解决方案?
所以,思路是很重要的,把握到了这个思路,面试的时候也能多掰扯几句嘛。
再比方,我看到这个玩意的时候,联想到了之前写过的线程池参数动静调整。
就拿 MemorySafeLBQ 这个队列来说,它外面的 maxFreeMemory 这个参数,可不可以做成动静调整的?
不外乎就是把之前的队列长度可调整批改为了队列占用的内存空间可调整。一个参数的变动而已,实现计划能够间接套用。
这些都是我从开源我的项目外面看到的,然而在我看到的那一刻,它就是我的。
当初,我把它写进去,分享给你,它就是你的了。
不客气,来个三连就行。