最近在分析一个 dump 的过程中发现其在 gen2 和 LOH 上有不少size较大的free,仔细看了下,这些free生前大多都是模板引擎生成的html片段的byte[]数组,当然这篇我不是来分析dump的,而是来聊一下,当托管堆有很多length较大的 byte[] 数组时,如何让内存利用更高效,如何让gc老先生压力更小。
不知道大家有没有发现在 .netcore 中增加了不少池化对象的东西,比如: ArrayPool,ObjectPool 等等,确实在某些场景下还是特别实用的,所以有必要对其进行较深入的理解。
在我花了将近一个小时的源码阅读之后,我画了一张 ArrayPool 的池化图,所谓:<code>一图在手,天下我有</code> 。
有了这张图,接下来再聊几个概念并配上相应源码,我觉得应该就差不多了。
ArrayPool 是由若干个 Bucket 组成, 而 Bucket 又由若干个 <code>buffer[]</code> 数组组成, 有了这个概念之后,再配一下代码。
这个问题很好回答,初始化时做了 <code>maxArraysPerBucket=50</code> 设定,当然你也可以自定义,具体参考如下代码:
框架做了默认假定,第一个bucket中的 <code>buffer[].length=16</code>, 后续 bucket 中的 <code>buffer[].length</code> 都是 x2 累计,涉及到代码就是 <code>GetMaxSizeForBucket()</code> 方法,参考如下:
其实在上图中我也没有给出 bucket 到底有多少个,那到底是多少个呢?😓😓😓 ,当我阅读完源码之后,这算法还挺有意思的。
先说一下结果吧,默认 17 个 bucket,你肯定会好奇怎么算的? 先说下两个变量:
maxArrayLength=1048576 = 2的20次方
buffer.length= 16 = 2的4次方
最后的算法就是取次方的差值:<code>bucket[].length= 20 - 4 + 1 = 17</code>,换句话说最后一个 bucket 下的 <code>buffer[].length=1048576</code>,详细代码请参考 <code>SelectBucketIndex()</code> 方法。
到这里我相信你对 ArrayPool 的池化架构思路已经搞明白了,接下来看下如何申请和归还 buffer[]。
既然 buffer[] 做了颗粒化,那就应该好借好还,反应到代码上就是 <code>Rent()</code> 和 <code>Return()</code> 方法,为了方便理解,上代码说话:
有了代码和图之后,再稍微捋一下流程。
从 ArrayPool 中借一个 <code>byte[10]</code> 大小的数组,为了节省内存,先不备货,临时生成一个 <code>byte[].size=16</code> 的数组出来,简化后的代码如下,参考 <code>if (flag)</code> 处:
这里有一个坑,那就是你以为借了 <code>byte[10]</code>,现实给你的是 <code>byte[16]</code>,这里稍微注意一下。
当用 ArrayPool.Return 归还 <code>byte[16]</code> 时, 很明显看到它落到了第一个bucket的第一个buffer[]上,参考如下简化后的代码:
这里也有一个值得注意的坑,那就是还回去的 <code>byte[16]</code> 里面的数据默认是不会清掉的,从上面的代码也是可以看出来的,要想做清理,需要在 Return 方法中指定 <code>clearArray=true</code>,参考如下代码:
学习这其中的 <code>池化架构</code> 思想,对平时项目开发还是能提供一些灵感的,其次对那些一次性使用 <code>byte[]</code> 的场景,用池化是个非常不错的方法,这也是我对朋友dump分析后提出的一个优化思路。
更多高质量干货:参见我的 GitHub: dotnetfly