一、JDBC驱动内存机制
Oracle为了提高数据库访问性能,提供了服务器端缓存和客户端缓存这两种缓存。其中,Oracle客户端缓存可能会使用大量的内存,这是一种有意识的设计选择,在使用大量内存与提高性能之间做出权衡,用内存换取性能。
其中主要对内存产生较大影响的缓存有以下几种:
1.查询结果缓存
JDBC驱动缓存中缓存的是sql的执行结果,这样相同sql再次执行时可以直接从缓存中获取数据,而不需要与服务器端交互,从而达到提高查询性能的作用。默认情况下,我们在使用一个数据库连接去执行查询时,JDBC驱动会为连接内的每一个Statement(Statement、PreparedStatement、CallableStatement)创建两个buffer:byte[]和char[]。其中char[]用来保存所有字符类型的行数据,如:CHAR,VARCHAR2,NCHAR等,byte[]用来保存所有的其它类型的行数据。这些buffer在在SQL被解析的时候分配,一般也就是在第一次执行该Statement的时候。Statement会持有这两个buffer,直到它被关闭。
2.PreparedStatement缓存
大型系统中往往很多次执行相同的sql,出于性能方面考虑,驱动重用了PreparedStatement,而不是每次为每条sql创建新的PreparedStatement。JDBC驱动内置了一个语句缓存-- Implicit Statement Cache(它的大小默认为0,即不缓存PreparedStatement)。这个语句缓存对用户是透明的,可通过配置连接属性,设置缓存的PreparedStatement数量。
3.buffer缓存
Oracle11.2版本驱动中,驱动程序中提供了一个内存管理更复杂的方法。这个方法有两个目标,最大限度减少内存未使用量和最小化buffer分配带来的成本。驱动程序在每个连接内部创建了一个buffer缓存(buffer cache)。当一个PreparedStatment使用结束被放回Implicit Statement Cache中时,它的buffer会被缓存到buffer缓存中buffer桶(buncket)中,一个buffer桶中的所有buffer都是相同大小且这个大小是预先确定的。当一个PreparedStatement是从Implicit Statement中取出时,也同时会从buffer缓存中根据查询结果的大小从适当的buffer桶中取出buffer。
驱动提供一个连接属性oracle.jdbc.maxCachedBufferSize。它是一个int字符串,默认是Integer.MAX_VALUE。此属性限定了保存在buffer桶中buffer最大的长度大小。超过这个大小 的buffer会在PreparedStatement被放回Implicit Statement Cache中时释放,小于这个大小的buffer会被缓存到相应的buffer桶中,当PreparedStatement从Implicit Statement Cache中取出时,小于maxCachedBufferSize的buffer会从合适的buffer 桶中取出,大于maxCachedBufferSize的buffer 会被重新创建。
二、内存占用情况
buffer是在SQL解析的时候被分配的,buffer的大小并不取决于查询返回的行数据的实际长度,而是行数据可能的最大的长度。在SQL解析时,每列的类型是已知的,从该信息中驱动程序可以计算存储每一列所需的内存的最大长度。驱动程序也有fetchSize属性,也就是每次fetch返回的行数。有了每列有大小和行数的大小,驱动程序可以由此计算出一次fetch所返回的数据最大绝对长度。这也就是所分配的buffer的大小。
字符数据存储在char[]buffer中。Java中的每个字符占用两个字节。一个VARCHAR2(10)列将包含最多10个字符,也就是10个Java的字符,也就是每行20个字节。一个VARCHAR2(4000)列将占用每行8K字节。重要的其实是column的定义大小,而不是实际数据的大小。一个VARCHAR2(4000)但是只包含了NULL的列,仍然需要每行8K字节。buffer是在驱动程序看到的查询结果之前被分配的,因此驱动程序必须分配足够的内存,以应付最大可能的行大小。一个定义为VARCHAR2(4000)的列最多可包含4000个字符。Buffer必须大到足以容纳4000个字符分配,尽管实际的结果数据可能没有那么大。
BFILE,BLOB和CLOB会被存储为locator。Locator可高达4K字节,每个BFILE,BLOB和CLOB列的byte[]必须有至少每行4K字节。RAW列最多可以包含4K字节。其它类型的则需要很少的字节。一个合理的近似值是假设所有其它类型的列,每行占用22个字节。
假设目前数据库中,每条记录定义:10K,fetchsize:50,PreparedStatementCache:100,连接池max值:30, 连接池个数:2个。在极端情况下,内存占用会达到:2*10K*50*100*30*2=6GB 。
三、控制参数说明
说明以下参数都可以通过-D方式添加到启动sofa容器的启动参数中设置系统属性。
1.oracle.jdbc.maxCachedBufferSize
此属性限定了保存在buffer缓存中buffer的最大的长度大小。它是一个int字符串,默认是Integer.MAX_VALUE。超过这个大小 的buffer会在PreparedStatement被放回Implicit Statement Cache中时释放,小于这个大小的buffer会被缓存到相应的buffer桶中,当PreparedStatement从Implicit Statement Cache中取出时,小于maxCachedBufferSize的buffer会从合适的buffer 桶中取出,大于maxCachedBufferSize的buffer 会被重新创建。
2. oracle.jdbc.useThreadLocalBufferCache
默认值:false,默认情况下,buffer缓存中存储在连接中的,即每个连接都会有自己的缓存。如果在真实业务场景中,我们存在大量这样的使用场景:在一个处理逻辑中,我们需要使用多个连接去访问数据库,那么这些连接各自都有自己的缓存。且相对于线程数来说存在大量空闲数据库连接,由于默认情况下buffer缓存是附属于每个连接的,空闲连接的结果就是buffer缓存中有很多不会被用到的buffer,会用很多没必要的内存。我们可以把oracle.jdbc.useThreadLocalBufferCache属性设置为true,这样我们可以让业务处理逻辑所在线程内的所有连接共用一个缓存,即把buffer存储到线程中,如此一来,线程内使用到的连接都会共用当前线程内的buffer。该属性可以通过-D设置System property或者通过的调用getConnection时的connection property。
3. OracleStatement.setLobPrefetchSize
LOB字段默认大小为4000bytes。设置BLOB字段每次获取字节数据的大小,这种方式也可以限制一行查询结果的大小。因sofa底层使用的是hibernate,不支持对该参数的设置控制。在使用jdbc方式时可以设置该参数。
4.fetchSize
buffer的大小不是由查询的实际行数决定的,而是由每次获取的行数决定的。这个每次获取的行数也就是fetchSize,见下图。
JDBC驱动默认是10。我们可以在hibernate_properties_config.xml中通过配置
<prop key="hibernate.jdbc.fetch_size">50</prop>来设置。
或在连接池中按不同连接池的属性去配置fetchSize大小。
5.优化连接池
buffer默认是存储在每个数据库连接中的。连接池中配置太多的连接,会导致连接大部分空闲。在极端情况下,如果连接池内的连接被全部使用,每个连接内都会有查询结果缓存,就会导致应用程序内存占用暴增。