对象池
用
ObjectPool<T>复用高频创建/销毁的短生命周期对象,减少 GC 压力,适用于StringBuilder、缓冲区、临时容器等场景。
适用场景
- Chat 应用里每次请求都要新建
StringBuilder拼装提示词,用后即弃。 - 高频创建和销毁同一类型的临时对象(缓冲区、序列化中间容器、请求上下文包装器)。
- 对象创建成本不算极高,但创建频率极高,导致 GC 触发过于频繁。
- 对象的状态可以在归还前重置到干净状态,下次取出即可直接使用。
如果你的场景是"创建一次、长期持有",对象池不是正确答案——这种情况应该直接用依赖注入管理生命周期。
相关依赖
| 包名 | 作用 | 什么时候需要 |
|---|---|---|
soulsoft_extensions_objectpool | 提供 ObjectPool<T>、ObjectPoolFactory、DefaultObjectPoolProvider、IPooledObjectPolicy<T> 等核心类型 | 始终需要 |
快速开始
先引入 soulsoft_extensions_objectpool,按"创建池 → 借出 → 使用 → 归还"这条最小路径走一遍。
import soulsoft_extensions_objectpool.*
class ChatPromptBuilder {
public var segments: Int64 = 0
public func append(text: String): Unit {
segments += 1
}
}
// 一行创建对象池,工厂函数定义"怎么创建新对象"
let pool = ObjectPoolFactory.create { => ChatPromptBuilder() }
// 借出 → 使用 → 归还
let builder = pool.get()
builder.append("system: you are a helpful assistant")
builder.append("user: hello")
// 使用完毕,归还前重置状态
builder.segments = 0
pool.put(builder)ObjectPoolFactory.create() 是最简入口:传入一个工厂 lambda,返回一个可用的 ObjectPool<T>。
| 角色 | 职责 | 常用入口 |
|---|---|---|
ObjectPoolFactory | 静态工厂,一行创建对象池 | create { => T() } |
ObjectPool<T> | 对象池抽象,提供 get() / put() | pool.get() / pool.put(obj) |
DefaultObjectPoolProvider | 可配置最大保留数的创建器 | provider.maximumRetained = 32 |
核心概念
soulsoft_extensions_objectpool 提供两层能力:
ObjectPool<T>:统一get()/put()两个操作,封装对象的借出和归还。IPooledObjectPolicy<T>:策略接口,控制对象的创建方式和归还条件。
这层抽象的价值在于把"怎么创建""能不能复用""归还时做什么"这三件事收敛到策略里,调用方只需要 get() 和 put()。
业务代码 ──get / put──▶ ObjectPool<T> ──create / release──▶ IPooledObjectPolicy<T>
│ │
│ ├──▶ DefaultPooledObjectPolicy(默认,识别 IResettable)
│ │
│ └──▶ 自定义策略
│
└──▶ DefaultObjectPool<T>(默认实现,快速槽位 + 并发队列)这张图只表达一条主线:业务代码依赖 ObjectPool<T> 的 get / put,策略决定对象如何创建和是否保留。默认实现基于工厂函数 + IResettable 自动识别,底层用快速槽位 CAS + 并发队列保证线程安全。
信息
soulsoft_extensions_objectpool 是零外部依赖的独立包,不依赖 DI 容器。你可以直接 new 出来用,也可以手动注册进容器。
借出与归还
对象池的核心语义只有两条:
get():优先从池中取出已有对象,池空时才创建新对象。put(obj):将对象归还给池,由策略决定保留还是丢弃。
归还后的对象不应再被调用方使用——它的生命周期已经交还给池。
策略决定复用
IPooledObjectPolicy<T> 定义了两个方法:
| 方法 | 作用 | 调用时机 |
|---|---|---|
create() | 创建新对象实例 | 池中无可用对象时被 get() 调用 |
release(obj) | 重置对象并决定是否保留 | put() 归还时调用,返回 true 保留、false 丢弃 |
策略把"能不能复用"的判断逻辑从业务代码里抽出来,让调用方不需要关心对象归还后的具体处理。
IResettable:让对象自己知道怎么重置
如果对象实现了 IResettable,DefaultPooledObjectPolicy<T> 会在归还时自动调用 tryReset()。
import soulsoft_extensions_objectpool.*
class ChatTransportOptions <: IResettable {
public var endpoint: String = ""
public var timeout: Int64 = 0
public func tryReset(): Bool {
endpoint = ""
timeout = 0
return true // 返回 true 表示重置成功,可以回池
}
}这样设计的原因是:重置逻辑和对象本身绑定,策略不需要知道每种类型的具体重置方式。tryReset() 返回 false 时,该对象会被丢弃而不是放回池中。
最大保留数
DefaultObjectPoolProvider.maximumRetained 控制池中最多保留多少个空闲对象。默认值是 getProcessorCount() * 2,即 CPU 核心数的两倍。
当归还的对象超过这个上限时,多余的对象会被直接丢弃,不会无限堆积。
可释放对象的自动清理
如果池化对象实现了 Resource 接口,DefaultObjectPoolProvider 会自动创建 DisposableObjectPool<T>:
- 池关闭时,所有保留对象会被依次
close()。 - 归还被拒绝的对象也会被
close(),防止资源泄漏。
这意味着数据库连接、文件句柄等实现了 Resource 的对象,在池化时可以安全释放。
常见用法
直接用工厂函数创建
这是最简路径,适合大多数场景。先看 ObjectPoolFactory.create() 一行完成创建。
import soulsoft_extensions_objectpool.*
class ChatSession {
public var messageCount: Int64 = 0
}
let pool = ObjectPoolFactory.create { => ChatSession() }
let session = pool.get()
session.messageCount = 3
session.messageCount = 0 // 归还前手动重置
pool.put(session)这段代码只做了三件事:创建池、借出对象、归还对象。工厂函数 { => ChatSession() } 只在池空时被调用。
实现 IResettable 自动重置
如果不想在每次 put() 前手动重置,让对象实现 IResettable。
import soulsoft_extensions_objectpool.*
class ChatPromptBuilder <: IResettable {
public var length: Int64 = 0
public var content: String = ""
public func tryReset(): Bool {
length = 0
content = ""
return true
}
}
let pool = ObjectPoolFactory.create { => ChatPromptBuilder() }
let builder = pool.get()
builder.content = "system prompt"
builder.length = builder.content.runes().count()
pool.put(builder) // tryReset() 自动被调用,无需手动重置关键区别:put() 时策略自动调用 tryReset(),调用方不需要在归还前手动清空字段。tryReset() 返回 true 表示对象已重置干净,可以回池;返回 false 表示该对象不应复用。
自定义策略控制创建和归还
当工厂函数不够用——比如创建时需要初始配置、归还时需要校验对象状态——用自定义策略。
import soulsoft_extensions_objectpool.*
class ChatTransportOptions {
public var endpoint: String = ""
public var retryCount: Int64 = 0
}
class ChatTransportPolicy <: IPooledObjectPolicy<ChatTransportOptions> {
public func create(): ChatTransportOptions {
let opts = ChatTransportOptions()
opts.endpoint = "https://api.default"
return opts
}
public func release(obj: ChatTransportOptions): Bool {
// 归还前校验:重试次数过多的对象不再复用
if (obj.retryCount > 3) {
return false // 丢弃
}
obj.endpoint = ""
obj.retryCount = 0
return true // 保留
}
}
let pool = ObjectPoolFactory.create(ChatTransportPolicy()) { => ChatTransportOptions() }自定义策略的核心价值在于 release() 可以加归还条件——不是所有对象都值得复用。上面这种写法适合"对象在使用过程中可能进入不可复用状态"的场景。
使用 DefaultObjectPoolProvider 控制最大保留数
需要调整池容量时,用 DefaultObjectPoolProvider 替代 ObjectPoolFactory。
import soulsoft_extensions_objectpool.*
let provider = DefaultObjectPoolProvider()
provider.maximumRetained = 16 // 覆盖默认值(CPU 核心数 × 2)
class ChatSession {
public var id: Int64 = 0
}
let pool = provider.create { => ChatSession() }区别在于:ObjectPoolFactory 是静态工厂,一行搞定;DefaultObjectPoolProvider 可以调整 maximumRetained,适合需要控制内存占用的场景。
复用 StringBuilder
字符串拼接是对象池最典型的应用场景。createStringBuilderPool() 提供了开箱即用的 StringBuilder 池。
import soulsoft_extensions_objectpool.*
let provider = DefaultObjectPoolProvider()
let sbPool = provider.createStringBuilderPool(
initialCapacity: 128, // 归还时容量回缩到这个值
maximumRetainedCapacity: 4096 // 容量超过此值的对象不保留
)
let sb = sbPool.get()
sb.append("Chat request from ")
sb.append("user: alice")
let prompt = sb.toString()
sbPool.put(sb)警告
归还 StringBuilder 时,策略会调用 reset(capacity: initialCapacity)——这会清空内容,同时把容量回缩到初始容量,而不是保留归还前的容量。如果后续拼接长度接近上次,可能会触发一次扩容。
与依赖注入集成
ObjectPool 本身不依赖 DI,但可以手动注册进容器。先看 ChatService 如何通过构造函数接收池,再看注册代码。
import soulsoft_extensions_objectpool.*
import soulsoft_extensions_injection.*
class ChatService {
private let sbPool: ObjectPool<StringBuilder>
public init(sbPool: ObjectPool<StringBuilder>) {
this.sbPool = sbPool
}
public func buildPrompt(userMessage: String): String {
let sb = sbPool.get()
sb.append("system: you are a helpful assistant\n")
sb.append("user: ${userMessage}")
let prompt = sb.toString()
sbPool.put(sb)
return prompt
}
}
// 注册阶段:手动创建池并注册为 Singleton
let provider = DefaultObjectPoolProvider()
let sbPool = provider.createStringBuilderPool(
initialCapacity: 256,
maximumRetainedCapacity: 4096
)
let services = ServiceCollection()
services.addSingleton(sbPool) // 注册已有实例
services.addTransient<ChatService, ChatService>()addSingleton(sbPool) 把池注册为单例——整个应用共享同一个池。ChatService 通过构造函数接收 ObjectPool<StringBuilder>,不关心池是怎么创建的。
最佳实践
- 优先用
ObjectPoolFactory.create { => T() }起步。只有需要控制池容量或将"能否复用"的判断集中管理时,再引入自定义策略或DefaultObjectPoolProvider。这样做是因为最简单的路径已经覆盖了 80% 的场景,过早引入策略和 Provider 只会增加不必要的概念负担。 - 让对象实现
IResettable,把重置逻辑从调用方移到对象内部。目的是让put()变成一行,调用方不需要知道"归还前要清哪些字段"。 tryReset()中做完整性校验,不只是在字段上填零。如果对象进入了不可恢复的状态,返回false让池丢弃它,这比把脏对象放回池里安全得多。- 池化对象不要持有外部资源的引用(文件句柄、网络连接、数据库会话)。如果必须持有,让对象实现
Resource接口,确保被丢弃时能正确释放。 - 不要把池注册为 Transient 或 Scoped——池的目的就是跨请求复用对象,短生命周期会让池形同虚设。
推荐组织方式:IResettable 对象 + ObjectPoolFactory.create(),调用方只做 get() → 使用 → put()。
import soulsoft_extensions_objectpool.*
class ChatPromptSegment <: IResettable {
public var role: String = ""
public var content: String = ""
public func tryReset(): Bool {
role = ""
content = ""
return true
}
}
let pool = ObjectPoolFactory.create { => ChatPromptSegment() }
let seg = pool.get()
seg.role = "system"
seg.content = "you are a helpful assistant"
// 使用 seg...
pool.put(seg) // tryReset() 自动清空字段限制与边界
- 当前包是零依赖的独立包,不提供
services.addObjectPool()之类的 DI 扩展方法。如需注册进容器,需手动addSingleton(poolInstance)。 maximumRetained默认值是处理器核心数 × 2,在高并发场景下可能需要调大;但这个值只控制池中保留的空闲对象数上限,不影响并发get()时创建新对象。DisposableObjectPool<T>关闭后,get()会抛出IllegalStateException,put()不再保留对象。关闭是幂等的。- 池不限制借出对象的总数——如果所有保留对象都被借出且持续不归还,每次
get()都会创建新对象。对象池解决的是"复用"问题,不是"限流"问题。
警告
对象池不适合管理有外部副作用的对象。如果一个对象的 close() 会断开数据库连接或释放文件锁,务必让对象实现 Resource 并理解 DisposableObjectPool 的关闭语义。
常见问题
get() 每次都返回新对象,池没生效?
最常见的原因是 put() 没有正确调用,或者 release() / tryReset() 返回了 false。检查两个地方:是否每次 get() 后都有对应的 put();策略的 release() 是否在不该丢弃对象时返回了 false。
池里应该保留多少对象?
默认值 处理器核心数 × 2 对大多数场景是合理的。如果池的命中率明显偏低(get() 频繁创建新对象),说明保留数不够或归还不及时;先排查归还路径,再调大 maximumRetained。
对象池和 DI 的作用域有什么区别?
DI 管理的是"谁持有对象、持有多久"(Singleton / Scoped / Transient),对象池管理的是"对象用完了别扔,下次接着用"。两者解决的问题不同:DI 解决依赖组装,对象池解决内存分配。池本身通常注册为 Singleton,但池里的对象不属于任何 DI 作用域。
StringBuilder 池的容量参数怎么选?
initialCapacity 设为你预期的单次拼接长度,maximumRetainedCapacity 设为你愿意保留的最大容量。超过 maximumRetainedCapacity 的 StringBuilder 在归还时会被丢弃——这是为了防止一个偶然的超长拼接永久占着一大块内存。