背景
在我们定义线程池时候,需要创建一个对列用来存储未执行而排队的任务,这个队列长度问题一直都是需要开发人员斟酌考虑点。在阿里巴巴开发手册中有怎么一个规则如:
说明: Executors返回的线程池对象的弊端如下
- FixedThreadPool和SingleThreadPool :允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
根据上面阿里规范中为什么不建议使用 FixedThreadPool 和 SingleThreadPool 呢?
因为队列太长了,请求会堆积,请求一堆积,容易造成 OOM。
那么问题又来了前面提到的线程池用的队列是什么队列呢?用的是没有指定长度的 LinkedBlockingQueue。没有指定长度,默认长度是 Integer.MAX_VALUE,可以理解为无界队列了。所以,在我的认知里面,使用 LinkedBlockingQueue 是可能会导致 OOM 的。如果想避免这个 OOM 就需要在初始化的时候指定一个合理的值。下面我针对这个合理的值进行开展设计思路。
内存限制队列思路(MemoryLimitedLBQ)
引入思路案例:
https://github.com/apache/dubbo/pull/9722
这个类从命名上也看得出来,也是一个 LinkedBlockingQueue,但是它的限定词是 MemoryLimited,可以限制内存的。
内存安全队列思路(MemorySafeLBQ)
引入思路案例
https://github.com/apache/dubbo/pull/10021
这个PR描述如下:
可以完全解决因为 LinkedBlockingQueue 造成的 OOM 问题,而且不依赖 instrumentation,比 MemoryLimitedLinkedBlockingQueue 更好用。
下面我们来深入了解一下MemoryLimitedLinkedBlockingQueue和MemorySafeLinkedBlockingQueue应用场景和解决问题。
MemoryLimitedLinkedBlockingQueue的实现原理是什么?
从字面意思理解就是内存限制队列,在 Dubbo 的 pr中了解到信息是:但是它本质上是一个队列的实现方式,并完全脱离与框架而存在。具体文件如:> https://github.com/apache/dubbo/pull/9722/files
我先给你看看 MemoryLimitedLBQ 这个类,它就是继承自 LinkedBlockingQueue,然后重写了它的几个核心方法。只是自定义了一个 memoryLimiter 的对象,然后每个核心方法里面都操作了 memoryLimiter 对象:
所以真正的秘密就藏在 memoryLimiter 对象里面。看看这个 put 方法:
这里面调用了 memoryLimiter 对象的 acquireInterruptibly 方法。在解读 acquireInterruptibly 方法之前,我们先关注一下它的几个成员变量:
- memoryLimit 就是表示这个队列最大所能容纳的大小。
- memory 是 LongAdder 类型,表示的是当前已经使用的大小。
- acquireLock、notLimited、releaseLock、notEmpty 是锁相关的参数,从名字上可以知道,往队列里面放元素和释放队列里面的元素都需要获取对应的锁。
- inst 这个参数是 Instrumentation 类型的。
Instrumentation参数解释:
这玩意日常开发中基本上用不上,但是用好了,这就是个黑科技了。很多工具都是基于这个玩意来实现的,比如大名鼎鼎的 Arthas。
它可以更加方便的做字节码增强操作,允许我们对已经加载甚至还没有被加载的类进行修改的操作,实现类似于性能监控的功能。
可以说 Instrumentation 就是 memoryLimiter 的关键点。比如在 memoryLimiter 的 acquireInterruptibly 方法里面,它是这样的用的:
看方法名称你也知道了,get 这个 object 的 size,这个 object 就是方法的入参,也就是要放入到队列里面的元素。
an implementation-specific approximation of the amount of storage consumed by the specified object
整句话翻译过来就是:返回指定对象所消耗的存储量的一个特定实现的近似值。
再说的直白点就是你传进来的这个对象,在内存里面到底占用了多长的长度,这个长度不是一个非常精确的值。
MemorySafeLinkedBlockingQueue的实现原理是什么?
MemorySafeLinkedBlockingQueue 还是继承自 LinkedBlockingQueue,只是多了一个自定义的成员变量,叫做 maxFreeMemory,初始值是 256 * 1024 * 1024。
这个变量的名字就非常值得注意,你再细细品品。maxFreeMemory,最大的剩余内存,默认是 256M。
前面一节讲的 MemoryLimitedLinkedBlockingQueue 限制的是这个队列最多能使用多少空间,是站在队列的角度。
而 MemorySafeLBQ 限制的是 JVM 里面的剩余空间。比如默认就是当整个 JVM 只剩下 256M 可用内存的时候,再往队列里面加元素我就不让你加了。
因为整个内存都比较吃紧了,队列就不能无限制的继续添加了,从这个角度来规避了 OOM 的风险。
另外,它说它不依赖 Instrumentation 了,那么它怎么检测内存的使用情况呢?
使用的是 ManagementFactory 里面的 MemoryMXBean。
这些信息就是从JConsole控制台 ManagementFactory 里面拿出来的:
所以,确实它没有使用 Instrumentation,但是它使用了 ManagementFactory。目的都是为了获取内存的运行状态。
MemorySafeLinkedBlockingQueue为什么比MemoryLimitedLinkedBlockingQueue更好用?
关键方法就是这个 hasRemainedMemory,在调用 put、offer 方法之前就要先调用这个方法:
![[Pasted image 20240730174318.png]]MemorySafeLinkedBlockingQueue 只是重写了放入元素的 put、offer 方法,并不关注移除元素。
因为它的设计理念是只关心添加元素时候的剩余空间大小,它甚至都不会去关注当前这个元素的大小。
所以我们看看 MemoryLimitCalculator 的源码。
![[Pasted image 20240730174504.png]]第一行是调用 refresh 方法,也就是对 maxAvilable 这个参数进行重新赋值,这个参数代表的意思是当前还可以使用的 JVM 内存。
第二行是注入了一个每 50ms 运行一次的定时任务。到点了,就触发一下 refresh 方法,保证 maxAvilable 参数的准实时性。
第三行是加入了 JVM 的 ShutdownHook,停服务的时候需要把这个定时任务给停了,达到优雅停机的目的。
思路扩展
场景演练:
项目里面用 Map 来做本地缓存,就会放很多元素进去,也会有 OOM 的风险!
解决方案
我们可以参考安全内存队列的思路解决避规OOM风险。