cpu 和垃圾回收的关系

前言

前文中介绍了,cpu 使用率和三个方面有关,一个是中断、一个是用户时间、一个是系统时间。

那么当我们排查问题时候,查到是用户时间占用比较多的话,那么该如何排查呢?

正文

首先我们想到的是,cpu高嘛,查看火焰图对吧。 火焰图可以看到哪个函数消耗cpu比较多?

似乎这个问题好像马上能查看到,如果火焰图调入gc函数消耗的cpu比较多的话,那么就直接定位了。

然而事情好像没有这么简单。

这个我们来看一下火焰图原理:

火焰图是一种性能分析工具,它通过采样CPU调用栈来生成可视化的性能分析图表。火焰图能够显示:

1.CPU时间消耗最多的函数调用路径
2. 调用栈的深度和宽度
3. 热点代码的位置

GC在火焰图中的表现

  1. GC线程活动:如果GC线程占用大量CPU时间,会在火焰图中显示为GC相关的调用栈
  2. GC暂停时间:虽然火焰图主要显示CPU时间,但GC暂停期间的CPU使用也会被记录
  3. 内存分配热点:频繁的内存分配会导致GC压力,这些分配点会在火焰图中显示

可能观察不到的情况:

  1. 后台GC:.NET的Server GC模式使用后台线程进行垃圾回收,这些线程的活动可能不会在火焰图中明显显示
  2. 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火焰图:

image

可以看到一个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();           // 等待时间可能不被采样

那就是抓取不到了,这是个问题。

image

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

image

那么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-countgc-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。

posted @ 2025-08-07 18:21  敖毛毛  阅读(26)  评论(0)    收藏  举报