大家好,我是伟伟。前几天在一个开源项目的github上看到这样一个pr:光看名字,里面有MemorySafe,有点卡壳。让我先给你看这个东西:这一定很眼熟吧?我从阿里巴巴的开发规范中截取了截图。为什么不推荐使用FixedThreadPool和SingleThreadPool?因为队列太长,请求会堆积,请求堆积很容易造成OOM。那么问题又来了:前面说的线程池使用的队列是什么?使用没有指定长度的LinkedBlockingQueue。不指定长度,默认长度为Integer.MAX_VALUE,可以理解为无界队列:所以,在我看来,使用LinkedBlockingQueue可能会导致OOM。如果想避免这种OOM,需要在初始化时指定一个合理的值。“合理价值”听起来有点轻描淡写,但这个价值是多少,你确定吗?基本上说不出来。所以,当我在pr上看到MemorySafeLinkedBlockingQueue这个名字时,我就爱上了它。在LinkedBlockingQueue前面添加限定符MemorySafe。表明这是一个内存安全的LinkedBlockingQueue。于是,我想研究一下如何做到“安全”,于是点进去,很快。这个pr中的MemorySafeLBQ,我们来看看它主要想干什么:https://github.com/apache/dub...提供代码的哥们是这样描述它的作用的:完全可以解决问题LinkedBlockingQueue引起的OOM问题,而且不依赖插桩,优于MemoryLimitedLinkedBlockingQueue。然后可以看到这次commit涉及到7个文件。其实真正的核心代码就是这两个:不过别慌,我们先熟悉一下这两个类,然后我先按下按钮。先追本溯源,从源头上。这两个类的名字太长了,先约定一下。在本文中,我使用MemoryLimitedLBQ而不是MemoryLimitedLinkedBlockingQueue。使用MemorySafeLBQ而不是MemorySafeLinkedBlockingQueue。可以看到,它在pr中也提到了“比MemoryLimitedLBQ更好”。换句话说,它用于替换MemoryLimitedLBQ类。这个类从名字上也能看出来。它也是一个LinkedBlockingQueue,但是它的限定符是MemoryLimited,可以限制内存。我找了一下,这个类对应的pr是这样的:https://github.com/apache/dub...在这个pr中,有个大佬问他:你newqueueimplementation的意义或目的是什么?您能否命名当前存储库中需要被该队列替换的队列?这样我们就可以决定是否使用这个队列。也就是说他只是提交了一个新的queue,但是他并没有说应用场景是什么,所以官方也不知道是否接受这个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方法中,是这样使用的:知道方法名,得到对象的大小,这个对象就是输入方法的参数,也就是把Elements放入队列中。为了证明我不是在胡说八道,我带大家看看这个方法的注释:指定对象消耗存储量的特定实现的近似值注意这个词:近似值。这是一本正经的四级词汇,还是a开头,长得不眼熟就要被罚。整个句子的翻译是:返回指定对象消耗的存储量的特定于实现的近似值。说白了就是你传入的对象在内存中占用了多长时间?这个长度不是一个非常精确的值。那么,在理解了inst.getObjectSize(e)这行代码之后,我们再仔细看看acquireInterruptibly是如何工作的:首先标①的两个地方表示必须锁定这个方法的运行,整个try中的方法是线程安全的。那么标有②的里面是怎么回事呢?就是计算LongAdder类型的内存加上当前对象的值之和是否大于等于memoryLimit。如果计算出来的值真的超过了memoryLimit,说明需要阻塞,调用notLimited.await()方法。如果没有超过memoryLimit,说明队列中还可以放东西,然后更新memory的值。然后来到标有③的地方。这里又来判断一下当前使用的值是否没有超过memoryLimit。如果是,调用notLimited.signal()方法唤醒由于memoryLimit参数限制无法放置的对象。整个逻辑非常清晰。整个逻辑中的核心逻辑是调用Instrumentation类型的getObjectSize方法获取当前放置对象的大小,判断当前使用的值加上大小是否大于我们设置的最大值。所以,你可以用脚趾猜出来。在release方法中,必须计算当前对象的大小,然后从内存中减去:说白了,就是这么大的事。然后,您再次查看acquireInterruptibly方法的try代码块中的逻辑。你有没有发现任何错误?好的?如果你还没反应过来,我直接给你代码。在随后的提交中,sum被更改为memory.sum():为什么要这样更改?给大家讲一个场景,假设我们的memoryLimit是1000,当前使用的内存是800,也就是sum是800。这时候我要放的元素的计算大小是300,也就是objectSize为300,sum+objectSize=1100,大于memoryLimit的值,是不是在while判断的时候拦截了:之后,假设队列中又释放了一个大小为600的对象。此时执行memory.add(-objectSize)方法,内存变为200:然后会调用signalNotLimited方法唤醒被拦截的小伙伴:一旦小伙伴被唤醒,看代码:while(sum+objectSize>=memoryLimit){notLimited.await();}我心想:我这里的sum是800,objectSize是300,还是比memoryLimit大。为什么要叫醒我,笨蛋?那你说,骂谁呢?这个地方的代码肯定是这样的,每次检查最新的内存值:while(memory.sum()+objectSize>=memoryLimit){notLimited.await();}所以,这个地方是BUG,或死循环错误。之前代码的截图里面还有一个链接,就是说这个BUG:https://github.com/apache/inc...另外可以看到链接里的项目名是incubator-shenyu,它是一个开源的APIGateway:本文中的MemoryLimitedLBQ和MemorySafeLBQ来源于这个开源项目。MemorySafeLBQ之前了解过MemoryLimitedLBQ的基本原理。接下来就带大家看看MemorySafeLBQ。它的源码可以通过这个链接直接获取:https://github.com/apache/dub...也是那种拿出来就可以放到自己项目里,改作者的以自己的名义归档。回到开头:这个pr说我创建MemorySafeLBQ来代替MemoryLimitedLBQ,因为我比它好用,而且我不依赖Instrumentation。但是看完源码你会发现其实思路是差不多的。只是MemorySafeLBQ属于对立面。什么样的“逆向”方法?看源码:MemorySafeLBQ还是继承自LinkedBlockingQueue,但是多了一个自定义成员变量maxFreeMemory,初始值为25610241024。这个变量的名字很值得注意,大家可以仔细看看。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多行代码:而整个方法的核心就是我框架的静态代码块。里面有三行代码。.第一行是调用refresh方法,即重新赋值参数maxAvilable。该参数表示当前可用的JVM内存。第二行注入一个每50毫秒运行一次的计划任务。时间到了,触发refresh方法,保证maxAvilable参数的准实时性能。第三行加入JVM的ShutdownHook。当服务停止时,需要停止定时任务,以达到优雅关机的目的。这就是核心逻辑。在我看来,确实比MemoryLimitedLBQ更简单,更好用。最后看一下作者提供的MemorySafeLBQ测试用例。我加了一点注释,很容易理解。自己去尝尝,别多说:就是你的了。文中提到的MemoryLimitedLBQ和MemorySafeLBQ,我说了,这两个gadget是完全独立于框架的,直接贴上代码就可以使用。只有几行代码。无论是使用Instrumentation还是ManagementFactory,其核心思想都是限制内存。让我们扩展这个想法。比如我们有些项目使用Map作为本地缓存,里面会放很多元素,也会有OOM的风险。那么通过上面提到的思路,我们是不是找到了解决问题的方法呢?因此,思路很重要。掌握了这个思路之后,面试的时候就可以多说一点了。再比如,看到这个东西,我就想到了之前写的线程池参数动态调整。以MemorySafeLBQ队列为例。里面的参数maxFreeMemory可以动态调整吗?无非就是把之前的可调队列长度改成可调队列占用的内存空间。只是参数变化,直接套用实施方案即可。这些都是我从开源项目中看到的东西,但当我看到它们的那一刻,它就是我的。现在,我把它写出来,与你分享,它就是你的了。不客气,只是三合一。
