在Java中有很多池管理的概念,典型的如线程池,数据库连接池,查看源码会发现这些池管理类都是继承于GenericObjectPool<T>,所以了解GenericObjectPool的工作机制。

1.为何需要它

采用对象池化的本意,是要通过减少对象生成的次数,减少花在对象初始化上面的开销,从而提高整体的性能。然而池化处理本身也要付出代价,因此,并非任何情况下都适合采用对象池化。最后,想想享元模式吧

对于类似Point这样的轻量级对象,进行池化处理后,性能反而下降,因此不宜池化;

对于类似Hashtable这样的中量级对象,进行池化处理后,性能基本不变,一般不必池化(池化会使代码变复杂,增大维护的难度);

对于类似JPanel这样的重量级对象,进行池化处理后,性能有所上升,可以考虑池化。

2.源码透析

GenericObjectPool对象池异常排查_池化

  • StackObjectPool,采用后进先出行为模式的池,采用的是java.util.Stack对象来保存对象池里的对象。
  • SoftReferenceObjectPool,采用后进先出行为模式的池。池内对象被SoftReference包裹,允许垃圾回收器在有内存需要的时候删除这部分对象。
  • GenericObjectPool,提供了后进先出(LIFO)与先进先出(FIFO)两种行为模式的池。默认情况是采用后进先出,即当有空闲对象时,调用borrowObject方法,返回最近时刻放进去的那个实例对象。这个行为是通过lifo这个属性控制的。它是三个实现中最复杂的,每次调用borrowObject或returnObject,度会涉及空闲对象分配,多线程等问题。分析源码时,推荐先从StackObjectPool看起,最后再分析GenericObjectPool。
  • KeyedObjectPool,包含多种类型的池,采用K-V方式,每个类型都可以通过特定的Key获取。

GenericKeyedObjectPool,采用先进先出行为模式的池。

StackKeyedObjectPool,采用后进先出行为模式的池。

3.基本配置及用法

public static class Config {  
            // 允许最大空闲对象数  
            public int maxIdle = GenericObjectPool.DEFAULT_MAX_IDLE;  
            // 允许最小空闲对象数  
            public int minIdle = GenericObjectPool.DEFAULT_MIN_IDLE;  
            // 允许最大活动对象数  
            public int maxActive = GenericObjectPool.DEFAULT_MAX_ACTIVE;  
            // 允许最大等待时间毫秒数  
            public long maxWait = GenericObjectPool.DEFAULT_MAX_WAIT;  
            // 当池中对象用完时,请求新的对象所要执行的动作  
            public byte whenExhaustedAction = GenericObjectPool.DEFAULT_WHEN_EXHAUSTED_ACTION;  
            // 是否在从池中取出对象前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个  
            public boolean testOnBorrow = GenericObjectPool.DEFAULT_TEST_ON_BORROW;  
            // 是否在向池中归还对象前进行检验,如果检验失败  
            public boolean testOnReturn = GenericObjectPool.DEFAULT_TEST_ON_RETURN;  
            // 连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除  
            public boolean testWhileIdle = GenericObjectPool.DEFAULT_TEST_WHILE_IDLE;  
            // 在空闲连接回收器线程运行期间休眠的时间毫秒数. 如果设置为非正数,则不运行空闲连接回收器线程  
            public long timeBetweenEvictionRunsMillis = GenericObjectPool.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;  
            // 设定在进行后台对象清理时,每次检查对象数  
            public int numTestsPerEvictionRun =  GenericObjectPool.DEFAULT_NUM_TESTS_PER_EVICTION_RUN;  
            // 被空闲对象回收器回收前在池中保持空闲状态的最小时间毫秒数  
            public long minEvictableIdleTimeMillis = GenericObjectPool.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;  
            // 被空闲对象回收器回收前在池中保持空闲状态的最小时间毫秒数  
            public long softMinEvictableIdleTimeMillis = GenericObjectPool.DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;  
            // 是否采用后进先出策略  
            public boolean lifo = GenericObjectPool.DEFAULT_LIFO;  
        }

实际用法参考JedisFactory、ShardedJedisPool这里不在赘述。

4.问题描述

演示一段代码但它发生异常。

GenericObjectPool对象池异常排查_行为模式_02

GenericObjectPool对象池异常排查_GenericObjectPool_03

GenericObjectPool对象池异常排查_后进先出_04

用100并发打压服务,发现拿到连接并在干活的线程数只有10+, 而其余80+的线程wait在borrowObject的逻辑, 相应的stack如下:

分析:

为什么maxActive设置为100, 还会有如此多的线程在makeObject?这是我们的setMaxActive没有生效, 还是GenericObjectPool有其他destroy对象的机制被我们忽略了?
打压开始后, 动态debug, 发现pool对象有许多我们未进行配置的成员变量,对照api逐一check各变量,发现一些可怀疑点:
maxIdle

        其值一直在2-8之间波动, 且API中如是描述:可知默认的maxIdle值为8,若未进行配置,当pool中idle的object超过默认值8时,多余的对象就会被destroy。

maxWait

     可知若不进行设置,当被依赖的服务由于某些故障(如机器资源被某进程大量占用)而响应极慢时,会有大量线程blocked在borrowObject的逻辑,最终导致线程耗尽,服务卡死,用户请求被拒绝。