最近遇見了個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++實非我所長,所以,這一次就到此為止吧。
