cpu 和垃圾回收的關(guān)系
前言
前文中介紹了,cpu 使用率和三個(gè)方面有關(guān),一個(gè)是中斷、一個(gè)是用戶(hù)時(shí)間、一個(gè)是系統(tǒng)時(shí)間。
那么當(dāng)我們排查問(wèn)題時(shí)候,查到是用戶(hù)時(shí)間占用比較多的話,那么該如何排查呢?
正文
首先我們想到的是,cpu高嘛,查看火焰圖對(duì)吧。 火焰圖可以看到哪個(gè)函數(shù)消耗cpu比較多?
似乎這個(gè)問(wèn)題好像馬上能查看到,如果火焰圖調(diào)入gc函數(shù)消耗的cpu比較多的話,那么就直接定位了。
然而事情好像沒(méi)有這么簡(jiǎn)單。
這個(gè)我們來(lái)看一下火焰圖原理:
火焰圖是一種性能分析工具,它通過(guò)采樣CPU調(diào)用棧來(lái)生成可視化的性能分析圖表。火焰圖能夠顯示:
1.CPU時(shí)間消耗最多的函數(shù)調(diào)用路徑
2. 調(diào)用棧的深度和寬度
3. 熱點(diǎn)代碼的位置
GC在火焰圖中的表現(xiàn)
- GC線程活動(dòng):如果GC線程占用大量CPU時(shí)間,會(huì)在火焰圖中顯示為GC相關(guān)的調(diào)用棧
- GC暫停時(shí)間:雖然火焰圖主要顯示CPU時(shí)間,但GC暫停期間的CPU使用也會(huì)被記錄
- 內(nèi)存分配熱點(diǎn):頻繁的內(nèi)存分配會(huì)導(dǎo)致GC壓力,這些分配點(diǎn)會(huì)在火焰圖中顯示
可能觀察不到的情況:
- 后臺(tái)GC:.NET的Server GC模式使用后臺(tái)線程進(jìn)行垃圾回收,這些線程的活動(dòng)可能不會(huì)在火焰圖中明顯顯示
- GC暫停:GC暫停期間應(yīng)用程序線程被掛起,火焰圖可能顯示為"空閑"時(shí)間
也就是說(shuō)火焰圖可能能看出一些gc問(wèn)題,但是有些可能不明顯。
那么從簡(jiǎn)單的說(shuō),內(nèi)存分配熱點(diǎn),也就是說(shuō)內(nèi)存頻繁的分配,那么可以查看出。
這個(gè)其實(shí)占用cpu的話,經(jīng)過(guò)測(cè)試,這個(gè)其實(shí)一般來(lái)說(shuō)占用cpu不會(huì)太多,很難看出來(lái)。
比如說(shuō):
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
namespace RealAllocationHotspot
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Real Memory Allocation Hotspot Demo");
Console.WriteLine("This will show actual allocation hotspots in flame graphs");
var cts = new CancellationTokenSource();
var tasks = new List<Task>();
// 任務(wù)1:CPU密集型的分配熱點(diǎn)
tasks.Add(Task.Run(() => CpuIntensiveAllocation(cts.Token)));
// 任務(wù)2:字符串操作熱點(diǎn)
tasks.Add(Task.Run(() => StringAllocationHotspot(cts.Token)));
// 任務(wù)3:集合操作熱點(diǎn)
tasks.Add(Task.Run(() => CollectionAllocationHotspot(cts.Token)));
// 任務(wù)4:復(fù)雜對(duì)象分配熱點(diǎn)
tasks.Add(Task.Run(() => ComplexObjectAllocation(cts.Token)));
Console.WriteLine("Press any key to stop...");
Console.ReadKey();
cts.Cancel();
Task.WaitAll(tasks.ToArray());
}
// CPU密集型的分配熱點(diǎn) - 會(huì)在火焰圖中顯示
static void CpuIntensiveAllocation(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// 這些操作會(huì)消耗大量CPU時(shí)間
for (int i = 0; i < 10000; i++)
{
// 熱點(diǎn)1:大量字符串操作
var result = "";
for (int j = 0; j < 100; j++)
{
result += $"String_{i}_{j}_"; // 字符串連接
result += DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
}
// 熱點(diǎn)2:字符串格式化
var formatted = string.Format("Complex string: {0}, {1}, {2}, {3}, {4}",
Guid.NewGuid(), DateTime.Now, Environment.TickCount,
i);
// 熱點(diǎn)3:數(shù)組操作
var array = new byte[1000];
for (int k = 0; k < array.Length; k++)
{
array[k] = (byte)(i + k);
}
}
}
}
// 字符串分配熱點(diǎn) - 會(huì)顯示為CPU熱點(diǎn)
static void StringAllocationHotspot(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// 這些操作會(huì)消耗CPU時(shí)間進(jìn)行字符串處理
for (int i = 0; i < 5000; i++)
{
// 熱點(diǎn):字符串連接操作
var sb = new StringBuilder();
for (int j = 0; j < 50; j++)
{
sb.Append($"String_{i}_{j}_");
sb.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
sb.Append("_");
}
var result = sb.ToString();
// 熱點(diǎn):字符串分割和重組
var parts = result.Split('_');
var reconstructed = string.Join("|", parts);
}
}
}
// 集合分配熱點(diǎn) - 會(huì)顯示為CPU熱點(diǎn)
static void CollectionAllocationHotspot(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// 這些操作會(huì)消耗CPU時(shí)間進(jìn)行集合操作
for (int i = 0; i < 1000; i++)
{
// 熱點(diǎn):列表操作
var list = new List<string>();
for (int j = 0; j < 100; j++)
{
list.Add($"Item_{i}_{j}");
}
// 熱點(diǎn):字典操作
var dict = new Dictionary<string, object>();
for (int j = 0; j < 50; j++)
{
dict[$"Key_{i}_{j}"] = new byte[100];
}
// 熱點(diǎn):集合操作
var set = new HashSet<string>();
for (int j = 0; j < 100; j++)
{
set.Add($"SetItem_{i}_{j}");
}
}
}
}
// 復(fù)雜對(duì)象分配熱點(diǎn) - 會(huì)顯示為CPU熱點(diǎn)
static void ComplexObjectAllocation(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// 這些操作會(huì)消耗CPU時(shí)間進(jìn)行對(duì)象創(chuàng)建和初始化
for (int i = 0; i < 1000; i++)
{
// 熱點(diǎn):復(fù)雜對(duì)象創(chuàng)建
var complexObject = new ComplexObject
{
Id = i,
Name = $"ComplexObject_{i}",
Data = new byte[500],
Tags = new List<string> { "tag1", "tag2", "tag3" },
Properties = new Dictionary<string, object>
{
["prop1"] = Guid.NewGuid(),
["prop2"] = DateTime.Now,
["prop3"] = Environment.TickCount
}
};
// 熱點(diǎn):對(duì)象序列化/反序列化模擬
var serialized = complexObject.ToString();
var deserialized = ComplexObject.FromString(serialized);
}
}
}
}
// 復(fù)雜對(duì)象 - 用于演示分配熱點(diǎn)
class ComplexObject
{
public int Id { get; set; }
public string Name { get; set; }
public byte[] Data { get; set; }
public List<string> Tags { get; set; }
public Dictionary<string, object> Properties { get; set; }
public override string ToString()
{
return $"ComplexObject{{Id={Id}, Name={Name}, DataLength={Data?.Length ?? 0}}}";
}
public static ComplexObject FromString(string str)
{
// 模擬反序列化
return new ComplexObject { Id = 0, Name = str };
}
}
}
那么來(lái)看一下cpu火焰圖:

可以看到一個(gè)datetime的now,后面都是大量的擴(kuò)容創(chuàng)建對(duì)象啥的。
GC暫停時(shí)間:雖然火焰圖主要顯示CPU時(shí)間,但GC暫停期間的CPU使用也會(huì)被記錄。
這個(gè)怎么說(shuō)呢?
// .NET中有兩種GC模式:
// 1. 工作站GC (Workstation GC) - 前臺(tái)GC,會(huì)暫停所有線程
// 2. 服務(wù)器GC (Server GC) - 后臺(tái)GC,使用專(zhuān)用線程
然后這兩種工作狀態(tài)呢?
// GC暫停期間,應(yīng)用程序線程的狀態(tài):
// 1. 前臺(tái)GC:所有線程被掛起
// 2. 后臺(tái)GC:應(yīng)用程序線程繼續(xù)運(yùn)行,GC線程在后臺(tái)工作
這里說(shuō)的是gc暫停時(shí)間呢?說(shuō)的是前臺(tái)gc,所有的線程被掛起的情況。
前臺(tái)gc暫停:
明顯可見(jiàn):前臺(tái)GC會(huì)暫停所有用戶(hù)線程,在火焰圖中表現(xiàn)為明顯的"熱點(diǎn)"
具體表現(xiàn):
GC.Collect() 調(diào)用會(huì)顯示為CPU熱點(diǎn)
GC.WaitForPendingFinalizers() 會(huì)顯示為等待時(shí)間
用戶(hù)線程在GC期間會(huì)顯示為 WaitHandle.WaitOne 或 Thread.Sleep
GC.Collect() 和 GC.WaitForPendingFinalizers() 是gc回收的函數(shù)。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace GCPauseFlameDemo
{
/// <summary>
/// 演示GC暫停在火焰圖中的不同表現(xiàn)
/// </summary>
public class GCFlameGraphDemo
{
public static void RunDemo()
{
Console.WriteLine("=== GC暫停在火焰圖中的表現(xiàn)演示 ===");
Console.WriteLine();
Console.WriteLine("請(qǐng)選擇測(cè)試模式:");
Console.WriteLine("1. 前臺(tái)GC演示 - 會(huì)明顯顯示在火焰圖中");
Console.WriteLine("2. 后臺(tái)GC演示 - 可能不明顯");
Console.WriteLine("3. 業(yè)務(wù)邏輯演示 - 在GC暫停期間會(huì)受到影響");
Console.WriteLine("4. 密集GC演示 - 專(zhuān)門(mén)設(shè)計(jì)用于在火焰圖中產(chǎn)生明顯熱點(diǎn)");
Console.WriteLine("5. 超激進(jìn)GC演示 - 嘗試在用戶(hù)線程上產(chǎn)生明顯的GC相關(guān)CPU使用");
Console.WriteLine("6. GC活動(dòng)診斷 - 監(jiān)控GC相關(guān)指標(biāo)并解釋火焰圖表現(xiàn)");
Console.WriteLine("7. 混合模式 - 所有任務(wù)同時(shí)運(yùn)行(用于對(duì)比)");
Console.WriteLine("8. 分階段測(cè)試 - 依次測(cè)試每種模式");
Console.WriteLine();
Console.Write("請(qǐng)輸入選擇 (1-8): ");
var choice = Console.ReadLine();
switch (choice)
{
case "1":
RunForegroundGCDemo();
break;
case "2":
RunBackgroundGCDemo();
break;
case "3":
RunBusinessLogicDemo();
break;
case "4":
RunIntensiveGCDemo();
break;
case "5":
RunUltraIntensiveGCDemo();
break;
case "6":
RunDiagnoseGCActivity();
break;
case "7":
RunMixedDemo();
break;
case "8":
RunStagedDemo();
break;
default:
Console.WriteLine("無(wú)效選擇,運(yùn)行混合模式演示...");
RunMixedDemo();
break;
}
}
/// <summary>
/// 演示前臺(tái)GC - 在火焰圖中會(huì)顯示為明顯的熱點(diǎn)
/// </summary>
static void DemonstrateForegroundGC(CancellationToken token)
{
Console.WriteLine("前臺(tái)GC演示開(kāi)始 - 這些操作會(huì)在火焰圖中顯示為熱點(diǎn)");
while (!token.IsCancellationRequested)
{
// 大量分配內(nèi)存,確保觸發(fā)GC
var objects = new List<object>();
for (int i = 0; i < 200000; i++) // 增加分配量
{
objects.Add(new byte[2000]); // 分配400MB
}
// 強(qiáng)制觸發(fā)前臺(tái)GC - 這會(huì)在火焰圖中顯示為CPU熱點(diǎn)
var sw = Stopwatch.StartNew();
// 多次調(diào)用GC,確保在火焰圖中可見(jiàn)
for (int i = 0; i < 5; i++) // 連續(xù)調(diào)用5次
{
GC.Collect(0, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 短暫休眠,讓采樣器有機(jī)會(huì)捕獲
Thread.Sleep(10);
}
sw.Stop();
Console.WriteLine($"[前臺(tái)GC] 強(qiáng)制GC耗時(shí): {sw.ElapsedMilliseconds}ms");
// 減少休眠時(shí)間,增加GC調(diào)用頻率
Thread.Sleep(200);
}
}
/// <summary>
/// 演示后臺(tái)GC - 在火焰圖中可能不明顯
/// </summary>
static void DemonstrateBackgroundGC(CancellationToken token)
{
Console.WriteLine("后臺(tái)GC演示開(kāi)始 - 這些操作在火焰圖中可能不明顯");
while (!token.IsCancellationRequested)
{
// 大量分配,讓后臺(tái)GC自然觸發(fā)
var objects = new List<object>();
for (int i = 0; i < 50000; i++)
{
objects.Add(new byte[2000]); // 分配100MB
}
// 不強(qiáng)制GC,讓后臺(tái)GC線程自然工作
// 后臺(tái)GC線程的工作可能不會(huì)在火焰圖中明顯顯示
Thread.Sleep(300);
}
}
/// <summary>
/// 演示業(yè)務(wù)邏輯在GC暫停期間的表現(xiàn)
/// </summary>
static void DemonstrateBusinessLogic(CancellationToken token)
{
Console.WriteLine("業(yè)務(wù)邏輯演示開(kāi)始 - 在GC暫停期間會(huì)受到影響");
while (!token.IsCancellationRequested)
{
// 模擬正常的業(yè)務(wù)邏輯
var sw = Stopwatch.StartNew();
// 密集計(jì)算
for (int i = 0; i < 10000; i++)
{
var result = Math.Sqrt(i) * Math.PI;
}
sw.Stop();
// 如果業(yè)務(wù)邏輯在GC暫停期間執(zhí)行,可能會(huì)顯示為:
// - Thread.Sleep (等待GC完成)
// - WaitHandle.WaitOne (等待GC完成)
// - 執(zhí)行時(shí)間異常長(zhǎng)
Console.WriteLine($"[業(yè)務(wù)邏輯] 計(jì)算耗時(shí): {sw.ElapsedMilliseconds}ms");
Thread.Sleep(100);
}
}
/// <summary>
/// 專(zhuān)門(mén)用于在火焰圖中產(chǎn)生明顯GC熱點(diǎn)的方法
/// </summary>
static void DemonstrateIntensiveGC(CancellationToken token)
{
Console.WriteLine("密集GC演示開(kāi)始 - 專(zhuān)門(mén)設(shè)計(jì)用于在火焰圖中產(chǎn)生明顯熱點(diǎn)");
Console.WriteLine("注意:GC工作主要在專(zhuān)用線程上,可能不會(huì)在用戶(hù)線程的火焰圖中顯示");
while (!token.IsCancellationRequested)
{
// 創(chuàng)建大量對(duì)象,確保觸發(fā)GC
var objects = new List<object>();
for (int i = 0; i < 500000; i++) // 大量分配
{
objects.Add(new byte[1000]); // 分配500MB
}
// 密集調(diào)用GC,確保在火焰圖中可見(jiàn)
var sw = Stopwatch.StartNew();
// 連續(xù)多次GC,產(chǎn)生明顯的CPU熱點(diǎn)
for (int j = 0; j < 10; j++) // 10次循環(huán)
{
// 強(qiáng)制Gen0 GC
GC.Collect(0, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 強(qiáng)制Gen1 GC
GC.Collect(1, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 強(qiáng)制Gen2 GC(最耗時(shí))
GC.Collect(2, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 短暫休眠,讓采樣器捕獲
Thread.Sleep(5);
}
sw.Stop();
Console.WriteLine($"[密集GC] 強(qiáng)制GC耗時(shí): {sw.ElapsedMilliseconds}ms");
// 短暫休眠后繼續(xù)
Thread.Sleep(100);
}
}
/// <summary>
/// 超激進(jìn)GC測(cè)試 - 嘗試在用戶(hù)線程上產(chǎn)生明顯的GC相關(guān)CPU使用
/// </summary>
static void DemonstrateUltraIntensiveGC(CancellationToken token)
{
Console.WriteLine("超激進(jìn)GC演示開(kāi)始 - 嘗試在用戶(hù)線程上產(chǎn)生明顯的GC相關(guān)CPU使用");
Console.WriteLine("這個(gè)測(cè)試會(huì)嘗試在用戶(hù)線程上執(zhí)行GC相關(guān)工作");
while (!token.IsCancellationRequested)
{
// 創(chuàng)建大量對(duì)象
var objects = new List<object>();
for (int i = 0; i < 1000000; i++) // 1M個(gè)對(duì)象
{
objects.Add(new byte[1000]); // 分配1GB
}
var sw = Stopwatch.StartNew();
// 在用戶(hù)線程上執(zhí)行密集的GC相關(guān)操作
for (int j = 0; j < 20; j++) // 20次循環(huán)
{
// 連續(xù)調(diào)用GC,嘗試在用戶(hù)線程上產(chǎn)生CPU熱點(diǎn)
for (int k = 0; k < 5; k++)
{
GC.Collect(0, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 在用戶(hù)線程上執(zhí)行一些計(jì)算,確保CPU使用
for (int l = 0; l < 10000; l++)
{
var result = Math.Sqrt(l) * Math.PI;
}
}
// 強(qiáng)制Gen1和Gen2 GC
GC.Collect(1, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
GC.Collect(2, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 在用戶(hù)線程上執(zhí)行更多計(jì)算
for (int l = 0; l < 5000; l++)
{
var result = Math.Sin(l) * Math.Cos(l);
}
}
sw.Stop();
Console.WriteLine($"[超激進(jìn)GC] 強(qiáng)制GC耗時(shí): {sw.ElapsedMilliseconds}ms");
Console.WriteLine($"當(dāng)前內(nèi)存使用: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
Thread.Sleep(50);
}
}
/// <summary>
/// 診斷GC活動(dòng)的方法
/// </summary>
static void DiagnoseGCActivity(CancellationToken token)
{
Console.WriteLine("GC活動(dòng)診斷開(kāi)始 - 監(jiān)控GC相關(guān)指標(biāo)");
var lastGen0 = GC.CollectionCount(0);
var lastGen1 = GC.CollectionCount(1);
var lastGen2 = GC.CollectionCount(2);
var lastMemory = GC.GetTotalMemory(false);
while (!token.IsCancellationRequested)
{
var currentGen0 = GC.CollectionCount(0);
var currentGen1 = GC.CollectionCount(1);
var currentGen2 = GC.CollectionCount(2);
var currentMemory = GC.GetTotalMemory(false);
if (currentGen0 > lastGen0 || currentGen1 > lastGen1 || currentGen2 > lastGen2)
{
Console.WriteLine($"\n=== GC事件檢測(cè)到 [{DateTime.Now:HH:mm:ss.fff}] ===");
Console.WriteLine($"Gen0: {currentGen0 - lastGen0} 次");
Console.WriteLine($"Gen1: {currentGen1 - lastGen1} 次");
Console.WriteLine($"Gen2: {currentGen2 - lastGen2} 次");
Console.WriteLine($"內(nèi)存變化: {(currentMemory - lastMemory) / 1024 / 1024} MB");
Console.WriteLine($"總內(nèi)存: {currentMemory / 1024 / 1024} MB");
// 解釋為什么GC可能不在火焰圖中顯示
Console.WriteLine("?? 火焰圖分析:");
Console.WriteLine(" - GC.Collect() 調(diào)用會(huì)在火焰圖中顯示");
Console.WriteLine(" - GC.WaitForPendingFinalizers() 會(huì)顯示為等待");
Console.WriteLine(" - 但GC的實(shí)際工作在專(zhuān)用線程上,可能不顯示");
Console.WriteLine(" - 用戶(hù)線程的等待會(huì)顯示為 Thread.Sleep 或 WaitHandle.WaitOne");
}
lastGen0 = currentGen0;
lastGen1 = currentGen1;
lastGen2 = currentGen2;
lastMemory = currentMemory;
Thread.Sleep(500);
}
}
/// <summary>
/// 監(jiān)控GC活動(dòng)并解釋火焰圖表現(xiàn)
/// </summary>
static void MonitorGCWithFlameGraph(CancellationToken token)
{
var lastGen0 = GC.CollectionCount(0);
var lastGen1 = GC.CollectionCount(1);
var lastGen2 = GC.CollectionCount(2);
Console.WriteLine("GC監(jiān)控開(kāi)始 - 觀察火焰圖中的表現(xiàn)");
while (!token.IsCancellationRequested)
{
var currentGen0 = GC.CollectionCount(0);
var currentGen1 = GC.CollectionCount(1);
var currentGen2 = GC.CollectionCount(2);
if (currentGen0 > lastGen0 || currentGen1 > lastGen1 || currentGen2 > lastGen2)
{
Console.WriteLine($"\n=== GC事件檢測(cè)到 [{DateTime.Now:HH:mm:ss.fff}] ===");
Console.WriteLine($"Gen0: {currentGen0 - lastGen0} 次");
Console.WriteLine($"Gen1: {currentGen1 - lastGen1} 次");
Console.WriteLine($"Gen2: {currentGen2 - lastGen2} 次");
Console.WriteLine($"總內(nèi)存: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
// 解釋火焰圖中的表現(xiàn)
if (currentGen2 > lastGen2)
{
Console.WriteLine("?? 火焰圖表現(xiàn): Gen2 GC會(huì)在火焰圖中顯示為明顯的CPU熱點(diǎn)");
Console.WriteLine(" 用戶(hù)線程會(huì)顯示為 WaitHandle.WaitOne 或 Thread.Sleep");
}
else if (currentGen1 > lastGen1)
{
Console.WriteLine("?? 火焰圖表現(xiàn): Gen1 GC可能顯示為中等程度的CPU使用");
}
else
{
Console.WriteLine("?? 火焰圖表現(xiàn): Gen0 GC可能顯示為輕微的CPU使用");
}
}
lastGen0 = currentGen0;
lastGen1 = currentGen1;
lastGen2 = currentGen2;
Thread.Sleep(1000);
}
}
/// <summary>
/// 單獨(dú)測(cè)試前臺(tái)GC
/// </summary>
static void RunForegroundGCDemo()
{
Console.WriteLine("\n=== 前臺(tái)GC演示 ===");
Console.WriteLine("前臺(tái)GC會(huì)在火焰圖中顯示為明顯的CPU熱點(diǎn)");
Console.WriteLine("按任意鍵開(kāi)始,再次按任意鍵停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateForegroundGC(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 單獨(dú)測(cè)試后臺(tái)GC
/// </summary>
static void RunBackgroundGCDemo()
{
Console.WriteLine("\n=== 后臺(tái)GC演示 ===");
Console.WriteLine("后臺(tái)GC在火焰圖中可能不明顯");
Console.WriteLine("按任意鍵開(kāi)始,再次按任意鍵停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateBackgroundGC(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 單獨(dú)測(cè)試業(yè)務(wù)邏輯
/// </summary>
static void RunBusinessLogicDemo()
{
Console.WriteLine("\n=== 業(yè)務(wù)邏輯演示 ===");
Console.WriteLine("業(yè)務(wù)邏輯在GC暫停期間會(huì)受到影響");
Console.WriteLine("按任意鍵開(kāi)始,再次按任意鍵停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateBusinessLogic(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 混合模式 - 所有任務(wù)同時(shí)運(yùn)行
/// </summary>
static void RunMixedDemo()
{
Console.WriteLine("\n=== 混合模式演示 ===");
Console.WriteLine("所有任務(wù)同時(shí)運(yùn)行,用于對(duì)比不同GC類(lèi)型的影響");
Console.WriteLine("按任意鍵開(kāi)始,再次按任意鍵停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var tasks = new List<Task>();
tasks.Add(Task.Run(() => DemonstrateForegroundGC(cts.Token)));
tasks.Add(Task.Run(() => DemonstrateBackgroundGC(cts.Token)));
tasks.Add(Task.Run(() => DemonstrateBusinessLogic(cts.Token)));
tasks.Add(Task.Run(() => MonitorGCWithFlameGraph(cts.Token)));
Console.ReadKey();
cts.Cancel();
Task.WaitAll(tasks.ToArray());
}
/// <summary>
/// 分階段測(cè)試 - 依次測(cè)試每種模式
/// </summary>
static void RunStagedDemo()
{
Console.WriteLine("\n=== 分階段測(cè)試 ===");
Console.WriteLine("將依次測(cè)試每種GC類(lèi)型,每次測(cè)試30秒");
// 階段1:前臺(tái)GC
Console.WriteLine("\n--- 階段1:前臺(tái)GC測(cè)試 (30秒) ---");
Console.WriteLine("前臺(tái)GC會(huì)在火焰圖中顯示為明顯的CPU熱點(diǎn)");
Console.WriteLine("按任意鍵開(kāi)始...");
Console.ReadKey();
var cts1 = new CancellationTokenSource();
var task1 = Task.Run(() => DemonstrateForegroundGC(cts1.Token));
Thread.Sleep(30000); // 30秒
cts1.Cancel();
task1.Wait();
// 階段2:后臺(tái)GC
Console.WriteLine("\n--- 階段2:后臺(tái)GC測(cè)試 (30秒) ---");
Console.WriteLine("后臺(tái)GC在火焰圖中可能不明顯");
Console.WriteLine("按任意鍵開(kāi)始...");
Console.ReadKey();
var cts2 = new CancellationTokenSource();
var task2 = Task.Run(() => DemonstrateBackgroundGC(cts2.Token));
Thread.Sleep(30000); // 30秒
cts2.Cancel();
task2.Wait();
// 階段3:業(yè)務(wù)邏輯
Console.WriteLine("\n--- 階段3:業(yè)務(wù)邏輯測(cè)試 (30秒) ---");
Console.WriteLine("業(yè)務(wù)邏輯在GC暫停期間會(huì)受到影響");
Console.WriteLine("按任意鍵開(kāi)始...");
Console.ReadKey();
var cts3 = new CancellationTokenSource();
var task3 = Task.Run(() => DemonstrateBusinessLogic(cts3.Token));
Thread.Sleep(30000); // 30秒
cts3.Cancel();
task3.Wait();
Console.WriteLine("\n=== 分階段測(cè)試完成 ===");
Console.WriteLine("現(xiàn)在可以分析每個(gè)階段的火焰圖表現(xiàn)");
}
/// <summary>
/// 單獨(dú)測(cè)試密集GC
/// </summary>
static void RunIntensiveGCDemo()
{
Console.WriteLine("\n=== 密集GC演示 ===");
Console.WriteLine("專(zhuān)門(mén)設(shè)計(jì)用于在火焰圖中產(chǎn)生明顯的GC熱點(diǎn)");
Console.WriteLine("這個(gè)演示會(huì)頻繁調(diào)用GC,確保在火焰圖中可見(jiàn)");
Console.WriteLine("按任意鍵開(kāi)始,再次按任意鍵停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateIntensiveGC(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 單獨(dú)測(cè)試超激進(jìn)GC
/// </summary>
static void RunUltraIntensiveGCDemo()
{
Console.WriteLine("\n=== 超激進(jìn)GC演示 ===");
Console.WriteLine("嘗試在用戶(hù)線程上產(chǎn)生明顯的GC相關(guān)CPU使用");
Console.WriteLine("這個(gè)測(cè)試會(huì)在用戶(hù)線程上執(zhí)行密集的GC相關(guān)操作");
Console.WriteLine("按任意鍵開(kāi)始,再次按任意鍵停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateUltraIntensiveGC(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 運(yùn)行GC活動(dòng)診斷
/// </summary>
static void RunDiagnoseGCActivity()
{
Console.WriteLine("\n=== GC活動(dòng)診斷 ===");
Console.WriteLine("監(jiān)控GC相關(guān)指標(biāo)并解釋火焰圖表現(xiàn)");
Console.WriteLine("這個(gè)測(cè)試會(huì)解釋為什么GC可能不在火焰圖中顯示");
Console.WriteLine("按任意鍵開(kāi)始,再次按任意鍵停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DiagnoseGCActivity(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
}
}
運(yùn)行5的時(shí)候呢,大量gc的時(shí)候出現(xiàn)了gc函數(shù)的調(diào)用,但是呢,火焰圖看不到gc線程具體執(zhí)行的信息。
dotnet-trace collect -p 53632 --format speedscope --providers Microsoft-DotNETCore-SampleProfiler --duration 00:00:30 --symbols ./bin/Debug/net8.0/
根本原因分析
1. SampleProfiler 的工作原理
CPU 采樣機(jī)制
SampleProfiler 采樣原理:
├── 定期中斷用戶(hù)線程
├── 收集當(dāng)前調(diào)用棧
├── 統(tǒng)計(jì) CPU 使用時(shí)間
└── 生成火焰圖數(shù)據(jù)
對(duì) GC 的限制
GC 工作的特點(diǎn):
├── GC 工作在專(zhuān)用線程上
├── 用戶(hù)線程在 GC 期間被暫停
├── SampleProfiler 主要采樣用戶(hù)線程
└── GC 線程可能不被采樣
### 2. **為什么 SampleProfiler 抓取不到 GC**
#### 原因1:GC 線程隔離
```csharp
// GC 工作流程
用戶(hù)線程: GC.Collect() → 等待 GC 完成 → 繼續(xù)執(zhí)行
GC線程: 執(zhí)行實(shí)際的垃圾回收工作(標(biāo)記、清除、壓縮)
原因2:用戶(hù)線程在 GC 期間被暫停
// 用戶(hù)線程在 GC 期間的狀態(tài)
while (GC.IsInProgress())
{
// 用戶(hù)線程被暫停,不執(zhí)行任何代碼
// SampleProfiler 無(wú)法采樣到 GC 相關(guān)活動(dòng)
}
原因3:采樣頻率問(wèn)題
// GC 調(diào)用可能很快完成
GC.Collect(0, GCCollectionMode.Forced); // 調(diào)用本身很快
GC.WaitForPendingFinalizers(); // 等待時(shí)間可能不被采樣
那就是抓取不到了,這是個(gè)問(wèn)題。

可以看到主動(dòng)調(diào)用才抓到這么幾次,難道只消耗著一點(diǎn)cpu嗎?

那么cpu到底被gc消耗掉了還是被用戶(hù)線程消耗掉了,這個(gè)怎么判斷呢?通過(guò)我們打印gc時(shí)間還是消耗了很多cpu呢,那么怎么看呢?
那么gc 問(wèn)題就有專(zhuān)門(mén)的工具了,一個(gè)是gc-verbose,dotnet-counters.
下面來(lái)試一下dotnet-counters。
重新跑這個(gè)例子:
dotnet-counters collect --process-id 20084 --refresh-interval 5 --duration 60 --output counters.csv
然后可以看到結(jié)果,因?yàn)樽ト〉谋容^多,這里只用一個(gè)來(lái)說(shuō)明:
08/07/2025 16:32:28,System.Runtime,CPU Usage (%),Metric,0
08/07/2025 16:32:28,System.Runtime,Working Set (MB),Metric,1078.317056
08/07/2025 16:32:28,System.Runtime,GC Heap Size (MB),Metric,1032.561848
08/07/2025 16:32:28,System.Runtime,Gen 0 GC Count (Count / 5 sec),Rate,373
08/07/2025 16:32:28,System.Runtime,Gen 1 GC Count (Count / 5 sec),Rate,233
08/07/2025 16:32:28,System.Runtime,Gen 2 GC Count (Count / 5 sec),Rate,88
08/07/2025 16:32:28,System.Runtime,Gen 0 GC Budget (MB),Metric,6
08/07/2025 16:32:28,System.Runtime,ThreadPool Thread Count,Metric,2
08/07/2025 16:32:28,System.Runtime,Monitor Lock Contention Count (Count / 5 sec),Rate,1
08/07/2025 16:32:28,System.Runtime,ThreadPool Queue Length,Metric,0
08/07/2025 16:32:28,System.Runtime,ThreadPool Completed Work Item Count (Count / 5 sec),Rate,0
08/07/2025 16:32:28,System.Runtime,Allocation Rate (B / 5 sec),Rate,1040950952
08/07/2025 16:32:28,System.Runtime,Number of Active Timers,Metric,0
08/07/2025 16:32:28,System.Runtime,GC Fragmentation (%),Metric,0.8735136169672377
08/07/2025 16:32:28,System.Runtime,GC Committed Bytes (MB),Metric,1043.730432
08/07/2025 16:32:28,System.Runtime,Exception Count (Count / 5 sec),Rate,0
08/07/2025 16:32:28,System.Runtime,% Time in GC since last GC (%),Metric,99
08/07/2025 16:32:28,System.Runtime,Time paused by GC (ms / 5 sec),Rate,5683.659000000001
08/07/2025 16:32:28,System.Runtime,Gen 0 Size (B),Metric,0
08/07/2025 16:32:28,System.Runtime,Gen 1 Size (B),Metric,0
08/07/2025 16:32:28,System.Runtime,Gen 2 Size (B),Metric,1024966792
08/07/2025 16:32:28,System.Runtime,LOH Size (B),Metric,16646536
08/07/2025 16:32:28,System.Runtime,POH (Pinned Object Heap) Size (B),Metric,32712
08/07/2025 16:32:28,System.Runtime,Number of Assemblies Loaded,Metric,11
08/07/2025 16:32:28,System.Runtime,IL Bytes Jitted (B),Metric,15131
08/07/2025 16:32:28,System.Runtime,Number of Methods Jitted,Metric,153
08/07/2025 16:32:28,System.Runtime,Time spent in JIT (ms / 5 sec),Rate,1605.8994
首先看一下gc評(píng)率:
08/07/2025 16:32:28,System.Runtime,Gen 0 GC Count (Count / 5 sec),Rate,373
08/07/2025 16:32:28,System.Runtime,Gen 1 GC Count (Count / 5 sec),Rate,233
08/07/2025 16:32:28,System.Runtime,Gen 2 GC Count (Count / 5 sec),Rate,88
線上5秒,發(fā)生了很多的gc。
08/07/2025 16:32:28,System.Runtime,% Time in GC since last GC (%),Metric,99
08/07/2025 16:32:28,System.Runtime,Time paused by GC (ms / 5 sec),Rate,5683.659000000001
這里表示gc 5秒占用的時(shí)間是5683.659000000001,這個(gè)應(yīng)該是計(jì)算有點(diǎn)問(wèn)題的。
Time in GC since last GC 這個(gè)是這一次gc,占用到距離上一次gc的百分比。
TimeInGCPercent = (TotalGCPauseTime / TotalTimeSinceLastGC) * 100
高百分比(如 99%)
// 表示應(yīng)用程序幾乎一直在進(jìn)行 GC
// 應(yīng)用程序線程大部分時(shí)間被暫停
// 用戶(hù)體驗(yàn)很差,響應(yīng)性很低
低百分比(如 5%)
// 表示 GC 占用時(shí)間很少
// 應(yīng)用程序線程大部分時(shí)間在執(zhí)行業(yè)務(wù)邏輯
// 用戶(hù)體驗(yàn)良好,響應(yīng)性好
3. CPU 使用分析
- CPU Usage:7.7-8.4%(相對(duì)穩(wěn)定)
- ThreadPool Thread Count:1-2個(gè)線程
分析:CPU 使用率適中,但 GC 工作占用了大量時(shí)間。
后臺(tái) GC 指標(biāo)
- 數(shù)據(jù)中沒(méi)有顯示
gc-background-gc-count或gc-concurrent-gc-count - 所有 GC 都是阻塞性的
內(nèi)存分配
- Allocation Rate:31MB-1GB/5秒(變化很大)
- GC Heap Size:約 1032MB(相對(duì)穩(wěn)定) // 只是gc托管的資源
- Working Set:1057-1087MB(緩慢增長(zhǎng))// 程序占用系統(tǒng)全部的資源
內(nèi)存分布
- Gen 0 Size:0-11KB(大部分時(shí)間為空)
- Gen 1 Size:0-6MB(偶爾有對(duì)象)
- Gen 2 Size:約 1024MB(主要內(nèi)存占用)
- LOH Size:約 16MB(大對(duì)象堆) # 這個(gè)需要看一下,large object heap
- POH Size:32KB(固定對(duì)象堆) # pined object heap,不用管比較小
分析:大部分對(duì)象都被提升到 Gen 2,說(shuō)明應(yīng)用程序在創(chuàng)建長(zhǎng)期存活的對(duì)象。
這樣就可以大量gc問(wèn)題,還有內(nèi)存的關(guān)系了。
結(jié)
在確定是用戶(hù)時(shí)間消耗cpu的時(shí)候,需要查看dotnetcount 查看gc,和查看火焰圖一起看,這樣才是合理的,因?yàn)榛鹧鎴D比較難看到gc,尤其是專(zhuān)有g(shù)c。
浙公網(wǎng)安備 33010602011771號(hào)