写点什么

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

2015 年 7 月 31 日

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

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

  • 对象以固定的速度不断地分配,垃圾收集时间逐步增加,内存使用率随之增大;
  • 对象分配存在爆发期,而每次爆发都会导致系统迟滞,并伴有明显的 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 介绍了如下三种分配触发方式:

  • 空池触发:任何时候,只要池空了,就分配对象。这是一种最简单的方式。
  • 水位线:空池触发的缺点是,某次对象请求会因为执行对象分配而中断。为了避免这种情况,可以使用水位线触发。当从池中请求新对象时,检查池中可用对象的数量。如果可用对象小于某个阈值,就触发分配过程。
  • Lease/Return 速度:大多数时候,水位线触发已经足够,但有时候可能会需要更高的精度。在这种情况下,可以使用leasereturn速度。例如,如果池中有 100 个对象,每秒有 20 个对象被取走,但只有 10 个对象返回,那么 9 秒后池就空了。开发者可以使用这种信息,提前做好对象分配计划。

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

  • 固定大小:这是最简单的对象池实现方式。对象一次性预分配,对象池后续不再增长。这种实现适用于对象数量相对确定的情况,但池大小固定可能会导致资源饥饿。
  • 小步增长:为了避免出现资源饥饿,可以允许对象池小步增长,比如一次额外分配一个对象。
  • 块增长:如果无法接受分配导致的中断,就需要保证池中任何时候都有可用的对象。这时,就必须使用块增长。例如,每当水位线到达 25% 时,就将对象池增大 25%。不过,这种方式容易导致内存溢出。搭配 Lease/Return 速度分配触发策略,可以得出更准确的池大小。

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

  • 引用泄露:对象在系统中某个地方注册了,但没有返回到池中。
  • 过早回收:消费者已经决定将对象返还给对象池,但仍然持有它的引用,并试图执行写或读操作,这时会出现这种情况。
  • 隐式回收:当使用引用计数时可能会出现这种情况。
  • 大小错误:这种情况在使用字节缓冲区和数组时非常常见:对象应该有不同的大小,而且是以定制的方式构造,但返回对象池后却作为通用对象重用。
  • 重复下单:这是引用泄露的一个变种,存在多路复用时特别容易发生:一个对象被分配到多个地方,但其中一个地方释放了该对象。
  • 就地修改:对象不可变是最好的,但如果不具备那样做的条件,就可能在读取对象内容时遇到内容被修改的问题。
  • 缩小对象池:当池中有大量的未使用对象时,要缩小对象池。
  • 对象重新初始化:确保每次从池中取得的对象不含有上次使用时留下的脏字段。

最后,Alex 指出:

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


感谢郭蕾对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015 年 7 月 31 日 09:389163
用户头像

发布了 1008 篇内容, 共 313.6 次阅读, 收获喜欢 282 次。

关注

评论

发布
暂无评论
发现更多内容

面试的季节到了,老哥确定不来复习下数据结构吗

Silently9527

面试 数据结构与算法

【STM32】PWM 输出 (标准库)

AXYZdong

硬件 stm32 2月春节不断更

3.Fiber(我是在内存中的dom)

全栈潇晨

React React Hooks react源码

门诊数字化:患者信息识别方式

boshi

医疗 数字化基础 七日更

用例文档

三生赤水

一维数组的动态和

小马哥

算法

哲少荐书:鞋狗

Jackey

书籍推荐

厉害了!这群95后正在用三维成像技术让科幻变成现实

华为云开发者社区

视频 华为云 三维 裸眼 光学

gradle中的增量构建

程序那些事

maven Gradle 程序那些事 构建工具

字幕组时代落幕,翻译的未来可能是?

字节跳动技术团队

LeetCode题解:1091. 二进制矩阵中的最短路径,BFS,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

Elasticsearch mapping 复杂数据类型

escray

elastic 七日更 死磕Elasticsearch 60天通过Elastic认证考试 2月春节不断更

up主周月纪念日前夜总结文

Kylin

视频创作 自媒体 up主 周月纪念日

EternalWallet为您提供快速、便捷、低价的国际汇款服务

Geek_c610c0

区块链挖矿系统APP开发|区块链挖矿软件开发(现成)

v16629866266

心理声学基础

行者AI

心理 音乐

什么是阻抗?

不脱发的程序猿

阻抗 电路设计 电子元器件

如何 1 天快速集成自己的“Clubhouse”?

融云 RongCloud

音视频 clubhouse 语音社交 融云

端口隔离和VLAN的区别

【LeetCode】重塑矩阵Java题解

HQ数字卡

算法 LeetCode 2月春节不断更

IDEA插件:快速删除Java代码中的注释

xiaoxi666

代码注释 Java 8 JavaParser

算法从有序数组中移除重复的数据,AI学习资源2020 John 易筋 ARTS 打卡 Week 38

John(易筋)

ARTS 打卡计划 ai youbute学习资源

第 4 周作业

老元宵

华为云FusionInsight MRS在金融行业存算分离的实践

华为云开发者社区

大数据 金融 华为云 存算分离 FusionInsight MRS

第四章作业-编写一个用例文档

秦挺

日记 2021年2月18日(周四)

Changing Lin

2月春节不断更

【STM32】EXTI---外部中断/事件控制器

AXYZdong

硬件 stm32 2月春节不断更

ElasticSearch.04 - 基础操作

insight

elasticsearch 2月春节不断更

话题讨论 | 如何使用“网站SEO”,让网站排在最前面?

魔王哪吒

前端 后端 话题讨论 SEO 2月春节不断更

阿里云大佬爆裂推荐“redis全新手册”,内容即精华

比伯

Java redis 程序员 架构 程序人生

【函数计算实践】nodejs初探示例——本地mac环境

程序员架构进阶

架构 nodejs 函数计算 七日更 2月春节不断更

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

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