摘要:不当的O/R-Mapping框架使用,导致垃圾对象的生成。
最近用JProfile测试一个比较大的工程,希望能找到一些程序运行的瓶颈。过去使用Hibernate,很多人反映效率低。特别是懒加载(lazy loading)关闭的时候,对象的持续生成最后会导致JVM直接发生OutOfMemory错误。在我的工程中使用的O/R Mapping框架是iBatis,通常以为在管理持久对象时,只有数据库中有对应记录的才会加载对象。可是实际使用中发现跟持久对象有关的临时对象特别多,一开始百思不解。通过使用JProfile查找并分析以后,终于找到了原因所在。
JProfile的一般性使用这里就不作专门介绍了,可以到google上寻求帮助。
首先,我要介绍一下工程中的对象引用关系:baseView ->InstanceModel -> City
其中,baseView 引用了InstanceMode,而InstanceMode 引用了City对象,City对象是一个比较简单的对象(城市),只有id、name、中文名称等属性。在工程实际使用环境中,InstanceModel 是比较多的,可能会有数万经常被使用。通过对JVM堆的观察,发现每次InstanceModel对象生成的时候,都会附带出大量的City对象。由于一般应用中City的个数都有限,所以程序中专门对City对象作了缓存。为什么会产生大量City的临时对象呢?
通过在JProfile中查看堆的情况,可以很清楚的看到上述引用关系。这很正常啊?纳闷中,反复查看堆、对象、引用关系等信息,终于发现了线索。那就是,被引用的City对象中只有ID值,没有中文名称等其他信息。这就让我们找到了突破口,通过与开发工程师的交谈,被告知只使用了ID值,没有使用其他的属性。于是我们找到iBatis加载baseView对象的iBatis映射文件:
<resultMap id="baseView" class="View">
…
<result column="CITYCODE"
property="InstanceModel.city.id" jdbcType="NUMERIC" />
</resultMap>
注意,在上面这个定义文件中,加载View对象的时候,从同一张数据库表中,加载了View所属的City的代码(id)。但是iBatis框架在加载View对象的同时,还在堆中同时生成了一个临时的City对象,并把ID赋了值。这就是原因!!
好了,原因找到了,优化的方案自然就出来了。只需要去掉InstanceModel对City对象的引用,直接取City的ID就行了!再次运行JProfile,发现不再生成大量的City临时对象,优化的目的达到了。
结论:使用Hibernate、iBatis等框架的时候,我们过于热衷于对象引用的方便,忽视了这种方便的代价。有时候,我们只为了使用一个简单类型的数据,却大量加载肥胖对象而不自知。然后抱怨框架效率低,有问题。其实回头看看,良好的数据库设计、良好的对象设计这些基本的东西,是任何框架都不能帮我们做的。同时感叹JProfile这种强大工具给我们带来的好处,让我感觉又回到了DOS Debug 的时代。