cpu 和垃圾回收的关系
前言
前文中介绍了,cpu 使用率和三个方面有关,一个是中断、一个是用户时间、一个是系统时间。
那么当我们排查问题时候,查到是用户时间占用比较多的话,那么该如何排查呢?
正文
首先我们想到的是,cpu高嘛,查看火焰图对吧。 火焰图可以看到哪个函数消耗cpu比较多?
似乎这个问题好像马上能查看到,如果火焰图调入gc函数消耗的cpu比较多的话,那么就直接定位了。
然而事情好像没有这么简单。
这个我们来看一下火焰图原理:
火焰图是一种性能分析工具,它通过采样CPU调用栈来生成可视化的性能分析图表。火焰图能够显示:
1.CPU时间消耗最多的函数调用路径
2. 调用栈的深度和宽度
3. 热点代码的位置
GC在火焰图中的表现
- GC线程活动:如果GC线程占用大量CPU时间,会在火焰图中显示为GC相关的调用栈
- GC暂停时间:虽然火焰图主要显示CPU时间,但GC暂停期间的CPU使用也会被记录
- 内存分配热点:频繁的内存分配会导致GC压力,这些分配点会在火焰图中显示
可能观察不到的情况:
- 后台GC:.NET的Server GC模式使用后台线程进行垃圾回收,这些线程的活动可能不会在火焰图中明显显示
- GC暂停:GC暂停期间应用程序线程被挂起,火焰图可能显示为"空闲"时间
也就是说火焰图可能能看出一些gc问题,但是有些可能不明显。
那么从简单的说,内存分配热点,也就是说内存频繁的分配,那么可以查看出。
这个其实占用cpu的话,经过测试,这个其实一般来说占用cpu不会太多,很难看出来。
比如说:
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>();
// 任务1:CPU密集型的分配热点
tasks.Add(Task.Run(() => CpuIntensiveAllocation(cts.Token)));
// 任务2:字符串操作热点
tasks.Add(Task.Run(() => StringAllocationHotspot(cts.Token)));
// 任务3:集合操作热点
tasks.Add(Task.Run(() => CollectionAllocationHotspot(cts.Token)));
// 任务4:复杂对象分配热点
tasks.Add(Task.Run(() => ComplexObjectAllocation(cts.Token)));
Console.WriteLine("Press any key to stop...");
Console.ReadKey();
cts.Cancel();
Task.WaitAll(tasks.ToArray());
}
// CPU密集型的分配热点 - 会在火焰图中显示
static void CpuIntensiveAllocation(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// 这些操作会消耗大量CPU时间
for (int i = 0; i < 10000; i++)
{
// 热点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");
}
// 热点2:字符串格式化
var formatted = string.Format("Complex string: {0}, {1}, {2}, {3}, {4}",
Guid.NewGuid(), DateTime.Now, Environment.TickCount,
i);
// 热点3:数组操作
var array = new byte[1000];
for (int k = 0; k < array.Length; k++)
{
array[k] = (byte)(i + k);
}
}
}
}
// 字符串分配热点 - 会显示为CPU热点
static void StringAllocationHotspot(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// 这些操作会消耗CPU时间进行字符串处理
for (int i = 0; i < 5000; i++)
{
// 热点:字符串连接操作
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();
// 热点:字符串分割和重组
var parts = result.Split('_');
var reconstructed = string.Join("|", parts);
}
}
}
// 集合分配热点 - 会显示为CPU热点
static void CollectionAllocationHotspot(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// 这些操作会消耗CPU时间进行集合操作
for (int i = 0; i < 1000; i++)
{
// 热点:列表操作
var list = new List<string>();
for (int j = 0; j < 100; j++)
{
list.Add($"Item_{i}_{j}");
}
// 热点:字典操作
var dict = new Dictionary<string, object>();
for (int j = 0; j < 50; j++)
{
dict[$"Key_{i}_{j}"] = new byte[100];
}
// 热点:集合操作
var set = new HashSet<string>();
for (int j = 0; j < 100; j++)
{
set.Add($"SetItem_{i}_{j}");
}
}
}
}
// 复杂对象分配热点 - 会显示为CPU热点
static void ComplexObjectAllocation(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// 这些操作会消耗CPU时间进行对象创建和初始化
for (int i = 0; i < 1000; i++)
{
// 热点:复杂对象创建
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
}
};
// 热点:对象序列化/反序列化模拟
var serialized = complexObject.ToString();
var deserialized = ComplexObject.FromString(serialized);
}
}
}
}
// 复杂对象 - 用于演示分配热点
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 };
}
}
}
那么来看一下cpu火焰图:

可以看到一个datetime的now,后面都是大量的扩容创建对象啥的。
GC暂停时间:虽然火焰图主要显示CPU时间,但GC暂停期间的CPU使用也会被记录。
这个怎么说呢?
// .NET中有两种GC模式:
// 1. 工作站GC (Workstation GC) - 前台GC,会暂停所有线程
// 2. 服务器GC (Server GC) - 后台GC,使用专用线程
然后这两种工作状态呢?
// GC暂停期间,应用程序线程的状态:
// 1. 前台GC:所有线程被挂起
// 2. 后台GC:应用程序线程继续运行,GC线程在后台工作
这里说的是gc暂停时间呢?说的是前台gc,所有的线程被挂起的情况。
前台gc暂停:
明显可见:前台GC会暂停所有用户线程,在火焰图中表现为明显的"热点"
具体表现:
GC.Collect() 调用会显示为CPU热点
GC.WaitForPendingFinalizers() 会显示为等待时间
用户线程在GC期间会显示为 WaitHandle.WaitOne 或 Thread.Sleep
GC.Collect() 和 GC.WaitForPendingFinalizers() 是gc回收的函数。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace GCPauseFlameDemo
{
/// <summary>
/// 演示GC暂停在火焰图中的不同表现
/// </summary>
public class GCFlameGraphDemo
{
public static void RunDemo()
{
Console.WriteLine("=== GC暂停在火焰图中的表现演示 ===");
Console.WriteLine();
Console.WriteLine("请选择测试模式:");
Console.WriteLine("1. 前台GC演示 - 会明显显示在火焰图中");
Console.WriteLine("2. 后台GC演示 - 可能不明显");
Console.WriteLine("3. 业务逻辑演示 - 在GC暂停期间会受到影响");
Console.WriteLine("4. 密集GC演示 - 专门设计用于在火焰图中产生明显热点");
Console.WriteLine("5. 超激进GC演示 - 尝试在用户线程上产生明显的GC相关CPU使用");
Console.WriteLine("6. GC活动诊断 - 监控GC相关指标并解释火焰图表现");
Console.WriteLine("7. 混合模式 - 所有任务同时运行(用于对比)");
Console.WriteLine("8. 分阶段测试 - 依次测试每种模式");
Console.WriteLine();
Console.Write("请输入选择 (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("无效选择,运行混合模式演示...");
RunMixedDemo();
break;
}
}
/// <summary>
/// 演示前台GC - 在火焰图中会显示为明显的热点
/// </summary>
static void DemonstrateForegroundGC(CancellationToken token)
{
Console.WriteLine("前台GC演示开始 - 这些操作会在火焰图中显示为热点");
while (!token.IsCancellationRequested)
{
// 大量分配内存,确保触发GC
var objects = new List<object>();
for (int i = 0; i < 200000; i++) // 增加分配量
{
objects.Add(new byte[2000]); // 分配400MB
}
// 强制触发前台GC - 这会在火焰图中显示为CPU热点
var sw = Stopwatch.StartNew();
// 多次调用GC,确保在火焰图中可见
for (int i = 0; i < 5; i++) // 连续调用5次
{
GC.Collect(0, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 短暂休眠,让采样器有机会捕获
Thread.Sleep(10);
}
sw.Stop();
Console.WriteLine($"[前台GC] 强制GC耗时: {sw.ElapsedMilliseconds}ms");
// 减少休眠时间,增加GC调用频率
Thread.Sleep(200);
}
}
/// <summary>
/// 演示后台GC - 在火焰图中可能不明显
/// </summary>
static void DemonstrateBackgroundGC(CancellationToken token)
{
Console.WriteLine("后台GC演示开始 - 这些操作在火焰图中可能不明显");
while (!token.IsCancellationRequested)
{
// 大量分配,让后台GC自然触发
var objects = new List<object>();
for (int i = 0; i < 50000; i++)
{
objects.Add(new byte[2000]); // 分配100MB
}
// 不强制GC,让后台GC线程自然工作
// 后台GC线程的工作可能不会在火焰图中明显显示
Thread.Sleep(300);
}
}
/// <summary>
/// 演示业务逻辑在GC暂停期间的表现
/// </summary>
static void DemonstrateBusinessLogic(CancellationToken token)
{
Console.WriteLine("业务逻辑演示开始 - 在GC暂停期间会受到影响");
while (!token.IsCancellationRequested)
{
// 模拟正常的业务逻辑
var sw = Stopwatch.StartNew();
// 密集计算
for (int i = 0; i < 10000; i++)
{
var result = Math.Sqrt(i) * Math.PI;
}
sw.Stop();
// 如果业务逻辑在GC暂停期间执行,可能会显示为:
// - Thread.Sleep (等待GC完成)
// - WaitHandle.WaitOne (等待GC完成)
// - 执行时间异常长
Console.WriteLine($"[业务逻辑] 计算耗时: {sw.ElapsedMilliseconds}ms");
Thread.Sleep(100);
}
}
/// <summary>
/// 专门用于在火焰图中产生明显GC热点的方法
/// </summary>
static void DemonstrateIntensiveGC(CancellationToken token)
{
Console.WriteLine("密集GC演示开始 - 专门设计用于在火焰图中产生明显热点");
Console.WriteLine("注意:GC工作主要在专用线程上,可能不会在用户线程的火焰图中显示");
while (!token.IsCancellationRequested)
{
// 创建大量对象,确保触发GC
var objects = new List<object>();
for (int i = 0; i < 500000; i++) // 大量分配
{
objects.Add(new byte[1000]); // 分配500MB
}
// 密集调用GC,确保在火焰图中可见
var sw = Stopwatch.StartNew();
// 连续多次GC,产生明显的CPU热点
for (int j = 0; j < 10; j++) // 10次循环
{
// 强制Gen0 GC
GC.Collect(0, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 强制Gen1 GC
GC.Collect(1, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 强制Gen2 GC(最耗时)
GC.Collect(2, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 短暂休眠,让采样器捕获
Thread.Sleep(5);
}
sw.Stop();
Console.WriteLine($"[密集GC] 强制GC耗时: {sw.ElapsedMilliseconds}ms");
// 短暂休眠后继续
Thread.Sleep(100);
}
}
/// <summary>
/// 超激进GC测试 - 尝试在用户线程上产生明显的GC相关CPU使用
/// </summary>
static void DemonstrateUltraIntensiveGC(CancellationToken token)
{
Console.WriteLine("超激进GC演示开始 - 尝试在用户线程上产生明显的GC相关CPU使用");
Console.WriteLine("这个测试会尝试在用户线程上执行GC相关工作");
while (!token.IsCancellationRequested)
{
// 创建大量对象
var objects = new List<object>();
for (int i = 0; i < 1000000; i++) // 1M个对象
{
objects.Add(new byte[1000]); // 分配1GB
}
var sw = Stopwatch.StartNew();
// 在用户线程上执行密集的GC相关操作
for (int j = 0; j < 20; j++) // 20次循环
{
// 连续调用GC,尝试在用户线程上产生CPU热点
for (int k = 0; k < 5; k++)
{
GC.Collect(0, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 在用户线程上执行一些计算,确保CPU使用
for (int l = 0; l < 10000; l++)
{
var result = Math.Sqrt(l) * Math.PI;
}
}
// 强制Gen1和Gen2 GC
GC.Collect(1, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
GC.Collect(2, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// 在用户线程上执行更多计算
for (int l = 0; l < 5000; l++)
{
var result = Math.Sin(l) * Math.Cos(l);
}
}
sw.Stop();
Console.WriteLine($"[超激进GC] 强制GC耗时: {sw.ElapsedMilliseconds}ms");
Console.WriteLine($"当前内存使用: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
Thread.Sleep(50);
}
}
/// <summary>
/// 诊断GC活动的方法
/// </summary>
static void DiagnoseGCActivity(CancellationToken token)
{
Console.WriteLine("GC活动诊断开始 - 监控GC相关指标");
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事件检测到 [{DateTime.Now:HH:mm:ss.fff}] ===");
Console.WriteLine($"Gen0: {currentGen0 - lastGen0} 次");
Console.WriteLine($"Gen1: {currentGen1 - lastGen1} 次");
Console.WriteLine($"Gen2: {currentGen2 - lastGen2} 次");
Console.WriteLine($"内存变化: {(currentMemory - lastMemory) / 1024 / 1024} MB");
Console.WriteLine($"总内存: {currentMemory / 1024 / 1024} MB");
// 解释为什么GC可能不在火焰图中显示
Console.WriteLine("🔥 火焰图分析:");
Console.WriteLine(" - GC.Collect() 调用会在火焰图中显示");
Console.WriteLine(" - GC.WaitForPendingFinalizers() 会显示为等待");
Console.WriteLine(" - 但GC的实际工作在专用线程上,可能不显示");
Console.WriteLine(" - 用户线程的等待会显示为 Thread.Sleep 或 WaitHandle.WaitOne");
}
lastGen0 = currentGen0;
lastGen1 = currentGen1;
lastGen2 = currentGen2;
lastMemory = currentMemory;
Thread.Sleep(500);
}
}
/// <summary>
/// 监控GC活动并解释火焰图表现
/// </summary>
static void MonitorGCWithFlameGraph(CancellationToken token)
{
var lastGen0 = GC.CollectionCount(0);
var lastGen1 = GC.CollectionCount(1);
var lastGen2 = GC.CollectionCount(2);
Console.WriteLine("GC监控开始 - 观察火焰图中的表现");
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事件检测到 [{DateTime.Now:HH:mm:ss.fff}] ===");
Console.WriteLine($"Gen0: {currentGen0 - lastGen0} 次");
Console.WriteLine($"Gen1: {currentGen1 - lastGen1} 次");
Console.WriteLine($"Gen2: {currentGen2 - lastGen2} 次");
Console.WriteLine($"总内存: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
// 解释火焰图中的表现
if (currentGen2 > lastGen2)
{
Console.WriteLine("🔥 火焰图表现: Gen2 GC会在火焰图中显示为明显的CPU热点");
Console.WriteLine(" 用户线程会显示为 WaitHandle.WaitOne 或 Thread.Sleep");
}
else if (currentGen1 > lastGen1)
{
Console.WriteLine("🔥 火焰图表现: Gen1 GC可能显示为中等程度的CPU使用");
}
else
{
Console.WriteLine("🔥 火焰图表现: Gen0 GC可能显示为轻微的CPU使用");
}
}
lastGen0 = currentGen0;
lastGen1 = currentGen1;
lastGen2 = currentGen2;
Thread.Sleep(1000);
}
}
/// <summary>
/// 单独测试前台GC
/// </summary>
static void RunForegroundGCDemo()
{
Console.WriteLine("\n=== 前台GC演示 ===");
Console.WriteLine("前台GC会在火焰图中显示为明显的CPU热点");
Console.WriteLine("按任意键开始,再次按任意键停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateForegroundGC(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 单独测试后台GC
/// </summary>
static void RunBackgroundGCDemo()
{
Console.WriteLine("\n=== 后台GC演示 ===");
Console.WriteLine("后台GC在火焰图中可能不明显");
Console.WriteLine("按任意键开始,再次按任意键停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateBackgroundGC(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 单独测试业务逻辑
/// </summary>
static void RunBusinessLogicDemo()
{
Console.WriteLine("\n=== 业务逻辑演示 ===");
Console.WriteLine("业务逻辑在GC暂停期间会受到影响");
Console.WriteLine("按任意键开始,再次按任意键停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateBusinessLogic(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 混合模式 - 所有任务同时运行
/// </summary>
static void RunMixedDemo()
{
Console.WriteLine("\n=== 混合模式演示 ===");
Console.WriteLine("所有任务同时运行,用于对比不同GC类型的影响");
Console.WriteLine("按任意键开始,再次按任意键停止...");
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>
/// 分阶段测试 - 依次测试每种模式
/// </summary>
static void RunStagedDemo()
{
Console.WriteLine("\n=== 分阶段测试 ===");
Console.WriteLine("将依次测试每种GC类型,每次测试30秒");
// 阶段1:前台GC
Console.WriteLine("\n--- 阶段1:前台GC测试 (30秒) ---");
Console.WriteLine("前台GC会在火焰图中显示为明显的CPU热点");
Console.WriteLine("按任意键开始...");
Console.ReadKey();
var cts1 = new CancellationTokenSource();
var task1 = Task.Run(() => DemonstrateForegroundGC(cts1.Token));
Thread.Sleep(30000); // 30秒
cts1.Cancel();
task1.Wait();
// 阶段2:后台GC
Console.WriteLine("\n--- 阶段2:后台GC测试 (30秒) ---");
Console.WriteLine("后台GC在火焰图中可能不明显");
Console.WriteLine("按任意键开始...");
Console.ReadKey();
var cts2 = new CancellationTokenSource();
var task2 = Task.Run(() => DemonstrateBackgroundGC(cts2.Token));
Thread.Sleep(30000); // 30秒
cts2.Cancel();
task2.Wait();
// 阶段3:业务逻辑
Console.WriteLine("\n--- 阶段3:业务逻辑测试 (30秒) ---");
Console.WriteLine("业务逻辑在GC暂停期间会受到影响");
Console.WriteLine("按任意键开始...");
Console.ReadKey();
var cts3 = new CancellationTokenSource();
var task3 = Task.Run(() => DemonstrateBusinessLogic(cts3.Token));
Thread.Sleep(30000); // 30秒
cts3.Cancel();
task3.Wait();
Console.WriteLine("\n=== 分阶段测试完成 ===");
Console.WriteLine("现在可以分析每个阶段的火焰图表现");
}
/// <summary>
/// 单独测试密集GC
/// </summary>
static void RunIntensiveGCDemo()
{
Console.WriteLine("\n=== 密集GC演示 ===");
Console.WriteLine("专门设计用于在火焰图中产生明显的GC热点");
Console.WriteLine("这个演示会频繁调用GC,确保在火焰图中可见");
Console.WriteLine("按任意键开始,再次按任意键停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateIntensiveGC(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 单独测试超激进GC
/// </summary>
static void RunUltraIntensiveGCDemo()
{
Console.WriteLine("\n=== 超激进GC演示 ===");
Console.WriteLine("尝试在用户线程上产生明显的GC相关CPU使用");
Console.WriteLine("这个测试会在用户线程上执行密集的GC相关操作");
Console.WriteLine("按任意键开始,再次按任意键停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DemonstrateUltraIntensiveGC(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
/// <summary>
/// 运行GC活动诊断
/// </summary>
static void RunDiagnoseGCActivity()
{
Console.WriteLine("\n=== GC活动诊断 ===");
Console.WriteLine("监控GC相关指标并解释火焰图表现");
Console.WriteLine("这个测试会解释为什么GC可能不在火焰图中显示");
Console.WriteLine("按任意键开始,再次按任意键停止...");
Console.ReadKey();
var cts = new CancellationTokenSource();
var task = Task.Run(() => DiagnoseGCActivity(cts.Token));
Console.ReadKey();
cts.Cancel();
task.Wait();
}
}
}
运行5的时候呢,大量gc的时候出现了gc函数的调用,但是呢,火焰图看不到gc线程具体执行的信息。
dotnet-trace collect -p 53632 --format speedscope --providers Microsoft-DotNETCore-SampleProfiler --duration 00:00:30 --symbols ./bin/Debug/net8.0/
根本原因分析
1. SampleProfiler 的工作原理
CPU 采样机制
SampleProfiler 采样原理:
├── 定期中断用户线程
├── 收集当前调用栈
├── 统计 CPU 使用时间
└── 生成火焰图数据
对 GC 的限制
GC 工作的特点:
├── GC 工作在专用线程上
├── 用户线程在 GC 期间被暂停
├── SampleProfiler 主要采样用户线程
└── GC 线程可能不被采样
### 2. **为什么 SampleProfiler 抓取不到 GC**
#### 原因1:GC 线程隔离
```csharp
// GC 工作流程
用户线程: GC.Collect() → 等待 GC 完成 → 继续执行
GC线程: 执行实际的垃圾回收工作(标记、清除、压缩)
原因2:用户线程在 GC 期间被暂停
// 用户线程在 GC 期间的状态
while (GC.IsInProgress())
{
// 用户线程被暂停,不执行任何代码
// SampleProfiler 无法采样到 GC 相关活动
}
原因3:采样频率问题
// GC 调用可能很快完成
GC.Collect(0, GCCollectionMode.Forced); // 调用本身很快
GC.WaitForPendingFinalizers(); // 等待时间可能不被采样
那就是抓取不到了,这是个问题。

可以看到主动调用才抓到这么几次,难道只消耗着一点cpu吗?

那么cpu到底被gc消耗掉了还是被用户线程消耗掉了,这个怎么判断呢?通过我们打印gc时间还是消耗了很多cpu呢,那么怎么看呢?
那么gc 问题就有专门的工具了,一个是gc-verbose,dotnet-counters.
下面来试一下dotnet-counters。
重新跑这个例子:
dotnet-counters collect --process-id 20084 --refresh-interval 5 --duration 60 --output counters.csv
然后可以看到结果,因为抓取的比较多,这里只用一个来说明:
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评率:
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秒,发生了很多的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秒占用的时间是5683.659000000001,这个应该是计算有点问题的。
Time in GC since last GC 这个是这一次gc,占用到距离上一次gc的百分比。
TimeInGCPercent = (TotalGCPauseTime / TotalTimeSinceLastGC) * 100
高百分比(如 99%)
// 表示应用程序几乎一直在进行 GC
// 应用程序线程大部分时间被暂停
// 用户体验很差,响应性很低
低百分比(如 5%)
// 表示 GC 占用时间很少
// 应用程序线程大部分时间在执行业务逻辑
// 用户体验良好,响应性好
3. CPU 使用分析
- CPU Usage:7.7-8.4%(相对稳定)
- ThreadPool Thread Count:1-2个线程
分析:CPU 使用率适中,但 GC 工作占用了大量时间。
后台 GC 指标
- 数据中没有显示
gc-background-gc-count或gc-concurrent-gc-count - 所有 GC 都是阻塞性的
内存分配
- Allocation Rate:31MB-1GB/5秒(变化很大)
- GC Heap Size:约 1032MB(相对稳定) // 只是gc托管的资源
- Working Set:1057-1087MB(缓慢增长)// 程序占用系统全部的资源
内存分布
- Gen 0 Size:0-11KB(大部分时间为空)
- Gen 1 Size:0-6MB(偶尔有对象)
- Gen 2 Size:约 1024MB(主要内存占用)
- LOH Size:约 16MB(大对象堆) # 这个需要看一下,large object heap
- POH Size:32KB(固定对象堆) # pined object heap,不用管比较小
分析:大部分对象都被提升到 Gen 2,说明应用程序在创建长期存活的对象。
这样就可以大量gc问题,还有内存的关系了。
结
在确定是用户时间消耗cpu的时候,需要查看dotnetcount 查看gc,和查看火焰图一起看,这样才是合理的,因为火焰图比较难看到gc,尤其是专有gc。
浙公网安备 33010602011771号