【前言】

這是一次難得的辦案經驗。

上週處理 Out Of Memory 茶包在爬文時查到好幾篇文章提到 「.NET 32bit 程式的可用記憶體上限是 800M」,與我所知的 1.6G 明顯不符,但官方文件卻指證歷歷,在我心中成為不解之謎。

想挖掘真相,但發現自己根基不穩技能不足,第一次為了破案去學新技能,逼自己搞懂 .NET 記憶體原理 (副產品便是前幾天的 .NET 記憶體管理探索系列文)。

累積到足夠知識,奇妙的事發生了,原本似懂非懂的文章變得通透清晰,也學會怎麼設計實驗,謎團就這麼毫無懸念地解開了,很棒的體驗。(雖然答案超沒營養的,哈)

我終於一償宿願,解開 800M 之謎。人證、物證俱全,鐵證如山,全案偵結。

【本文開始】

上週的非典型 Out Of Memory 茶包,問題程式在 800MB 時出現 Out Of Memory Exception (以下簡稱 OOM),經過但依過去處理 .NET 記憶體不足案例的經驗,32bit .NET 程式可用記憶體上限約為 1.6 ~ 1.7GB。在搞懂 .NET 記憶體管理之後,知道 LOH 碎片化及閒置保留空間確實能造成用不到 1.6G,算是有了合理解釋。但扯出的案外案卻成為不解之謎,但爬文過程找到及網友提供的微軟文章,有多篇鐵錚錚寫著「.NET 32bit 程式只能用到 800MB」的結論:

將上述 800MB 上限文章的解釋歸納如下:

不管實體記憶體有多大,32Bit 作業系統只能定址 4GB 虛擬定址空間,而其中 2GB 要保留給作業系統(Kernel Mode Memory),故每個 32Bit 程序能用的空間(User Mode Memory)剩下 2GB (註:透過一些方法可擴大到 3GB)。

當應用程式需要記憶體,會先保留一段虛擬記憶體位址再取用 (先 Reserve 再 Commit,註:可參考 為什麼程式爆記憶體,用工作管理員卻看不出來?),這工作由 .NET Framework GC (Garbage Collector) 在需要 Managed Heap 記憶體時執行。GC 會配置 64MB 區段作為 Small Object Heap (85000 以下的小物件),大型物件則配置 16MB Large Object Heap,且必須是 2GB 空間中的連續區塊(Contiguous Blocks),當 GC 無法取得連續區塊,即會拋出 System.OutOfMemoryException (OOM) 例外。 來源

程序有許多地方需用虛擬定址空間,例如:

  • Dll
  • Native Heap (non .net heaps)
  • Thread (每個 Tread 需保留 1 MB Stack Memory)
  • .NET Heap (儲存 Managed 變數)
  • .NET Loader Heap (組件及相關 Structture)
  • COM 元件所配置的虛擬空間

配置虛擬記憶體時,未必是(也很少是)一段連續空間,例如:有些 DLL 有偏好的載入位址,故會留下間隙;記憶體用完歸還也會留下空洞,當可用記憶體過於破碎時,便會可能因為無法取得一塊夠大的連續記憶體而出現 OOM。(註:可參考記憶體還剩很多為什麼 Out Of Memory?)

在 .NET 1.1,.NET Heap 的建立單位為 64MB (如果 CPU 小於 8 顆且未手動調整的話),這 64MB 必須是完整一段區段,若記憶體記憶體空間過於破碎,擠不出一段 64MB 時,就算還有幾百 MB,也會發生 OOM。在高度使用記憶體的情境下,由 Performance Monitor 可觀察到 Virtual Bytes (Reserved Memory) 大於 Private Bytes (Commited Memory),當 Virtual Bytes 到 1.2 ~ 1.4 GB 時,Virtual Bytes 與 Private Bytes 的差距可能大到 500MB,再加上 DLL 及 COM 耗用空間,就可能因為擠不出 64MB 連續空間開始發生 OOM 例外,這就是 800MB 的由來。(1.4G - 500MB - DLL/COM 用量) 來源
[註:這段讓我有些狐疑,跟我所知的 SOH 64M、LOH 16M 有點出入]

以前幾篇文章的知識為基礎,我能完全讀懂上述論點(雖然不同文章的說法有所出入),但即便有 LOH 碎片化及 Reserve 空間未充分利用因素,上限數字會因個案而異,怎麼都不該有「實務上最多只能用到 800M」的通則。

BizTalk 記憶體抓漏文有張圖表,提到作業系統是 32 或 64 也有關係,32 位元程式在 32 位元作業系統定址空間只到 2G,實務可用上限是 800M。近十年來我接觸的 OS 清一色都是 64 位元,會不會文章說的 800M 只發生在 32 位元 Windows,我後來用的都是 64 位元 Windows,所以遇到的上限是 1.6G 而非 800M?

我做了有點瘋狂的事,在 2021 年新裝一台 32 位元 Windows XP VM。

你知道 2021 年新裝 XP 有多尷尬嗎?幾乎沒法上網,IE 也不能下載 Chrome,因為全天下網站他X的都改用 https 了,而活化石級的 XP/IE6 不支援新版 TLS,寸步難行,故要下載安裝任何東西,得在 Windows 10 抓好再 Copy 進去。

前幾天文章寫的吃 RAM 程式 Build 成 .NET 2.0 送進去執行。我失望了! 還是吃到 1.6G。所以 32bit OS 並不是造成 800M 天花板的關鍵。

再推敲了一下,我想到另一種可能,這幾篇文章年代都偏久遠,2005 年、BizTalk 2009,尤其 64MB Heap 那段還提到 .NET 1.1,該不會 800M 是 1.1 時代的事?

為了滿足好奇心,我又花了功夫在 XP 裝了 .NET Framework 1.1/2.0 SDK,將程式改寫成 1.1 可以跑的版本(沒泛型可用了,得回頭用 ArrayList),在 XP 上用 csc.exe 編譯:

using System;
using System.Collections;
using System.Runtime;
using System.Runtime.InteropServices;

namespace MemTest
{
    class Program
    {
        static void Main(string[] args)
        {
			ArrayList list = new ArrayList();
			int i = 1;
			try
			{
				while (true)
				{
					list.Add(new byte[2 * 1024 * 1024]);
					i++;
				}
			}
			catch (Exception ex)
			{
				DumpMemSize(i + "*2M byte[]");
				Console.WriteLine("ERROR-" + ex.Message);
			}
            Console.ReadLine();
        }

        static void DumpMemSize(string msg)
        {
            long memSz = GC.GetTotalMemory(false) / 1024 / 1024;
            Console.WriteLine("Managed Heap = " + memSz + "MB " + msg);
        }
	}
}

用 Notepad 敲程式碼,分別用 2.0 及 1.1 .NET Framework SDK csc.exe 編譯成 Program-2.exe、Program-11.exe。

薑薑薑薑~~ 真相大白! 一模一樣的程式碼,用 2.0 編譯執行執行結果為 1.6G、用 1.1 編譯只能用到 854M,重現文章所說的 800M 上限。

實驗驗證完,想起前幾天 FB 上 Dollen Kuo Jerry 同學還有分享一段疑似 MSDN 文件的 800M 說明:

This limit is important to adjust when your server has 4 GB or more of RAM. The 60 percent default memory limit means that the worker process is allocated 2.4 GB of RAM, which is larger than the default virtual address space for a process (2 GB). This disparity increases the likelihood of causing an OutOfMemoryException. To avoid this situation in .NET Framework 1.0, you should set the limit to the smaller of 800 MB or 60 percent of physical RAM. .NET Framework 1.1 supports a virtual space of 3 GB. If you put a /3GB switch in boot.ini, you can safely use 1,800 MB as an upper bound for the memory limit.

追查到文件源頭 - ASP.NET 1.1 Performance Guidelines - Caching,斗大標題寫著「ASP.NET 1.1 效能指引」,800M 上限再次跟 .NET 1.1 發生連結! 若早點追進這篇,我會更早想到 .NET 1.1 少繞點圈子,但挖到這個深度,加上這陣子累積一堆相關知識,我還是會選擇把實驗做完,親眼見證吧! 會少花時間,但不會少花功夫。

人證、物證俱全,鐵證如山,全案偵結。

Study of the ysterious 800M limit of 32bit .NET program.


Comments

# by Nelson Yuan

好文有點感動,這行為是不是太CSI,我被fed up滿滿黑暗feel的一集

# by Huang

還好在.net 1.1後就變成都市傳說了XD

Post a comment