存管理:GC原理與診斷實(shí)戰(zhàn)指南)
簡(jiǎn)介《.NET內(nèi)存寶典》是一本系統(tǒng)講解.NET內(nèi)存管理與性能調(diào)優(yōu)的技術(shù)專著比《CLR via C#》在內(nèi)存機(jī)制上更為聚焦深入適合中高級(jí).NET開發(fā)者作為進(jìn)階與案頭參考書研讀。書中重點(diǎn)講解垃圾回收、代際模型、對(duì)象分配與生命周期、內(nèi)存碎片成因并結(jié)合實(shí)際案例演示PerfView等診斷工具的使用方法幫助定位內(nèi)存泄漏和性能瓶頸。同時(shí)覆蓋非托管資源釋放、對(duì)象池化、裝箱與拆箱開銷以及多線程與異步編程下的內(nèi)存可見性內(nèi)容系統(tǒng)且貼近實(shí)戰(zhàn)對(duì)排查高內(nèi)存占用與垃圾回收停頓很有幫助。資源為1個(gè)PDF電子書壓縮包約24.27MB章節(jié)安排由淺入深適合離線閱讀和反復(fù)查閱。已有381人學(xué)習(xí)下載無論是夯實(shí)內(nèi)存知識(shí)體系還是優(yōu)化服務(wù)端高并發(fā)應(yīng)用的性能與可擴(kuò)展性都能從中獲得系統(tǒng)的診斷思路與優(yōu)化方案。1. 為什么內(nèi)存管理值得單開一本書不只是《CLR via C#》順帶講的那一章線上服務(wù)內(nèi)存三天漲了1.8GB業(yè)務(wù)量紋絲不動(dòng)GC每秒觸發(fā)幾十次CPU卻一直滿負(fù)荷……這種問題本質(zhì)上是 .NET 內(nèi)存管理的功課沒補(bǔ)上遲早會(huì)遇到不如一次解決?!禤ro .NET Memory Management》國(guó)內(nèi)常叫 .NET 內(nèi)存寶典就是為這件事寫的。很多人拿它和《CLR via C#》比CLR 是一張全景地圖內(nèi)存只是眾多章節(jié)之一這本從硬件緩存、虛擬內(nèi)存講起一路深入到 GC 內(nèi)部和診斷工具全程只圍繞內(nèi)存一件事。適合被 OOM 和 GC 抖動(dòng)折磨過的后端與桌面開發(fā)者也適合面試前突擊內(nèi)存知識(shí)的人。讀之前你最好寫過一年左右 C#知道 class 和 struct 的基礎(chǔ)差異。沒寫過也能看但只看理論不寫代碼效果會(huì)打折扣。2. 先把地基打牢寄存器機(jī)、緩存與虛擬內(nèi)存如何決定托管代碼性能2.1 棧、堆與寄存器機(jī)對(duì)象分配的真實(shí)路徑在 CLR 里JIT 會(huì)把中間語(yǔ)言編譯成機(jī)器碼最終在 CPU 寄存器里求值。CLR 不是棧機(jī)而是寄存器機(jī)。JVM 的字節(jié)碼執(zhí)行模型是棧機(jī)操作數(shù)都?jí)涸诓僮鲾?shù)棧上CLR 編譯過后絕大多數(shù)算術(shù)直接在寄存器里完成。這個(gè)差異直接決定值類型的去向。值類型只要不逃逸出當(dāng)前方法就可以完全在棧上分配不碰托管堆一旦裝箱、被全局引用或作為對(duì)象字段存在它才被挪到堆上。理解這一點(diǎn)就不會(huì)寫出無意義的小對(duì)象堆化。下面這段代碼展示了對(duì)象分配的真實(shí)路徑struct Point { public int X; public int Y; } class Rectangle { public Point Location; public int Width; public int Height; } static long Demo() { var rect new Rectangle(); rect.Location new Point { X 3, Y 4 }; return rect.Location.X rect.Location.Y; }這里rect變量本身在棧幀里Rectangle對(duì)象在托管堆上Point作為Rectangle的字段嵌入在堆對(duì)象內(nèi)部沒有單獨(dú)產(chǎn)生一次分配。把Point賦給object變量或轉(zhuǎn)成某個(gè)接口時(shí)裝箱才會(huì)把它的副本丟到堆上。在頻繁調(diào)用的熱路徑上每多一次裝箱就多一次堆分配。所以這本書開頭花大篇幅講棧與堆不是基礎(chǔ)科普而是后續(xù)討論 GC 壓力的前提。2.2 順序訪問優(yōu)于隨機(jī)訪問緩存局部性的差距不止一個(gè)數(shù)量級(jí)第二個(gè)基礎(chǔ)是 CPU 緩存。從內(nèi)存讀一個(gè)對(duì)象大約 60 到 100 納秒從 L1 緩存讀只要 1 納秒上下差距不止一個(gè)數(shù)量級(jí)。程序員的直覺經(jīng)常低估這個(gè)差距只看算法復(fù)雜度 O(n)卻不看緩存命中率。存儲(chǔ)層級(jí)典型延遲典型容量L1 Cache約 1ns32KB 左右L2 Cache約 4ns256KB 到 1MBL3 Cache約 10ns8MB 以上主內(nèi)存60 到 100ns按機(jī)器配置數(shù)組與鏈表的遍歷就能看出差距// 順序遍歷數(shù)組預(yù)取器能按順序把數(shù)據(jù)拉進(jìn)緩存 long SumArray(int[] arr) { long acc 0; for (int i 0; i arr.Length; i) { acc arr[i]; } return acc; } // 遍歷鏈表節(jié)點(diǎn)可能散布在整個(gè)堆的不同頁(yè)里 long SumList(Node? head) { long acc 0; for (Node? n head; n ! null; n n.Next) { acc n.Value; } return acc; }同樣數(shù)量節(jié)點(diǎn)鏈表隨機(jī)跳指針會(huì)頻繁打新緩存行數(shù)據(jù)量越大差距越明顯。這也是為什么ListT多數(shù)時(shí)候比LinkedListT快一個(gè)是連續(xù)內(nèi)存順序訪問一個(gè)是不連續(xù)內(nèi)存隨機(jī)訪問。寫代碼時(shí)先想清楚節(jié)點(diǎn)內(nèi)存分布再談算法復(fù)雜度。2.3 操作系統(tǒng)與虛擬內(nèi)存大頁(yè)面、NUMA 和不同平臺(tái)的內(nèi)存布局再往下一層是操作系統(tǒng)。進(jìn)程拿到的地址是虛擬地址操作系統(tǒng)分頁(yè)管理通過頁(yè)表翻譯成物理地址。TLB 是頁(yè)表的快速緩存直接決定每次內(nèi)存訪問的成本。默認(rèn) 4KB 一頁(yè)程序活動(dòng)內(nèi)存一大TLB 頻繁失效啟用大頁(yè)面后一頁(yè)覆蓋 2MB 甚至更大TLB 條目不用增加覆蓋范圍翻了幾十倍。Linux 上透明大頁(yè)的開關(guān)常見位置是/sys/kernel/mm/transparent_hugepage/enabled# 查看當(dāng)前狀態(tài) cat /sys/kernel/mm/transparent_hugepage/enabled # 切換為 always生產(chǎn)環(huán)境務(wù)必先做性能對(duì)比 echo always /sys/kernel/mm/transparent_hugepage/enabled設(shè)置項(xiàng)有三個(gè)always對(duì)所有內(nèi)存分配生效madvise只對(duì)顯式聲明了大頁(yè)的內(nèi)存生效never完全關(guān)閉。這個(gè)開關(guān)影響的是頁(yè)面大小和分配對(duì)齊不是內(nèi)存總量。需要注意透明大頁(yè)不是免費(fèi)午餐某些數(shù)據(jù)庫(kù)和高并發(fā)服務(wù)在always模式下反而出現(xiàn)延遲抖動(dòng)所以我一般先在測(cè)試環(huán)境開起來壓一晚上確認(rèn)穩(wěn)定再上生產(chǎn)。NUMA 關(guān)注的是跨 CPU 訪問遠(yuǎn)端內(nèi)存的延遲差異。線程從自己所在的 Node 訪問本地內(nèi)存最快跨 Node 訪問要加一次延遲。.NET Server GC 默認(rèn)按 CPU 組建堆配置項(xiàng)里有HeapCount、NoAffinitize亂調(diào)前先看機(jī)器拓?fù)洹3R娮龇ㄊ窍葓?zhí)行l(wèi)scpu看一眼 Node 拆分再?zèng)Q定要不要開相關(guān)配置。Windows 和 Linux 的進(jìn)程內(nèi)存布局不一樣棧、堆、模塊映射的位置都不同但開發(fā)者感知最深的是同一個(gè)現(xiàn)象虛擬內(nèi)存大不代表物理內(nèi)存忙。任務(wù)管理器里的工作集和GC.GetTotalMemory是兩個(gè)口徑書的工具篇章就是專門解決這種看走眼的問題。3. 沒有度量就沒有優(yōu)化PerfView、dotnet-dump 與 perfcollect 的實(shí)操路徑書里反復(fù)強(qiáng)調(diào) Measure Early盡早度量用數(shù)據(jù)而不是感覺決策。常見誤區(qū)是發(fā)現(xiàn)內(nèi)存大就直接加內(nèi)存或者手工調(diào)用 GC連基本計(jì)數(shù)器都沒看過。工具先到哪后到哪直接決定排查效率。工具作用適合場(chǎng)景dotnet-counters實(shí)時(shí)看 GC 與內(nèi)存計(jì)數(shù)器線上服務(wù)初步判斷整體趨勢(shì)PerfView / ETW追蹤 GC 事件與分配詳情定位 GC 頻率與觸發(fā)原因dotnet-dump抓進(jìn)程快照做離線分析分析堆里對(duì)象分布與根引用perfcollectLinux 下抓取 GC 等核心事件容器與 Linux 環(huán)境VMMap看進(jìn)程虛擬內(nèi)存分布Windows 下確認(rèn)虛擬內(nèi)存增長(zhǎng)3.1 先看計(jì)數(shù)器再看事件最后才看 dump不要上來就抓 dump。dump 一個(gè)運(yùn)行一小時(shí)的進(jìn)程再分析半天往往只看到結(jié)果看不到過程。正確順序是先用計(jì)數(shù)器確認(rèn)是持續(xù)增長(zhǎng)還是正常波動(dòng)再用事件看 GC 觸發(fā)頻率與原因確定有嫌疑對(duì)象再做 dump。dotnet-counters 會(huì)連接運(yùn)行中的 .NET 進(jìn)程讀取運(yùn)行庫(kù)的 EventCounter 數(shù)據(jù)。命令示例dotnet-counters monitor --process-id 10086 --counters System.Runtime--process-id指定 PID我一般先在ps -ef輸出里確認(rèn)進(jìn)程號(hào)再執(zhí)行--counters System.Runtime表示訂閱運(yùn)行庫(kù)提供的那組計(jì)數(shù)器。最常看的是這幾個(gè)gc-heap-size托管堆當(dāng)前大小。gen-0/1/2-gc-count各級(jí)別 GC 次數(shù)。cpu-usage進(jìn)程實(shí)時(shí) CPU。working-set物理內(nèi)存占用。如果gc-heap-size曲線平穩(wěn)但working-set一直上漲增長(zhǎng)大概率來自非托管內(nèi)存、文件映射或線程棧。別急著懷疑托管泄漏先看這些計(jì)數(shù)。3.2 Windows 下的 GC 事件PerfView 與 dotnet-dump 配合PerfView 是 Windows 上抓 GC 事件的首選。雖然它默認(rèn)開 GUI但也可以先用命令行收集減少對(duì)目標(biāo)進(jìn)程的干擾PerfView.exe /nogui /GCCollectOnly collect mytrace.etl/nogui表示收集完不打開界面/GCCollectOnly只啟用 GC 相關(guān)提供程序減少采集量收集結(jié)果落在mytrace.etl。注意全量 ETW 采集會(huì)暫停系統(tǒng)務(wù)必先在小流量窗口驗(yàn)證。收集完成后雙擊打開mytrace.etl進(jìn)入 Memory Group查看 GC Stats 和 Heap Allocations。GC Stats 會(huì)列出每次 GC 的耗時(shí)、原因和代際Heap Allocations 按調(diào)用棧聚合分配量一下就能看出哪條熱路徑在瘋搶堆內(nèi)存。抓到大堆后如果需要看對(duì)象細(xì)節(jié)用 dotnet-dumpdotnet-dump collect --process-id 10086 --output web.dmp dotnet-dump analyze web.dmpcollect做一次進(jìn)程快照analyze進(jìn)入交互式分析會(huì)話。會(huì)話里我常用兩招dumpheap -stat clrstackdumpheap -stat按類型統(tǒng)計(jì)托管堆對(duì)象數(shù)量和總大小能快速看到哪類對(duì)象占了大頭clrstack打印當(dāng)前線程的托管調(diào)用棧配合看哪個(gè)調(diào)用點(diǎn)正在分配。分析完輸入quit退出。這套組合能回答絕大多數(shù)“到底是誰(shuí)分配了這么多對(duì)象”的問題。3.3 Linux 與容器環(huán)境perfcollect 打開另一扇門生產(chǎn)容器里跑 PerfView 不方便Linux 常用開源工具是 perfcollect。它依賴內(nèi)核的 perf 事件跑起來通常需要 root 權(quán)限sudo perfcollect install sudo perfcollect collect mytraceinstall會(huì)安裝內(nèi)核符號(hào)和依賴的設(shè)備采集工具首次運(yùn)行需要預(yù)置包或能從源拉取collect mytrace開始追蹤默認(rèn)按時(shí)間窗口和內(nèi)存大小生成追蹤文件。收集結(jié)束后可以把得到的追蹤數(shù)據(jù)導(dǎo)入 Trace Compass 打開或者轉(zhuǎn)換后交給熟悉的分析環(huán)境。跑容器的同事最關(guān)心“采集過程會(huì)影響業(yè)務(wù)嗎”——perfcollect 采集時(shí)對(duì)目標(biāo)進(jìn)程是只讀式采樣影響比 ETW 全量追蹤小但內(nèi)核側(cè)符號(hào)解析仍要占用系統(tǒng)資源。我會(huì)先在預(yù)發(fā)容器跑一次完整采集確認(rèn)沒有明顯延遲抖動(dòng)再上生產(chǎn)。在拿到第一份數(shù)據(jù)之前不要下任何“內(nèi)存泄漏”的結(jié)論。這是整本書診斷章節(jié)最想糾正的習(xí)慣。4. GC 不是黑匣子分代、觸發(fā)點(diǎn)與運(yùn)行時(shí)配置參數(shù)打開 System.Runtime 計(jì)數(shù)器經(jīng)常看到 Gen0 一分鐘幾十次Gen2 半個(gè)鐘不來一次。這不是異常分代回收本來就這么設(shè)計(jì)的。真正要看的是 Gen2 與gc-heap-size的配合Gen2 頻繁說明跨代際存活對(duì)象多或者 LOH 出問題。先讀懂這三個(gè)數(shù)字再動(dòng)配置。4.1 分代與堆分區(qū)為什么 Gen0 天天忙、Gen2 難得清靜.NET GC 用的是分代追蹤回收。新對(duì)象進(jìn)第 0 代活過一次回收后晉升到第 1 代再活過一次到第 2 代。代齡越大回收頻率越低Gen0 回收非常頻繁Gen2 可能幾分鐘甚至幾小時(shí)才來一次。兩個(gè)沒有代際概念的區(qū)域是 LOHLarge Object Heap和 POHPinned Object Heap。LOH 的對(duì)象默認(rèn)超過 85KB 就進(jìn)回收后不壓縮容易碎片化POH 是 .NET Core 3.0 之后引入的固定對(duì)象單獨(dú)放避免固定對(duì)象把普通小對(duì)象堆的壓縮過程攪亂。代碼里很容易觀察到這個(gè)分界byte[] small new byte[80_000]; // SOH進(jìn) Gen0 byte[] big new byte[100_000]; // LOH超過 85KB 閾值80KB的數(shù)組走小對(duì)象堆參與代際晉升100KB的數(shù)組直接進(jìn) LOH回收代價(jià)按大對(duì)象算而且默認(rèn)不壓縮。如果面試或面試別人把“大對(duì)象堆不壓縮導(dǎo)致碎片”這個(gè)問題答清楚比背十個(gè)配置名有用得多。4.2 GC 模式與運(yùn)行時(shí)開關(guān)Server、Concurrent、HeapHardLimitGC 有兩種基本模式Workstation GC工作站和 Server GC服務(wù)器。工作站 GC 多用于桌面程序回收在觸發(fā)它的線程上做服務(wù)器 GC 為每個(gè)邏輯 CPU 核心建一個(gè)托管堆與一個(gè) GC 線程吞吐率高但線程數(shù)也多小容器里容易過度配置。與模式并列的是并發(fā)還是后臺(tái)回收。舊式并發(fā)回收有暫停窗口后臺(tái) GC 在后臺(tái)線程執(zhí)行部分回收主線程繼續(xù)跑業(yè)務(wù)。默認(rèn)配置在多數(shù)服務(wù)器上是 Server 后臺(tái)這些在 runtimeconfig.json 里都能改{ runtimeOptions: { configProperties: { System.GC.Server: true, System.GC.Concurrent: true, System.GC.HeapHardLimit: 1073741824, System.GC.HeapHardLimitPercent: 3, System.GC.LOHThreshold: 102400 } } }配置項(xiàng)作用常見取值System.GC.Server開啟服務(wù)器 GCtrue / falseSystem.GC.Concurrent后臺(tái)回收true / falseSystem.GC.HeapHardLimit托管堆硬上限字節(jié)數(shù)System.GC.HeapHardLimitPercent按進(jìn)程內(nèi)存百分比限制0 到 100System.GC.LOHThresholdLOH 閾值字節(jié)數(shù)這套配置不是越多越好。我見過把 HeapHardLimit 設(shè)太小的服務(wù)GC 被壓得頻繁阻塞吞吐反而掉一半。調(diào)參必須跟指標(biāo)走先看默認(rèn)配置下的 GC 暫停和堆增長(zhǎng)再?zèng)Q定要不要限制。4.3 觸發(fā)條件的三個(gè)來源分配、內(nèi)存壓力、顯式調(diào)用GC 觸發(fā)有三個(gè)主要來源。第一是分配壓力比如 Gen0 滿了第二是系統(tǒng)內(nèi)存壓力高第三是代碼顯式調(diào)用GC.Collect()。顯式調(diào)用不是完全不能碰但絕大多數(shù)業(yè)務(wù)代碼里輪不到你替 GC 做決定。需要它的時(shí)候是進(jìn)程即將進(jìn)入對(duì)延遲敏感的空閑窗口而且你確認(rèn)堆里確實(shí)有大量一次性質(zhì)對(duì)象否則不要碰。注意GC.Collect屬于高危操作默認(rèn)不要出現(xiàn)在業(yè)務(wù)代碼里。理解觸發(fā)點(diǎn)是為了讀dotnet-counters里的gen-0/1/2-gc-count如果 Gen2 次數(shù)高得離譜優(yōu)先檢查 LOH 分配和進(jìn)程內(nèi)存壓力如果 Gen0 次數(shù)極密說明熱路徑上存在大量臨時(shí)對(duì)象該找分配熱點(diǎn)而不是加內(nèi)存。下一章的避坑記錄全部來自這些場(chǎng)景里栽過的跟頭。5. 避坑手冊(cè)五個(gè)把我坑過一遍的內(nèi)存陷阱避坑不是看二手經(jīng)驗(yàn)就能學(xué)會(huì)的這里記錄的五條都來自我的實(shí)際處理經(jīng)歷每條按現(xiàn)象、原因、解決三步說清方便照做。5.1 現(xiàn)象內(nèi)存只漲不降怎么看都像泄漏現(xiàn)象working-set三天漲了 1.5GB抓 dump 找不到大對(duì)象托管堆大小也只是略有增加。原因非托管資源沒有被釋放。文件句柄、socket、Marshal.AllocHGlobal分配的內(nèi)存或者某個(gè)第三方組件在后臺(tái)保持著一塊非托管緩存。托管堆再干凈進(jìn)程內(nèi)存也會(huì)繼續(xù)漲。解決先用dotnet-counters分開兩類指標(biāo)。gc-heap-size平穩(wěn)、working-set上漲時(shí)用系統(tǒng)工具看VmRSS同時(shí)檢查句柄和 GDI 對(duì)象數(shù)量長(zhǎng)期做法是給非托管資源統(tǒng)一走IDisposableusing并單獨(dú)記錄打開后必須關(guān)閉。5.2 現(xiàn)象開了 Server GC 后反而更頻繁卡頓現(xiàn)象換到多核機(jī)器后把System.GC.Server打開GC 次數(shù)沒少服務(wù)延遲反而上升。原因容器 CPU 限制下Server GC 按可見邏輯核心數(shù)建堆線程切換和堆同步開銷把收益吃掉了。一個(gè)小容器拿到 32 核的配置創(chuàng)建幾十個(gè) GC 線程光調(diào)度就夠喝一壺。解決在容器里先看ProcessorCount實(shí)際暴露了幾核再?zèng)Q定是否啟用 Server GC。更常見的做法是保留默認(rèn) Workstation 后臺(tái)回收或者用System.GC.HeapCount把堆數(shù)限制到容器允許的核心數(shù)。調(diào)完后用 GC 暫停指標(biāo)反向驗(yàn)證別按“核心多就該 Server”的經(jīng)驗(yàn)辦事。5.3 現(xiàn)象StringBuilder 已經(jīng)用了內(nèi)存還是在漲現(xiàn)象日志拼接都改成了StringBuildergen-0-gc-count沒有明顯下降。原因StringBuilder.ToString()每次都會(huì)產(chǎn)出一個(gè)新字符串如果日志模板里還有參數(shù)插值$在編譯后會(huì)生成string.Format或默認(rèn)插值處理器最終仍有點(diǎn)臨時(shí)分配。StringBuilder 只是降低了拼接次數(shù)沒有消滅最終的字符串對(duì)象。解決把日志模板緩存為常量枚舉值預(yù)處理成字符串再拼接高 QPS 場(chǎng)景給字符緩沖走對(duì)象池而不是反復(fù)new StringBuilder。這一條靠 GC 分配統(tǒng)計(jì)確認(rèn)光看代碼看不出門道。5.4 現(xiàn)象PerfView 一開服務(wù)立刻抖動(dòng)現(xiàn)象只在高峰期抓 60 秒 ETW服務(wù) P99 直接翻倍業(yè)務(wù)方開始打電話。原因全量事件追蹤會(huì)讓系統(tǒng)短暫停頓GC 本身又疊加了額外事件度量過程影響了被測(cè)系統(tǒng)。書中對(duì)采集手段的侵入性有過明確提醒。解決先在低峰期測(cè)一次/GCCollectOnly的暫停影響如果必須高峰采集縮小采集范圍只開 GC 事件關(guān)掉 Context Switch 等重型提供程序或者退而求其次用dotnet-counters的采樣模式。5.5 現(xiàn)象Dispose 都寫了內(nèi)存還沒立刻下來現(xiàn)象代碼里所有實(shí)現(xiàn)IDisposable的對(duì)象都套了using但GC.GetTotalMemory升高的勢(shì)頭沒停。原因Dispose只釋放非托管資源它不回收托管堆內(nèi)存。托管對(duì)象要等 GC 回收終結(jié)器型的資源還要額外一輪。把“釋放”和“回收”混成一個(gè)概念是剛接觸內(nèi)存管理最容易翻車的地方。解決區(qū)分兩類資源。非托管資源用Dispose保證確定性釋放托管堆大對(duì)象走池化或數(shù)組租用降低 GC 壓力。驗(yàn)證方式不是看GetTotalMemory而是看gen-0-gc-count是否下降。6. 把書用到日常寫代碼前先建一個(gè)分配基線書讀完了最該帶走的是“先度量再優(yōu)化”的肌肉記憶。6.1 一條可復(fù)現(xiàn)的驗(yàn)證路徑我現(xiàn)在的習(xí)慣是凡是要進(jìn)熱路徑的函數(shù)先寫一段幾十毫秒的基準(zhǔn)代碼用GC.GetAllocatedBytesForCurrentThread前后對(duì)比。long before GC.GetAllocatedBytesForCurrentThread(); for (int i 0; i 100_000; i) { string id string.Format(ID-{0:D6}, i); // 優(yōu)化前每次循環(huán)都有臨時(shí)分配 } long allocated GC.GetAllocatedBytesForCurrentThread() - before;GC.GetAllocatedBytesForCurrentThread返回當(dāng)前線程累計(jì)分配量適合做相對(duì)比較配合dotnet-counters看gen-0-gc-count能確認(rèn)優(yōu)化是否真的減少了 GC 壓力。string.Format的形式在十萬(wàn)次循環(huán)里會(huì)制造十萬(wàn)個(gè)中間字符串這就是 Gen0 壓力來源。6.2 從“減少分配”到“減少 GC 壓力”換成數(shù)組租用后分配量會(huì)立刻下來char[] buffer ArrayPoolchar.Shared.Rent(256); try { int len FormatId(42, buffer); string id new string(buffer, 0, len); // 只在最后產(chǎn)生一個(gè)字符串 } finally { ArrayPoolchar.Shared.Return(buffer); } static int FormatId(int id, char[] dst) { dst[0] I; dst[1] D; dst[2] -; dst[3] (char)(0 (id % 10)); dst[4] (char)(0 (id / 10 % 10)); dst[5] (char)(0 (id / 100 % 10)); return 6; }Rent從共享池取緩沖Return歸還池內(nèi)部復(fù)用避免每次循環(huán)都向堆申請(qǐng)新數(shù)組。改動(dòng)看起來小熱路徑上省掉的卻是大量 Gen0 分配和后續(xù)回收入口。從那以后我每次寫批量處理代碼都強(qiáng)制先跑一遍分配基線用數(shù)據(jù)說話再談優(yōu)化。多花十分鐘能省掉線上加內(nèi)存的尷尬。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取