問題
最近遇到朋友分享 2 個 .NET 記憶體飆高的問題。
以往最常聽到的是字串串接,例如 s += "abc";,
而最近聽到的則是 ToArray()
案例一: 高頻率解密 Connection String 導致的 GC 壓力
在許多企業級應用中,為了資安考量,資料庫連接字串(Connection String)會經過 AES 加密後存放在 appsettings.json 中。每次程式需要建立連線時,都會呼叫類似底下的方法:
1 | public static string getConnStr(string connStrName = "Default") |
初看這段程式碼,你可能會覺得:「這只是讀取一個幾百個字元的字串,能耗費多少記憶體?」
但如果我們把 getDecryptStr 內部呼叫的核心解密邏輯 Decrypt(string encryptedText) 攤開來看,就可以發現它用許多的ToArray()
1 | public string Decrypt(string encryptedText) |
系統在頻繁連線 DB 時,就會有以下問題:
- 頻繁的
ToArray()記憶體配置: 解密高頻率發生時,fullData.Take(16).ToArray()和fullData.Skip(16).ToArray()會在極短時間內在記憶體中產生海量的暫時性byte[]垃圾。 - 字串不可變性(Immutable):
string.Format與最終解密出來的連線字串,每次執行都會在 Heap 上配置新字串。 - 如果這個解密動作發生在每次資料庫請求(例如每次開新連線就解密一次),在高併發(High Concurrency)的情境下,系統會在幾毫秒內堆積出數萬個短命的陣列與字串。垃圾回收器(GC)來不及清理,你就會看到伺服器的 RAM 持續往上飆升。
加入 Cache
由於連線字串在程式啟動後幾乎固定不變,我們根本不需要讓系統反覆去踩 ToArray() 和解密的效能地雷!
另外,若要避免高併發下「同一個 key 被重複解密」,建議直接用 GetOrAdd 搭配 Lazy<string>:
1 | private static readonly ConcurrentDictionary<string, Lazy<string>> _connStrCache = |
如果無法做 Cache,至少把 Take/Skip/ToArray 改為 ReadOnlySpan<byte> 切片,減少不必要的陣列配置:
1 | public string Decrypt(string encryptedText) |
案例二:大檔案下載時 byte[] 導致的大物件堆積(LOH)
在處理檔案或 Stream 轉換的共用工具類別時,也發現了極為相似的 ToArray() 問題,
1 | // 舊的寫法:將 Stream 轉成 byte[] |
它的問題為,
- 呼叫
.ToArray()時,.NET 會在記憶體中再複製一份全新的陣列回傳。若檔案 50MB,這瞬間就需要至少 100MB 空間。 - 大物件堆積(LOH, Large Object Heap)碎片化: 在 .NET 中,大於 85,000 位元組(約 83 KB) 的物件會進入 LOH。LOH 在預設情境下通常不會頻繁壓縮(可透過設定或特定時機調整),頻繁配置大 byte[] 仍可能導致碎片化,即便用完了,RAM 也常常降不下來。
Stream 改用 RecyclableMemoryStream
在 ToArray() 的部份,如果來源是 Stream 就不再轉成 byte[],改完後,再查看使用MemoryStream的部份,改用 RecyclableMemoryStream
1 | public static class StreamPool |
不再強行轉換成 byte[],而是直接回傳由 Pool 管理的 Stream。
- 注意:這裡不能使用 using,否則 Stream 會在離開方法時被提早關閉並歸還,導致外部讀取失敗!
1 | public static Stream GetRecyclableStream(Stream inputStream) |
補充:RecyclableMemoryStream 的定位是「降低配置與 GC 壓力」,不是把超大檔案都先吃進 RAM 的萬用解法。若來源本來就是檔案或網路串流,優先考慮直接串流回傳(例如 FileStream / 直接寫入 Response Body)。
API 直接將這個 Stream 餵給 File() 回傳
1 | [] |
建議觀測指標(壓測前後可對比)
- 配置率(Allocation Rate / MB/s)
- Gen 2 GC 次數與停頓時間
- LOH 大小與成長趨勢
所以現在除了字串串接外,也要注意有沒有不必要的 ToArray()
參考資源
Windows 系統上的大型物件堆積
.NET性能优化-使用RecyclableMemoryStream替代MemoryStream