概念

所谓的对象池技术,就是说一个 Java 对象用完之后把它保存起来,之后再拿出来重复使用,省去了对象创建、初始化和 GC 的过程,减少 CPU 和内存的开销。对象池技术是典型的以空间换时间的思路。

使用场景

条件1: Java 对象数量很多并且存在的时间比较短,
条件2: 对象本身又比较大比较复杂,对象初始化的成本比较高。

这样的场景就适合用对象池技术。比如 Tomcat 和 Jetty 处理 HTTP 请求的场景就符合这个特征:
1 请求的数量很多,为了处理单个请求需要创建不少的复杂对象(比如 Tomcat 连接器中 SocketWrapper 和 SocketProcessor)。
2 一般来说请求处理的时间比较短,一旦请求处理完毕,对象就需要被销毁。

对象池的实现

Tomcat 的 SynchronizedStack

目的:用最低的内存和 GC 的代价来实现无界容器。

实现上,内部维护一个对象数组,用数组来模拟栈;用 synchronized 进行线程同步。

使用上,提供了push、pop接口;本身只支持扩容不支持缩容。

Jetty 的 ByteBufferPool

本质是一个 ByteBuffer 对象池。

当 Jetty 在进行网络数据读写时,不需要每次都在 JVM 堆上分配一块新的 Buffer,只需在 ByteBuffer 对象池里拿到一块预先分配好的 Buffer,这样就避免了频繁的分配内存和释放内存。这种设计同样可以在高性能通信中间件比如 Mina 和 Netty 中看到。

实现上

ByteBufferPool 是一个接口,提供 acquire 和 release 分别用来分配和释放内存;ArrayByteBufferPool 是实现类####没有抽象基类。

ArrayByteBufferPool 是用不同的桶(Bucket)来管理不同长度的 ByteBuffer,因为我们可能需要分配一块 1024 字节的 Buffer,也可能需要一块 64K 字节的 Buffer。而桶的内部用一个 ConcurrentLinkedDeque 来放置 ByteBuffer 对象的引用。

Buffer 的分配和释放过程,就是找到相应的桶,并对桶中的 Deque 做出队和入队的操作,而不是直接向 JVM 堆申请和释放内存。

image.png

使用上,acquire 方法的 direct 参数指定 buffer 是从 JVM 堆上分配还是从本地内存分配。

对象池设计的核心要点

并发控制/线程同步

对象池作为全局资源,高并发环境中多个线程可能同时需要获取对象池中的对象,因此多个线程在争抢对象时会因为锁竞争而阻塞。但不使用对象池则有创建和销毁对象的开销。

对于对象池本身的设计来说,需要尽量做到无锁化,比如 Jetty 就使用了 ConcurrentLinkedDeque。如果你的内存足够大,可以考虑用线程本地(ThreadLocal)对象池,这样每个线程都有自己的对象池,线程之间互不干扰。

对象池的大小限制

对象池太小发挥不了作用,对象池太大的话可能有空闲对象,这些空闲对象会一直占用内存,造成内存浪费。需要根据实际情况做一个平衡,因此对象池本身除了应该有自动扩容的功能,还需要考虑自动缩容。

内存泄漏

所有的池化技术,包括缓存,都会面临内存泄露的问题。

对象池或者缓存的本质是一个 Java 集合类,比如 List 和 Stack,这个集合类持有缓存对象的引用,只要集合类不被 GC,缓存对象也不会被 GC。使用了对象之后,如果没有归还,就会发生内存泄漏。

使用维持大量的对象比较占用内存空间,必要时需要主动清理这些对象。

以 Java 的线程池 ThreadPoolExecutor 为例,它提供了
allowCoreThreadTimeOut 和 setKeepAliveTime 两种方法,在超时后销毁线程,可以参考这个策略。

对象池使用规范

申请时

向对象池请求对象时有可能出现的阻塞、异常或者返回 null 值,这些都需要我们做一些额外的处理,来确保程序的正常运行。

使用前

从对象池中取对象时,需要重置。不重置可能会导致内存泄漏,程序在运行过程中会发生意想不到的问题。

使用完

对象在用完后,需要调用对象池的方法归还对象。

归还后

对象一旦归还给对象池,使用者就不能对它做任何操作了。

知识总结

数组模拟栈
本地内存与堆内存
ConcurrentLinkedDeque 无锁
ThreadLocal
内存泄漏

https://webtide.com/object-pooling-benchmarks-and-another-way/