[关闭]
@lsmn 2015-07-31T11:18:12.000000Z 字数 2467 阅读 3693

一个广为人知但鲜有人用的技巧:对象池

设计模式 对象池 内存管理 垃圾收集


摘要

对象池是一种设计模式,它会预先初始化一组可重用的实体,而不是按需销毁然后重建。在使用套接字描述符时,人们通常会将其池化。实际上,套接字描述符的数量通常比较少(最多上千个),之所以要采用池的方式,是因为它们的初始化成本非常高。而在最近发表的一篇博文中,ClojureWerkz核心成员Alex Petrov探讨了另一种对象池应用场景,即将大量的存活期短且初始化成本低的对象池化,以降低内存分配和再分配成本,避免内存碎片。

正文

对象池是一种设计模式,它会预先初始化一组可重用的实体,而不是按需销毁然后重建。在使用套接字描述符时,人们通常会将其池化。实际上,套接字描述符的数量通常比较少(最多上千个),之所以要采用池的方式,是因为它们的初始化成本非常高。而在最近发表的一篇博文中,ClojureWerkz核心成员Alex Petrov探讨了另一种对象池应用场景,即将大量的存活期短且初始化成本低的对象池化,以降低内存分配和再分配成本,避免内存碎片。

Alex将对象池看作是减少GC压力的首选方法,同时也是最简单的方法。在下面两种分配模式下,可以选择使用对象池:

在绝大多数情况下,这些对象要么是数据容器,要么是数据封装器,其作用是在应用程序和内部消息总线、通信层或某些API之间充当一个信封。这很常见。例如,数据库驱动会针对每个请求和响应创建Request和Response对象,消息系统会使用Message和Event封装器,等等。对象池可以帮助保存和重用这些构造好的对象实例。

Alex介绍了两种基本的对象池回收模式:“借用(borrowing)”和引用计数。前者更清晰,而后者则意味着要实现自动回收。

借用非常像垃圾收集运行时之上的malloc/free。自然地,在使用这种方式时,开发人员需要面对早先使用非垃圾收集语言时面对的问题。如果某个对象已经释放并返回到池中,那么任何对它的修改或读取都会产生不可预见的结果。例如,在C语言中,对已释放的指针进行任何操作都会产生块错误。借用适用于有明确的开始/结束点的操作。绝大多数时候,都不要将它用于对象可以被多个线程同步访问的情况。借用最大的优点是,它不知道对象池的存在。被借用的对象本身要有某种reset机制,借用和返回操作都由对象消费者完成。

引用计数在实现方面稍微复杂些,但它对数据结构提供了更细粒度的控制。将对象池封装到一个函数式接口中,消费者就可以不必了解它,就像下面这个样子:

(pooledObject, pooledObjectConsumer) -> {
  pooledObject.retain();
  pooledObjectConsumer.accept(pooledObject);
  pooledObject.release();
  };

每当对象进入上述代码块,调用者就会retain该对象,并在执行块执行完毕后将其release。每个对象都持有一个内部计数器和一个指向池的引用。当计数器为0时,对象就会返回池中。

通常,引用计数用于同时有多个消费者访问已分配对象的情况,只有当所有的消费者都释放了对象引用时,对象才可以被回收。这种方式也适用于管道或嵌套处理。在这种情况下,开发者可以避免显式的开始/结束操作。

分配触发负责在池中对象不足时分配新资源。Alex介绍了如下三种分配触发方式:

增长策略用于指定分配过程被触发后需要分配的对象的数量。Alex也介绍了三种方式:

当然,使用对象池就意味着开发者开始自己管理内存,所以需要注意以下问题:

最后,Alex指出:

对象池并不适合所有人。在应用程序开发的早期阶段就开始使用对象池是没有意义的,因为你那时候还不能确切地知道什么需要池化,也不确定如何池化。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注