6 对象重用与GC
有时候我们为了提高系统的性能,避免重复耗时的操作,希望能够重用某些创建完成的对象,但是既然是重用(reuse)就涉及对象保存的问题,通常将用来缓存对象的应用称为对象池(ObjectPool),通过这个途径我们可以大大地提高应用的速度,减少内存需求,例如,我们经常提到的JDBC连接池与EJB实例池等概念都属于对象池的范畴。
通过使用对象池的办法来提高系统性能,节约系统内存开销是一个非常简易、高效的方法,对象池通过对其所保存对象的共享与重用,缩减了应用线程反复重建、装载对象的过程所需要的时间,并且也有效地避免了频繁垃圾回收带来的巨大系统开销。
下面我们给出对象池的代码框架,以帮助你理解对象池是怎样避免系统频繁的垃圾回收所带来的巨大系统开销的。下面是一个对象池的抽象类,也是应用对象池的基类:
public abstract class ObjectPool { private Hashtable locked, unlocked; private long expirationTime; abstract Object create(); abstract void expire(Object o); abstract void validate(Object o); synchronized Object getObject() {...} synchronized void freeObject(Object o) {...} }
在这个类中声明了5个重要方法: 创建对象方法create()、对象过期方法expire()、获取对象方法getObject()、对象有效性验证方法validate()与对象释放方法freeObject()。我们可以通过create()方法创建新的对象实例,并且将这个对象实例保存到哈希表(Hashtable)对象中,当其他应用请求对象实例时,可以通过调用getObject()方法获取哈希表中的对象,并检测其有效性是否过期,如果一切正常则将该对象传递给调用者,调用者使用完对象实例后可以通过调用方法freeObject()将该对象实例释放(归还)给对象池。
既然对象实例被保存,当应用试图重用该对象时就不需要重新创建新的对象,避免大量垃圾对象的产生,即使对象的生命周期较短可以被系统及时回收,但是这样会引发JVM频繁GC的危机导致系统性能下降。
但是如果长时间地将对象保存在内存中,如果这些对象又不被经常使用无疑也会造成不必要的内存资源浪费,或者该对象在对象池中遭到破坏,如果不能将该对象及时清除而继续占用系统的内存资源,也是非常麻烦的。因此在应用对象池技术重用对象时应该考虑其必要性并权衡利弊做出最优的选择,如果决定使用对象池技术,需要采取相应的手段清除遭到破坏的对象,甚至在某些情况下需要清除对象池中所有的对象,并且为每个对象分配一个时间戳,设定对象的过期时间,当对象过期后及时将其在内存中清除,下面以JDBC连接池为例说明通过对象池技术重用对象中的技术要点,帮助你理解怎样才能更好地提高系统性能,降低系统内存的开销。
在对象池中声明了对象创建方法abstract Object create() throws Exception。在JDBC连接池中也需要创建一个该抽象方法的实现方法Object create() throws SQLException,这个方法抛出了SQLException,在这个方法中通过对方法DriverManger.getConnection()的调用获取一个JDBC数据库连接对象。
Object create() throws SQLException { return ( DriverManger.getConnection(dsurl, usr, pwd) ); }
JDBC数据库连接池在接收到外部请求获取连接对象的请求之后,要getConnection()方法中调用创建连接对象方法create()方法。调用create()方法的前提是,连接池要确认连接池中的连接对象数量。当连接池中的对象实例数没有达到对象池实例最大值,并且连接池中所有已存在的连接都处于被占用状态,也就是说,此时连接池中没有空闲连接对象。当具备了上述条件后,才可以通过调用create()方法,创建新的连接对象,响应外部获取连接的请求,然后将创建的对象传递给getObject()方法的调用者。为了同步多线程对资源的访问,getObject()方法的声明如下:
synchronized Object checkOut() throws Exception
在getConnection()方法处理过程中有可能抛出SQL异常,因此其声明如下:
public Connection getConnection() throws SQLException { try { return ( (Connection)super.getObject() ); } catch (Exception ex) { throw ( (SQLException) ex ); } }
为了防止已损坏连接对象残存在连接池中而不能被及时清除,浪费系统内存资源,可以通过一个专门的线程来清除这些连接对象,缩减系统内存开销。我们可以通过创建一个线程及时检测连接池中的对象是否有效,如果无效则主动清除,如下所示。
class ConnectionCleanUpThread extends Thread { private ObjectPool pool; private long sleepTime; ConncetionCleanUpThread (ObjectPool pool, long sleepTime) { this.pool = pool; this.sleepTime = sleepTime; } public void run() { while(true) { try { sleep(sleepTime); } catch (InterruptedException ex) { // 做相应处理 } pool.cleanUp(); } } }
通过这个线程,就可以完成上面所提到的无效连接对象的清除工作,这个线程是在ObjectPool类的构造器中被初始化并启动的。
... ... public ObjectPool() { ... cleaner = new ConnectionCleanUpThread( this, expirationTime ); cleaner.start(); ... } ... ...
cleanUp()方法在清除所有无效Connection对象的同时,还会要求系统做垃圾回收工作,以及时回收这些被清除的对象。
synchronized void cleanUp() { Connection conn; long currentTime = System.currentTimeMillis(); Enumeration enumeration = unlocked.keys(); while (enumeration.hasMoreElements()) { conn = enumeration.nextElement(); if ( (currentTime - ((long) unlocked.get(conn) ).longValue() ) > expirationTime ) { unlocked.remove( conn ); expire( conn ); conn = null; // 请注意这一行代码的作用 } } System.gc(); }
在这个方法中的最后一行代码强制系统做垃圾回收,这是因为我们已经将连接池中被清除的对象做了空值的赋值操作,也就是释放了对该对象的引用,使其对虚拟机来说变得不可达,转化为系统垃圾,然后回收之,释放其占用的内存,结合上面的知识很容易理解这一点。
综上所述,使用对象池是有诸多好处的,但是我们一定要恰当地使用这项技术,否则反受其累。
如果对象池中的对象过多,或者没有做必要的清除处理,没有考虑应用所运行环境的内存资源的限制等,都会使系统导致灾难性的错误。因此当你决定采用这种技术时应当依据上面我们讲解的知识,考虑周全。正如上面所说的,其他对象池的技术与连接池的技术都是类似的,因此我们讲解本节的目的就是想起到抛砖引玉的作用,使你在处理这方面的应用时不至于在内存管理方面出现可避免的疏漏。