Java对象池技术的原理及其实现

Java对象的生命周期分析

Java对象的生命周期大致包括三个阶段:

  • 对象的创建
  • 对象的使用
  • 对象的清

因此,对象的生命周期长度可用如下的表达式表示:T = T1 + T2 +T3
其中T1表示对象的创建时间,T2表示对象的使用时间,而T3则表示其清除时间
由此,我们可以看出,只有T2是真正有效的时间,而T1、T3则是对象本身的开销

下面再看看T1、T3在对象的整个生命周期中所占的比例。

我们知道,Java对象是通过构造函数来创建的,在这一过程中,该构造函数链中的所有构造函数也都会被自动调用。另外,默认情况下,调用类的构造函数时,Java会把变量初始化成确定的值:所有的对象被设置成null,整数变量(byte、short、int、long)设置成0,float和double变量设置成0.0,逻辑值设置成false。所以用new关键字来新建一个对象的时间开销是很大的,如表1所示。

表1 一些操作所耗费时间的对照表


从表1可以看出,新建一个对象需要980个单位的时间,是本地赋值时间的980倍,是方法调用时间的166倍,而若新建一个数组所花费的时间就更多了。

再看清除对象的过程。我们知道,Java语言的一个优势,就是Java程序员勿需再像C/C++程序员那样,显式地释放对象,而由称为垃圾收集器(Garbage Collector)的自动内存管理系统,定时或在内存凸现出不足时,自动回收垃圾对象所占的内存。凡事有利总也有弊,这虽然为Java程序设计者提供了极大的方便,但同时它也带来了较大的性能开销。这种开销包括两方面,首先是对象管理开销,GC为了能够正确释放对象,它必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。其次,在GC开始回收“垃圾”对象时,系统会暂停应用程序的执行,而独自占用CPU。

因此,如果要改善应用程序的性能,一方面应尽量减少创建新对象的次数;同时,还应尽量减少T1、T3的时间,而这些均可以通过对象池技术来实现。

对象池技术的基本原理

对象池技术基本原理的核心有两点:缓存和共享
即对于那些被频繁使用的对象,在使用完后,不立即将它们释放,而是将它们缓存起来,以供后续的应用程序重复使用,从而减少创建对象和释放对象的次数,进而改善应用程序的性能。事实上,由于对象池技术将对象限制在一定的数量,也有效地减少了应用程序内存上的开销。

实现一个对象池,一般会涉及到如下的类:

1)对象池工厂(ObjectPoolFactory)类
该类主要用于管理相同类型和设置的对象池(ObjectPool),它一般包含如下两个方法:

  • createPool:用于创建特定类型和设置的对象池;
  • destroyPool:用于释放指定的对象池;

同时为保证ObjectPoolFactory的单一实例,可以采用Singleton设计模式,见下述getInstance方法的实现。

  • 对象池创建(参考GenericObjectPool):
    public GenericObjectPool(PoolableObjectFactory factory, GenericObjectPool.Config config)
    此方法创建一个GenericObjectPool实例,GenericObjectPool类已经实现了和对象池有关的所有核心操作,开发者可以通过继承或者封装的方式来使用它.通过此构造函数,我们能够清晰的看到,一个Pool中需要指定PoolableObjectFactory 实例,以及此对象池的Config信息.PoolableObjectFactory主要用来"创建新对象",比如当对象池中的对象不足时,可以使用PoolableObjectFactory.makeObject()方法来创建对象,并交付给Pool管理.

此构造函数实例化了一个LinkedList作为"对象池"容器,用来存取"对象".此外还会根据timeBetweenEvictionRunsMillis的值来决定是否启动一个后台线程,此线程用来周期性扫描pool中的对象列表,已检测"对象池中的对象"空闲(idle)的时间是否达到了阀值,如果是,则移除此对象.

2)参数对象(ParameterObject)类

该类主要用于封装所创建对象池的对象存取机制,如池中可存放对象的数目的最大值(maxCount)、最小值(minCount)等。

3)对象池(ObjectPool)类

用于管理要被池化对象的借出和归还,并通知PoolableObjectFactory完成相应的工作。它一般包含如下两个方法:

  • Object borrowObject() : 从Pool获取一个对象,此操作将导致一个"对象"从Pool移除(脱离Pool管理),调用者可以在获得"对象"引用后即可使用,且需要在使用结束后"归还".
public Object borrowObject() throws Exception {  
    Object value = null;  
    synchronized (this) {  
        if(!_pool.isEmpty()){  
            value = _pool.remove();  
        }  
          
    }         
    for(;;) {       
        //如果Pool中没有"对象",则根据相应的"耗尽"策略  
        if(value == null) {  
            switch(whenExhaustedAction) {  
                //如果耗尽,仍继续创建新"对象"  
                case WHEN_EXHAUSTED_GROW:  
                    value = _factory.makeObject();  
                    break;  
                //如果耗尽,则终止,此时以异常的方式退出.  
                case WHEN_EXHAUSTED_FAIL:  
                    throw new NoSuchElementException("Pool exhausted");  
                //如果耗尽,则阻塞,直到有"对象"归还  
                case WHEN_EXHAUSTED_BLOCK:  
                    try {  
                        synchronized (value) {  
                            if (value == null) {  
                                //maxWait为Config中指定的"最大等待时间"  
                                if(maxWait <= 0) {  
                                    latch.wait();  
                                } else {  
                                        latch.wait(waitTime);  
                                }  
                            } else {  
                                break;  
                            }  
                        }  
                    } catch(InterruptedException e) {         
                        //  
                        break;  
                    }  
                      
                default://  
            }  
        }  
  
        try {  
            _factory.activateObject(latch.getPair().value);  
            if(_testOnBorrow &&  
                    !_factory.validateObject(latch.getPair().value)) {  
                throw new Exception("ValidateObject failed");  
            }  
           return value;  
        }  
        catch (Throwable e) {  
            try {  
                _factory.destroyObject(latch.getPair().value);  
            } catch (Throwable e2) {  
                //  
            }  
        }  
    }  
}  
  • void returnObject(Object obj) : "归还"对象,当"对象"使用结束后,需要归还到Pool中,才能维持Pool中对象的数量可控,如果不归还到Pool,那么将意味着在Pool之外,将有大量的"对象"存在,那么就使用了"对象池"的意义.如下为伪代码:
public void returnObject(Object obj) throws Exception {  
    try {  
        boolean success = true;//  
        if(_testOnReturn && !(_factory.validateObject(obj))) {  
            success = false;  
        } else {  
            _factory.passivateObject(obj);  
        }  
  
        synchronized (this) {  
            //检测pool中已经空闲的对象个数是否达到阀值.  
            if((_maxIdle >= 0) && (_pool.size() >= _maxIdle)) {  
                success = false;  
            } else if(success) {  
                _pool.addFirst(new ObjectTimestampPair(obj));  
            }  
        }  
  
        // Destroy the instance if necessary  
        if(!success) {  
            try {  
                _factory.destroyObject(obj);  
            } catch(Exception e) {  
                // ignored  
            }  
        }  
    } catch (Exception e) {  
        //  
    }  
}  

4)池化对象工厂(PoolableObjectFactory)类

该类主要负责管理池化对象的生命周期,就简单来说,一般包括对象的创建及销毁。该类同ObjectPoolFactory一样,也可将其实现为单实例。
通过使用ObjectFactory(工厂模式)的方式将"对象池中的对象"的创建/检测/销毁等特性解耦出来,这是一个非常良好的设计思想.此接口有一个抽象类BasePoolableObjectFactory,可供开发者继承和实现.

  • Object makeObject() : 创建一个新对象;当对象池中的对象个数不足时,将会使用此方法来"输出"一个新的"对象",并交付给对象池管理.
  • void destroyObject(Object obj) : 销毁对象,如果对象池中检测到某个"对象"idle的时间超时,或者操作者向对象池"归还对象"时检测到"对象"已经无效,那么此时将会导致"对象销毁";"销毁对象"的操作设计相差甚远,但是必须明确:当调用此方法时,"对象"的生命周期必须结束.如果object是线程,那么此时线程必须退出;如果object是socket操作,那么此时socket必须关闭;如果object是文件流操作,那么此时"数据flush"且正常关闭.
  • boolean validateObject(Object obj) : 检测对象是否"有效";Pool中不能保存无效的"对象",因此"后台检测线程"会周期性的检测Pool中"对象"的有效性,如果对象无效则会导致此对象从Pool中移除,并destroy;此外在调用者从Pool获取一个"对象"时,也会检测"对象"的有效性,确保不能讲"无效"的对象输出给调用者;当调用者使用完毕将"对象归还"到Pool时,仍然会检测对象的有效性.所谓有效性,就是此"对象"的状态是否符合预期,是否可以对调用者直接使用;如果对象是Socket,那么它的有效性就是socket的通道是否畅通/阻塞是否超时等.
  • void activateObject(Object obj) : "激活"对象,当Pool中决定移除一个对象交付给调用者时额外的"激活"操作,比如可以在activateObject方法中"重置"参数列表让调用者使用时感觉像一个"新创建"的对象一样;如果object是一个线程,可以在"激活"操作中重置"线程中断标记",或者让线程从阻塞中唤醒等;如果object是一个socket,那么可以在"激活操作"中刷新通道,或者对socket进行链接重建(假如socket意外关闭)等.
  • void void passivateObject(Object obj) : "钝化"对象,当调用者"归还对象"时,Pool将会"钝化对象";钝化的言外之意,就是此"对象"暂且需要"休息"一下.如果object是一个socket,那么可以passivateObject中清除buffer,将socket阻塞;如果object是一个线程,可以在"钝化"操作中将线程sleep或者将线程中的某个对象wait.需要注意的时,activateObject和passivateObject两个方法需要对应,避免死锁或者"对象"状态的混乱.

首选需要声明,不同的"对象池"(或者连接池)在设计上可能存在很大的区别,但是在思想上大同小异

基本对象获取与归还原理

image.png

管理控制策略PoolConfig

  • maxActive: 链接池中最大连接数,默认为8.
  • maxIdle: 链接池中最大空闲的连接数,默认为8.
  • minIdle: 连接池中最少空闲的连接数,默认为0.
  • maxWait: 当连接池资源耗尽时,调用者最大阻塞的时间,超时将跑出异常。单位,毫秒数;默认为-1.表示永不超时.
  • minEvictableIdleTimeMillis: 连接空闲的最小时间,达到此值后空闲连接将可能会被移除。负值(-1)表示不移除。
  • softMinEvictableIdleTimeMillis: 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留“minIdle”个空闲连接数。默认为-1.
  • numTestsPerEvictionRun: 对于“空闲链接”检测线程而言,每次检测的链接资源的个数。默认为3.
  • testOnBorrow: 向调用者输出“链接”资源时,是否检测是有有效,如果无效则从连接池中移除,并尝试获取继续获取。默认为false。建议保持默认值.
  • testOnReturn: 向连接池“归还”链接时,是否检测“链接”对象的有效性。默认为false。建议保持默认值.
  • testWhileIdle: 向调用者输出“链接”对象时,是否检测它的空闲超时;默认为false。如果“链接”空闲超时,将会被移除。建议保持默认值.
  • timeBetweenEvictionRunsMillis: “空闲链接”检测线程,检测的周期,毫秒数。如果为负值,表示不运行“检测线程”。默认为-1.
  • whenExhaustedAction: 当“连接池”中active数量达到阀值时,即“链接”资源耗尽时,连接池需要采取的手段, 默认为1:
    -> 0 : 抛出异常,
    -> 1 : 阻塞,直到有可用链接资源
    -> 2 : 强制创建新的链接资源

通用对象池的实现

对象池的构造和管理可以按照多种方式实现。最灵活的方式是将池化对象的Class类型在对象池之外指定,即在ObjectPoolFactory类创建对象池时,动态指定该对象池所池化对象的Class类型,其实现代码如下:

public ObjectPool createPool(ParameterObject paraObj,Class clsType) {
 return new ObjectPool(paraObj, clsType);
}

其中,paraObj参数用于指定对象池的特征属性,clsType参数则指定了该对象池所存放对象的类型。对象池(ObjectPool)创建以后,下面就是利用它来管理对象了,具体实现如下:

public class ObjectPool {
 private ParameterObject paraObj;//该对象池的属性参数对象
 private Class clsType;//该对象池中所存放对象的类型
 private int currentNum = 0; //该对象池当前已创建的对象数目
 private Object currentObj;//该对象池当前可以借出的对象
 private Vector pool;//用于存放对象的池
 public ObjectPool(ParameterObject paraObj, Class clsType) {
  this.paraObj = paraObj;
  this.clsType = clsType;
  pool = new Vector();
 }
 public Object getObject() {
  if (pool.size() <= paraObj.getMinCount()) {
   if (currentNum <= paraObj.getMaxCount()) {
    //如果当前池中无对象可用,而且已创建的对象数目小于所限制的最大值,就利用
    //PoolObjectFactory创建一个新的对象
    PoolableObjectFactory objFactory =PoolableObjectFactory.getInstance();
    currentObj = objFactory.create Object (clsType);
    currentNum++;
   } else {
    //如果当前池中无对象可用,而且所创建的对象数目已达到所限制的最大值,
    //就只能等待其它线程返回对象到池中
    synchronized (this) {
     try {
      wait();
     } catch (InterruptedException e) {
      System.out.println(e.getMessage());
      e.printStackTrace();
     }
     currentObj = pool.firstElement();
    }
   }
  } else {
   //如果当前池中有可用的对象,就直接从池中取出对象
   currentObj = pool.firstElement();
  }
  return currentObj;
}
  public void returnObject(Object obj) {
   // 确保对象具有正确的类型
   if (obj.isInstance(clsType)) {
    pool.addElement(obj);
    synchronized (this) {
     notifyAll();
    }
   } else {
    throw new IllegalArgumentException("该对象池不能存放指定的对象类型");
   }
  }
}

从上述代码可以看出,ObjectPool利用一个java.util.Vector作为可扩展的对象池,并通过它的构造函数来指定池化对象的Class类型及对象池的一些属性。在有对象返回到对象池时,它将检查对象的类型是否正确。当对象池里不再有可用对象时,它或者等待已被使用的池化对象返回池中,或者创建一个新的对象实例。不过,新对象实例的创建并不在ObjectPool类中,而是由PoolableObjectFactory类的createObject方法来完成的,具体实现如下:

public Object createObject(Class clsType) {
 Object obj = null;
 try {
  obj = clsType.newInstance();
 } catch (Exception e) {
  e.printStackTrace();
 } 
 return obj;
}

这样,通用对象池的实现就算完成了,下面再看看客户端(Client)如何来使用它,假定池化对象的Class类型为StringBuffer:

//创建对象池工厂
ObjectPoolFactory poolFactory = ObjectPoolFactory. getInstance ();
//定义所创建对象池的属性
ParameterObject paraObj = new ParameterObject(2,1);
//利用对象池工厂,创建一个存放StringBuffer类型对象的对象池
ObjectPool pool = poolFactory.createPool(paraObj,String Buffer.class);
//从池中取出一个StringBuffer对象
StringBuffer buffer = (StringBuffer)pool.getObject();
//使用从池中取出的StringBuffer对象
buffer.append("hello");
System.out.println(buffer.toString()); 

可以看出,通用对象池使用起来还是很方便的,不仅可以方便地避免频繁创建对象的开销,而且通用程度高。但遗憾的是,由于需要使用大量的类型定型(cast)操作,再加上一些对Vector类的同步操作,使得它在某些情况下对性能的改进非常有限,尤其对那些创建周期比较短的对象。

专用对象池的实现

由于通用对象池的管理开销比较大,某种程度上抵消了重用对象所带来的大部分优势。为解决该问题,可以采用专用对象池的方法。即对象池所池化对象的Class类型不是动态指定的,而是预先就已指定。这样,它在实现上也会较通用对象池简单些,可以不要ObjectPoolFactory和PoolableObjectFactory类,而将它们的功能直接融合到ObjectPool类,具体如下(假定被池化对象的Class类型仍为StringBuffer,而用省略号表示的地方,表示代码同通用对象池的实现):

public class ObjectPool {
 private ParameterObject paraObj;//该对象池的属性参数对象
 private int currentNum = 0; //该对象池当前已创建的对象数目
 private StringBuffer currentObj;//该对象池当前可以借出的对象
 private Vector pool;//用于存放对象的池
 public ObjectPool(ParameterObject paraObj) {
  this.paraObj = paraObj;
  pool = new Vector();
 }
 public StringBuffer getObject() {
  if (pool.size() <= paraObj.getMinCount()) {
   if (currentNum <= paraObj.getMaxCount()) {
    currentObj = new StringBuffer();
    currentNum++;
   } 
   . . . 
  }
  return currentObj;
 }
 public void returnObject(Object obj) {
  // 确保对象具有正确的类型
  if (StringBuffer.isInstance(obj)) {
   . . . 
  }
 }

小结

恰当地使用对象池技术,能有效地改善应用程序的性能。目前,对象池技术已得到广泛的应用,如对于网络和数据库连接这类重量级的对象,一般都会采用对象池技术。但在使用对象池技术时也要注意如下问题:

·并非任何情况下都适合采用对象池技术。基本上,只在重复生成某种对象的操作成为影响性能的关键因素的时候,才适合采用对象池技术。而如果进行池化所能带来的性能提高并不重要的话,还是不采用对象池化技术为佳,以保持代码的简明。

·要根据具体情况正确选择对象池的实现方式。如果是创建一个公用的对象池技术实现包,或需要在程序中动态指定所池化对象的Class类型时,才选择通用对象池。而大部分情况下,采用专用对象池就可以了。

代码实例

本实例主要用来演示一个"TCP连接池".
1) ConnectionPoolFactory.java:

public class ConnectionPoolFactory {  
  
    private GenericObjectPool pool;  
  
    public ConnectionPoolFactory(Config config,String ip,int port){  
        ConnectionFactory factory = new ConnectionFactory(ip, port);  
        pool = new GenericObjectPool(factory, config);  
    }  
      
    public Socket getConnection() throws Exception{  
        return (Socket)pool.borrowObject();  
    }  
      
    public void releaseConnection(Socket socket){  
        try{  
            pool.returnObject(socket);  
        }catch(Exception e){  
            if(socket != null){  
                try{  
                    socket.close();  
                }catch(Exception ex){  
                    //  
                }  
            }  
        }  
    }  
      
    /** 
     * inner  
     * @author qing 
     * 
     */  
    class ConnectionFactory extends BasePoolableObjectFactory {  
  
        private InetSocketAddress address;  
          
        public ConnectionFactory(String ip,int port){  
            address = new InetSocketAddress(ip, port);  
        }  
          
        @Override  
        public Object makeObject() throws Exception {  
            Socket socket = new Socket();  
            socket.connect(address);  
            return socket;  
        }  
          
        public void destroyObject(Object obj) throws Exception  {  
            if(obj instanceof Socket){  
                ((Socket)obj).close();  
            }  
        }  
  
        public boolean validateObject(Object obj) {  
            if(obj instanceof Socket){  
                Socket socket = ((Socket)obj);  
                if(!socket.isConnected()){  
                    return false;  
                }  
                if(socket.isClosed()){  
                    return false;  
                }  
                return true;  
            }  
            return false;  
        }  
    }  
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 转自:http://blog.csdn.net/jackfrued/article/details/4492194...
    王帅199207阅读 8,384评论 3 93
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,015评论 11 349
  • 在最美的年纪遇见你,正如这一年,这个季节,我和大学有个“约会”。 都说人生是一本书,那青春则是其中最精彩的...
    飘零的回忆阅读 349评论 0 2
  • 版权信息的常用属性
    我_巨可爱阅读 136评论 0 0