SQLite通过动态内存分配来获取各种对象(例如数据库连接和SQL预处理语句)所需内存、建立数据库文件的内存Cache、保存查询结果。
1、特性
SQLite内核和它的内存分配子系统提供以下特性:
(1)对内存分配失败的健壮处理。如果一个内存分配请求失败(即malloc()或realloc()返回NULL),SQLite将释放未关联的缓存页,然后重新进行分配请求。如果失败,SQLite返回SQLITE_NOMEM给应用程序。
(2)无内存泄漏。应用程序负责销毁已分配的任何对象。例如应用程序必须使用sqlite3_finalize()结束每个预处理SQL语句,使用sqlite3_close关闭每个数据库连接。只要应用程序配合,即使在内存分配失败或系统出错的情况下SQLite也绝不会泄漏内存。
(3)内存使用限制。sqlite3_soft_heap_limit64()机制可以让应用程序设置SQLite的内存使用限制。SQLite会从缓存中重用内存,而不是分配新的内存,以满足设置的限制。
(4)零分配选项。应用程序可以在启动时给SQLite提供几个大块内存的缓冲区,SQLite将用这些缓冲区作为它所有内存分配的需要,不再调用系统的malloc()和free()。
(5)应用程序提供内存分配器。应用程序在启动时可以给SQLite提供可选的内存分配器,以代替系统的malloc()和free()。
(6)对防止内存分配失败或堆内存出现碎片提供形式化的保证。SQLite能配置成保证不会出现内存分配失败或内存碎片。这个特性对长期运行、高可靠性的嵌入式系统至关重要,在这样的系统上一个内存分配错误可能会导致整个系统失效。
(7)内存使用统计。应用程序可以统计SQLite使用了多少内存,可以检测内存使用是否接近或超过设计限制。
(8)尽量少调用分配器。系统的malloc()和free()实现在很多系统上是低效的。SQLite通过尽量少地使用malloc()和free()来减少整个处理时间。
(9)开放式存取。可插拨的SQLite扩展模块或应用程序自己可以通过sqlite3_malloc()、sqlite3_realloc()和sqlite3_free()接口来访问SQLite使用的底层内存分配器。
2、测试
测试基础设施通过使用一个特殊的检测内存分配器来验证SQLite没有错误地使用动态分配的内存。检测内存分配器通过编译时使用SQLITE_MEMDEBUG选项来激活,它比缺省的内存分配器更慢,因此不建议在产品中使用它。但是在测试时激活它,可以做以下检查:
(1)边界检查。检测内存分配器在每个内存分配的末尾放置哨兵值,以验证SQLite没有任何例程写出了分配的边界。
(2)释放后的内存使用。当每块内存被释放时,每个字节会填充一些无用的位模式。这可以确保内存在释放后绝不会留下曾经使用过的痕迹。
(3)从非malloc获取的内存的释放。每个来自于检测内存分配器的内存分配都含有一个哨兵值,以验证每个分配的释放来自于先前的malloc。
(4)未初始化的内存。检测内存分配器用一些无用的位模式初始化每块内存的分配,以确保用户不能对所分配内存的内容做任何假设。
无论是否使用检测内存分配器,SQLite都会跟踪当前已取出多少内存。有数百个测试脚本用于测试SQLite。在每个脚本的末尾,所有的对象被销毁,并有一段测试保证所有内存已释放。这就是检测内存泄漏的方法。注意内存泄漏的检测在任何时候都是大批量的进行,包括测试构建和产品构建过程中。每当一个开发者运行任意单个测试脚本,内存泄漏检测就会被激活。因此开发过程中引入的内存泄漏能够迅速地被检测到并修复。
SQLite使用一个特殊的、能模拟内存失败的内存分配器覆盖层来测试内存不足(OOM)的错误。覆盖层被插入到内存分配器和SQLite内核之间,它直接传递内存分配请求到底层的分配器,并把结果传回到请求者。覆盖层可设置让第N次内存分配失败。为了运行OOM测试,覆盖层首先设置第一次分配失败,并运行一些测试脚本以验证分配被正确捕捉和处理。然后覆盖层设置第二次分配失败并重复运行测试。失败点继续一次通知一个分配,直到整个测试过程完成并且没有出现内存分配错误。这样的完整测试流程运行两遍,第一遍覆盖层只设置第N次分配失败,第二遍覆盖层设置第N次和后续的分配失败。注意即使遇到OOM覆盖层,内存泄漏检测逻辑也继续工作,以验证SQLite即使遇到内存分配错误也不会泄漏内存。OOM覆盖层能与任何底层内存分配器一起工作,包括检测内存分配器(这时可验证OOM错误不会引入其他的内存使用错误)。
检测内存分配器和内存泄漏检测逻辑工作在整个SQLite测试套件上。其中TCL测试套件提供99%的语句测试覆盖,TH3测试提供100%的分支测试覆盖以保证无内存泄漏。因此SQLite的动态内存分配器可以在各种场合下正确地工作。
3、配置
3.1 可选的底层内存分配器
SQLite源代码包含几种不同的内存分配器模块,可以在编译时选择,或在启动时作为一个有限的扩展。
(1)缺省内存分配器
缺省情况下,SQLite使用C标准库中的malloc()、realloc()和free()例程来分配内存。实现中还对这些例程做一层薄的包装以提供一个memsize()函数返回一个现存分配的大小。memsize()也能精确地跟踪未归还内存的字节数,它能确定当一个分配被释放时有多少字节从未归还的内存中移除。缺省内存分配器中的memsize()实现是在每个malloc()请求上多分配额外8个字节作为头部,并把分配的大小保存到这个8字节的头部。
在大多数应用程序中我们都建议使用缺省内存分配器,如果没有使用可选内存分配器的强制性需求,使用缺省的即可。
(2)调试内存分配器
如果SQLite使用SQLITE_MEMDEBUG选项编译时,会使用一个不同的对系统malloc()、realloc()和free()进行重型包装的内存分配器。重型包装器对每个分配请求多分配100字节的额外空间,用来在分配的末尾放置哨兵值。当一个分配被释放时,检查这些哨兵值以确保SQLite内核没有超出缓冲区的两端。当系统库来自GLIBC时,重型包装器也会使用GNU backtrace()函数来检查栈,并记录malloc()调用的祖先函数。当运行测试套件时,重型包装器还会记录当前测试用例的名称。这两个特性对跟踪内存泄漏是非常有用的。
重型包装器只用于SQLite的测试、分析和调试。它有显著的性能和内存开销,一般不用在最终产品中。
(3)零