最近遇见了个OutOfMemoryException, 花了一些时间调查了一下这个问题,觉得很有意思,写出来供大家乐乐。
故事背景:
我们有一个.net程序,其功能是从一个dat文件中按行读取数据,处理后,写到一个压缩文件中去。
最早上线的时候,没有启用压缩功能,是直接输出,工作的很好;后来启用了压缩功能,也工作的很好。如果文件被正确处理了,则这些文件会被删除。
最近对于一些文件忽然报OutOfMemoryException。Tester发的bug是说对于超过1.2G 的文件处理就会报错。
调查过程:
首先,我们从EventLog上看见了这个OutOfMemoryException,但是这个log实在是记的一般,没有callStack,没有出错文件信息,只有一个Exception Message(多么典型的问题啊)。
没办法,只能从剩下的文件上挑选一些大的手动测试会不会抛出异常,找到了几个会出异常的文件。
因为OutOfMemory,首先想到的就是,那么想办法让他多一点内存用啊,为什么机器的内存有16GB呢,而application才用了2G的内存就溢出了呢,有时候2G都用不到就内存溢出了。
第一步怀疑,是因为Windows对应用程序的可用内存有限制,并可以通过运行:
|
改了,测试,不行……
那会不会是32位或者是64位程序的问题呢?如果是32位的,程序的寻址空间有限,所以outOfMemory了。
但是检查发现,程序是以AnyCPU为platform targe编译的,应该不是这个问题。
第二步怀疑,因为是最近enable了压缩,所以第一步怀疑是压缩出了问题。呵呵,我想另外一个原因是压缩是第三方的library,dev希望说问题不是出在自己的身上,所以首先就怀疑是压缩的问题。
不过,经过调查,发现在用SteamReader.ReadLine()的时候抛出的异常,还没有到压缩模块呢。所以,不是压缩的问题。
StreamReader.ReadLine()为什么会消耗那么多内存呢?我们采用的是FileStream为base stream,用StreamReader和FileStream的目标就是为了不消耗内存,可是效果没达到……
什么StreamReader.ReadLine()会消耗内存呢,一位同事认为是最近打了补丁造成的问题。我颇为怀疑。用ILSPY反编译了StreamReader,一行行读,没发现问题。
只能测试了,写了一个简单的工具,按行读取文件,然后每个10000行输出一下已经消耗的内存。发现开始一些都很美好,到最后,开始内存激增,然后爆掉。
看来是后面的数据有问题,马上一个念头出现:ReadLine读取的时候读不到换行符。所以,改用Stream的Read读取到一个Buffer里面,检测是否有\n的存在。问题开始变得清晰了:普通行的数据都是300~400Bytes左右的数据,而最后一行居然有1GB的数据!无论如何,这一定是内存爆掉的原因。
为什么后面的丢失了换行符?继续debug,使用按块读取,输出了最后几块的数据内容。发现都是0,是空块。
问题似乎清晰了…… 因为前一段时间我们的一些操作将一些数据错误的分配到一个远程的服务器上,后来发现问题,需要将数据拿回来,应该是直接用的Ctrl+C -> Ctrl+V。把数据搬过来,但是中途应该是网络出问题,造成了只copy了前面的数据,后面的都只是分配了空间但是没有赋值。
解决起来也就容易了,用robocopy将文件再copy一遍。
搞定了。
后来,我又思考了一下这个问题,为什么在一个16G的机器上,采用了2G的内存就爆掉呢?我尝试不断在heap上加载一些图片,发现分配超过4G的内存都没有问题。
应该还是StreamReader.ReadLine()这里有情况,经过仔细测试,大概可以看见两个错误类型:
如果读取一行包含3G的数据的时候会抛出:
"System.OutOfMemoryException: Insufficient memory to continue the execution of the program.\r\n at System.Text.StringBuilder.ExpandByABlock(Int32 minBlockCharCount)\r\n at System.Text.StringBuilder.Append(Char* value, Int32 valueCount)\r\n at System.Text.StringBuilder.Append(Char[] value, Int32 startIndex, Int32 charCount)\r\n at System.IO.StreamReader.ReadLine()\r\n at FileCheck.Form1.button4_Click(Object sender, EventArgs e) in d:\\V-zhsunProjects\\FileCheck\\FileCheck\\Form1.cs:line 205"
|
而读取一行包含2G的数据的时候会抛出:
System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.\r\n at System.Text.StringBuilder.ToString()\r\n at FileCheck.Form1.button4_Click(Object sender, EventArgs e) in d:\\V-zhsunProjects\\FileCheck\\FileCheck\\Form1.cs:line 205
|
对于前者很容易理解,Stringbuilder internal int m_MaxCapacity;是一个int32的值,3GB的数量已经超出了一个范围,所以,报错是必然的。
可是对于后者,就稍微有一些难理解了。为什么都读到StringBuilder里面结果ToString的时候爆了呢?
有意思。
为了进一步验证,我尝试初始化一个StringBuilder:
StringBuilder builder = new StringBuilder(2000 * 1024 * 1024); |
结果,outOfMemory, 但是如果
string subString = new string('a', 1024 * 1024); StringBuilder builder = new StringBuilder(1023 * 1024 * 1024); for (int i = 0; i < 2000; i++) { builder.Append(subString); } |
这个却又能执行成功。
更有意思了。哈哈。
注意,这里StringBuilder builder = new StringBuilder(1023 * 1024 * 1024);
如果用1024,那就会exception了。
最后,看了.net内部的代码,发现了一个新的嫌疑犯:
wstrcpy(char* dmem, char* smem, int charCount)
这个东西在构造函数和ToString()方法中都被用到过。
考虑到是wstrcpy,这大概可以解释为什么1023可以初始化,而1024就会报错了吧。
不过C++实非我所长,所以,这一次就到此为止吧。