由于PreparedStatementCache性能提升明显,DruidDataSource、DBCP、JBossDataSource、WeblogicDataSource都实现了PreparedStatementCache。

    PreparedStatementCache带来的问题

    阿里巴巴在使用jboss连接池做PreparedStatementCache时,遇到了full gc频繁的问题。通过mat来分析jmap dump的结果,发现T4CPreparedStatement占内存很多,出问题的几个项目,有的300M,有的500M,最夸张的900M。这些应用都是用jboss连接池访问Oracle数据库,T4CPreparedStatement是Oracle JDBC Driver的PreparedStatement一种实现。oracle driver不是开源,通过逆向工程以及mat分析,发现其中占内存的是字段char[] defineChars,defineChars大小的计算公式是这样的:

    rowPrefetchCount在Oracle中,缺省值为10。

    其中rowSize是执行查询设计的每一列的大小的和。计算公式是:

    实际占据内存的公式:

    我们实际分析,一个应用运行的SQL大约数百条,PreparedStatementCacheSize为50,PreparedStatementCache的算法为LRU,很多的SQL执行之后,在Cache中HitCount为0就被淘汰了,淘汰的过程,其位置从第1移到第50,这个漫长的过程导致了defineChars不能够被young gc回收。

    Druid的解决方案

    使用OracleDriver提供的PreparedStatementCache支持方法,清理PreparedStatement所持有的buffer。Oracle在10.x和11.x的Driver中,都提供了如下管理PreparedStatementCache的接口,如下:

    DruidDataSource在管理Oracle PreparedStatement Cache时,调用了上述方法。当调用了enterImplicitCache之后,T4CPreparedStatement中的defineChars和defineBytes都会被清空。

    根据PreparedStatement执行的结果,计算RowPrefetch大小DrudDataSource对在PreparedStatement.executeQuery和execute方法返回的ResultSet做监控统计执行SQL返回的行数,然后根据统计的结果来设置rowPrefetchSize。例如SQL

      这样的SQL每次返回的纪录数量都是0或者1,根据这个统计的最大值来设置rowPrefetchSize。如果最大值为1,则需要设置rowPrefetchSize为2。

      计算公式如下:

      根据生产环境的监控统计,大多数的SQL返回的行数都是比较小的,通常是1。通过这种算法,能够减少PreparedStatementCache的内存占用。

      通过这五个计数器,我们清晰了解PreparedStatementCache的工作情况,然后根据实际情况调整。