三颗纽扣

世界上最宽广的是海洋,比海洋更宽广的是天空,比天空更宽广的是人的胸怀

导航

控制内存的使用之二:对象缓存 pool and cache

降低程序的内存使用并非是内存控制的最终目的,任何一个程序,特别是服务类程序,最终都会考虑最大程度的利用系统所能提供的资源,在允许的范围内申请更多的CPU、IO带宽以及内存等等,以提供更多并发、更高吞吐量。因此,当我们通过一些手段将必须的内存需求降下来以后,反过头来,需要考虑如何将节省下来的这些内存资源更有效的利用起来,而不是空闲着。一种最简单的就是大量的使用缓存。

 

缓存的目的,一般就是避免频繁的创建以及销毁对象,以及在这个过程中所需要进行的相关计算处理过程。 pool 和 cache 两种技术都有可能成为我们的选择。当然它们在技术本质上是不一样的,他们适合于不同的应用场景。

 

pool 的概念是对象的“借出”和“归还”,也就是说从池中提取出一个对象以后,它就已经不在池中了,其他线程或者随后的提取操作无法提取到同一个对象。直到对象被归还到池中之后,它才能被随后的提前操作取出,对于使用者而言,一旦将对象归还到池中,就不应该再使用它,而是应该立刻将对它的引用置为null。对于一些会被频繁的临时使用的对象,这些对象在执行过程中频繁的被使用,但是每次使用的范围一般局限于一个函数中的前后几行,随后就抛弃不用,例如 StringBuilder, Buffer, Connection 等等,可能需要考虑使用 pool 来处理。

 

cache 则相反,只要提供相同的 key,一般情况下每次都能提取到同样的对象,并且取到即用,也不需要将它归还。cache 比较常见的用法是用来保存处理后的结果,以便以后在需要进行相同计算的时候,可以完全跳过计算过程,直接获取结果。

 

简单的说,pool所管理的对象应该是不能被多个线程或者代码段共享使用的,而 cache 则正好相反,提供给多个线程或者代码段共享的实例。因此,具体使用 pool 还是 cache,应视乎对缓存内容的使用方式而定。 但是从根本上来说,如果有那么一些对象,现在用不着了,但可以预见将来可能还会被用到,那么就可能需要考虑扔到 pool 或者 cache 中去。

 

需要注意的一点就是不论 pool 还是 cache,利用现成的框架时,在多线程环境下都需要仔细考察框架的线程安全特性,应该说,大部分的 pool 或者 cache 框架都不是线程安全的,很多时候需要应用本身来处理锁或者同步的问题。从框架本身的角度上来说,不在内核级别提供线程安全保障是明智的,这可避免锁机制带来的额外开销,更高效的提供服务。是否需要锁或者同步机制,只有在应用环境中加以考虑。例如对于 pool 而言, 借出重复的对象显然是无法容忍的,但是对归还时是否允许丢失对象,则视池中之物而定,丢失一个 StringBuilder 没什么关系,但丢失一个 Connection 则可能比较严重了。对于 cache 也一样,大部分时候如果 cache 没有命中的话我们将创建一个新的丢到 cache 中去,没有同步时候这可能导致为同一个 key 创建了两个实例,其中一个将被丢弃,但是如果要创建的内容需要消耗较大的资源,或者逻辑上不允许重复创建,那么显然就需要考虑通过同步锁来避免重复创建的问题了。 

 

有一个很有意思的问题,本质上来说 pool 和 cache 中保留的都是预计将来要用的东西,他们对代码逻辑应该不会产生任何影响,因此原则上缓存的内容都是可以被过期回收的,因此,应该有一个对 pool 和 cache 统一进行过期管理回收的机制,然而很遗憾的是目前我还没有发现这样的东东,更糟糕的是,即使是同一个 pool manager 或者 cache manager,也没有统一管理自己旗下多个 pool 或者 cache 的方法,这使得我们不得不通过对 pool 和 cache 的不断检测观察,来确定缓存的大小以及过期策略,仔细的调整各种参数,以便使它们有更高的命中率,即使这样这个静态的参数也往往不能和很好的适合程序动态运行环境的需要。很期待有这么一个产品的出现。

 

== StringBuilder ==

StringBuilder 本身是一个让我觉得很鸡肋的东西,因为我觉得它所适用的大部分场合,例如输出时的拼接字符串,都应该是编译系统以及虚拟机需要考虑的问题,可惜编译系统以及虚拟机不知道为什么,死活就不提供这种支持,因此,既然我们这么大量的使用到它,池化一下也许是有必要的。

 

建立一个 StringBuilder 池本身是一个非常简单的事情,由于这个对象本身是这么的不重要,因此,一个简单的ArrayList 都足以用来管理 StringBuilder 池,即使在并发的情况下,丢失那么一两个对象,也不是什么大不了的事情。

 

然而 StringBuilder 的设计本身并没有考虑池化的问题,因此,如果它在使用的过程中被扩展了,那么实际上还是另外分配了一块新的空间,原有的空间被简单的丢弃了。一般情况下这也没什么,但是如果偶尔会有那么几次它们被扩展到非常的大,此后由于某种原因那些小的又都被过期丢弃了,最后将在池中保留了一些预分配了大量空间的 StringBuilder,而实际上通常基本上用不了那么大,这将造成大量的空间浪费。或者,和我一样,有着极度节约癖好,对于哪怕是几个 KB 的内存,也斤斤计较希望能够重复利用,这时候或许需要考虑实现自己的 StringBuilder 了。其实它只需要简单的允许将自己的内部 byte[] Buffer 切换为从 Buffer 缓存中获取一个,并将旧的归还给 Buffer Pool,就可以工作得很好了。

 

== Buffer ==

byte[] 是在代码中经常会用到的对象,特别有较多 IO 操作的情况下,几乎无处不在,每次分配动则就是几十K甚至几M,用完就丢弃实在是比较可惜,将这些东西池化以后,不但 IO 的时候可用得到,一些需要 byte[] 的地方,例如 MyStringBuilder 等,都可以从中获益。

 

== BufferListBuffer ==

这个拗口的名称的意思是说,将几个Buffer串联起来,使之看起来象是一个更大的Buffer,而不是重新分配一个。我们的 Buffer 缓存中保留了那么多预分配的空间,但是并没有一个简单的策略可以保证每次我需要的时候都能一次性的提供一个足够大小的给我,因此,在使用的过程中,还会出现 Buffer 大小不够需要扩展的情况,重新分配一个更大的 Buffer 代价往往是不菲的,需要将原有的数据拷贝过去,而且这个巨大的Buffer可能以后很少会碰到需要用满它的情况,它的大部分空间被浪费了。BufferListBuffer 将一些小个的Buffer组织成大的空间,避免了重新分配以及拷贝过程,更主要的是,合适尺寸的 Buffer 有更高的利用率。

posted on 2010-03-21 14:59  三颗纽扣  阅读(427)  评论(0编辑  收藏  举报