日期:2014-05-16  浏览次数:20667 次

Oracle JDBC内存管理--Oracle白皮书2009年8月
Oracle JDBC内存管理--Oracle白皮书2009年8月
原文:http://www.oracle.com/technetwork/database/enterprise-edition/memory.pdf

介绍
Oracle JDBC驱动程序可能会使用大量的内存。这是一种有意识的设计选择,在使用大量内存与提高性能之前做出权衡。在大多数情况下,对于大多数用户,这已被证明是一个不错的选择。一些用户已经经历了JDBC驱动程序使用的大量内存的问题。本白皮书正是写给这些用户。如果您的应用程序的性能表现是可接受的,那么你没有理由担心内存使用。如果您的应用程序并没有达到你所期望的性能,并且使用了比预期的更多的内存,那么请继续读下去。

7.3.4,8i中,和9i Oracle JDBC驱动程序使用差不多尽可能少的内存。不幸的是,这导致了不可接受的性能损失。在10g中,为了提高性能,驱动程序的体系结构被重新设计了。在该体系结构重建的最重要的变化之一是如何驱动程序使用的内存。开发团队做出了有针对性的决定,用内存换取性能。10g驱动程序因此比9i的驱动程序平均快约30%。当然,您所以没得的数据可能会有所不同。

由于内存价格相对比较便宜,伴随着10g驱动程序发布之后物理内存的大小也在显着地不断上涨,因此大多数用户都受益于(内存换性能的)性能改进。有一些用户,而且大部分是具有非常大规模应用的用户,已经看到了由于heap过大,垃圾收集器的trashing,甚至OutOfMemoryExceptions所引起的性能问题。在后续的版本中,开发团队一直在努力解决这些问题,通过改进驱动程序使用内存的方式,为用户提供额外的(内存使用)控制,以解决这些具体的问题。本白皮书介绍了驱动程序是如何使用内存的,应用程序特点如何具体地影响内存的使用,以及用户可以做些什么以更好地管理内存使用和提高应用程序性能。

注:在本文的其余部分的单词“驱动程序”本身是指10g及以后版本的Oracle JDBC驱动程序。其它情况会指明特定版本或被引用的版本。

它们都被用到哪里去了?
10g的Oracle JDBC驱动程序,具有比以前的版本更大、更复杂的类层次结构。这些类的对象存储了更多的信息,所以需要更多的内存。这确实增加了内存的使用,但并这并不是真正的问题所在。真正的问题是用来存储查询结果的buffer。每个语句(包括PreparedStatement和CallableStatement的)都持有两个缓冲区,一个byte[](字节数组)和一个char [](字符数组)。char[]用来保存所有字符类型的行数据,如:CHAR,VARCHAR2,NCHAR等,byte[]用来保存所有的其它类型的行数据。这些buffer在在SQL被解析的时候分配,一般也就是在第一次执行该Statement的时候。Statement会持有这两个buffer,直到它被关闭。

由于buffer是在SQL解析的时候被分配的,buffer的大小并不取决于查询返回的行数据的实际长度,而是行数据可能的最大的长度。在SQL解析时,每列的类型是已知的,从该信息中驱动程序可以计算存储每一列所需的内存的最大长度。驱动程序也有fetchSize属性,也就是每次fetch返回的行数。有了每列有大小和行数的大小,驱动程序可以由此计算出一次fetch所返回的数据最大绝对长度。这也就是所分配的buffer的大小。

某些大类型,如LONG和LONG RAW,由于太大而无法直接存于buffer中会采取另外一种不同的处理方式。如果查询结果包含一个LONG或LONG RAW,将fetchSize设置为1之后,所遇见的内存问题就会变得明朗很多了。这种类型的问题不在这里讨论了。

字符数据存储在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个字节。

范例
CREATE TABLE TAB (ID NUMBER(10), NAME VARCHAR2(40), DOB DATE)

ResultSet r = stmt.executeQuery(“SELECT * FROM TAB”);

当驱动器执行executeQuery方法,数据库将解析SQL。数据库会返回结果集将有三列:一个NUMBER(10),一个VARCHAR2(40),和一个DATE。第一列的需要(约)每行22个字节。第二列需要每行40个字符。第三列的需要(约)每行22个字节。因此,每行需要22 +(40 * 2)+ 22 = 124个字节。请记住,每个字符需要两个字节。fetchSize默认是10行,所以驱动程序将分配一个char[] 10 * 40 = 400个字符(800字节)和一个byte[] 10 *(22 + 22)= 440个字节,共1240字节。 1240字节不会导致什么内存问题。但有一些查询结果会比较更大一些。

在最坏的情况下,考虑一个返回255个VARCHAR(4000)列查询。每行每列需要8K字节。乘以255列,每行2040K字节也就是2MB。如果fetchSize设置为1000行,那么驱动程序将尝试分配一个2GB的char[]。这将是很糟糕的。

作为一个Java开发人员,读者无疑想到其它分配和使用内存的方式。请放心,Oracle的JDBC的开发团队也是能够想到。其它的方式都进行了测试,然后才选得这样一种方式。虽然还没有进入具体细节,但是确实有很多好的理由让我们选择这种方式。这个选择完全是为了驱动程序的性能。

管理buffer的大小
用户可以通过几种方法来管理这些buffer的大小。

1. 小心地定义table

2. 小心地编写查询代码

3. 小心地设置fetchSize的值

一个VARCHAR2(20)就够用的列被定义成VARCHAR2(4000)会导致非常大的区别。一个VARCHAR2(4000)列的要求每行8K字节。一个VARCHAR2(20)列每行只需要40个字节。如果列事实上从来没有超过20个字符,然后定义一个VARCHAR2(4000)列是在浪费驱动程序分配的buffer空间。

在只需要几列的情况下使用SELECT *,除了从buffer大小以外,也会非常大地影响性能,。它会需要更多时间来获取行的内容,将其转换,然后通过网络发送,再将其转换为Java表达。虽然返回了几十列,但只有少数是需要的,会导致驱动程序分配大量的buffer来存储那些不需要的结果。

控制内存使用的主要工具是fetchSize。虽然2MB也算比较大的,但是大多数Java环境分配这样大的buffer不会有任何问题。即使在最坏的情况下,如果fetchSize设置为1,255个VARCHAR(4000)列的结果也不会在大多数应用中产生问题。

解决内存使用问题的第一步是检查SQL。计算每个查询的一行数据的近似大小,然后看看fetchSize的大小。如果每行的大小是非常大,那考虑一下是否有可能获取较少的列或修改schema使得数据更加紧密以限制行大小。最后,设置fetchSize以保持buffer在一个合理的大小。什么是“合理的”取决于应用程序的具体情况。Oracle建议fetchSize不要超过100,虽然在某些情况下,更大一些的值也是合适的。就算会返回很多行,设置fetchSize为100了可能是不恰当的。

注: Oracle提供的方法OracleStatement.defineColumnType可用于减少过大的列定义的大小 。当调提供一个size参数时,size将覆盖该schema中列定义的大小。这允许你在无法自由修改schema的情况下,也可以一定程度上的解决问题。使用Thin驱动程序时,你可以只在有问题的列上调用defineColumnType。当使用OCI驱动程序时,你可以在具体的Statement上调用它,也可以为所有的Statement的所有