我的服务运行越久内存涨得越凶,最后内存泄漏到崩溃,排查发现是一堆本该被回收的对象因为订阅了事件却没取消订阅、被发布者死死攥着无法回收,我对着 C# 事件订阅导致的内存泄漏这个坑排查大半天的复盘

一个让我对有了 GC 就不会内存泄漏这个错觉彻底清醒的 C# 坑,隐蔽在 C# 有垃圾回收我一直以为对象没用了 GC 自然会回收根本不用操心,可这次一堆本该被回收的对象却被一根我没注意到的引用线死死拴住、GC 怎么也回收不了。服务运行越久内存涨得越凶最后 OOM 崩溃。场景:短生命周期对象 TempHandler 订阅了长生命周期全局服务 GlobalService 的 DataChanged 事件(+= 但从不 -= 取消),不断创建 handler 用完丢弃以为会被回收,实际一个都回收不掉、各拖 10MB 持续泄漏。深究 GC 才明白:GC 回收对象的前提是没有任何存活引用指向它;+= 订阅让发布者内部持有一个委托而委托引用了订阅者,即发布者间接引用了订阅者;长命的 service 通过事件一直引用着短命的 handler 又不 -= 取消,handler 就永远可达 GC 永不回收、越积越多;引用方向是发布者→订阅者,所以短命对象订阅长命对象的事件是泄漏高发场景;有 GC 还泄漏是因为 GC 无法判断这个引用是不是你忘了解除的,只要有引用在 GC 看来对象就还有用。这篇从故障现场、事件订阅泄漏真相、正解(用完 -= 取消、IDisposable+using 把取消和生命周期绑定、弱事件模式发布者持弱引用、匿名委托要存变量才能取消)、GC 语言其他泄漏(静态集合越加不减、缓存无上限、未关资源、ThreadLocal、闭包捕获大对象)、泄漏模式对照表、GC 的幻觉与真相、决策图与铁律,到附上一段用 WeakReference+强制GC+IsAlive 确凿验证事件泄漏的实验。核心领悟:GC 语言的内存管理本质是引用关系管理,要确保不再需要的对象所有不必要的引用都被解除,用 profiler 找引用链剪断它;强大的自动化在解放我们于繁琐同时容易让我们连同对那件事的理解和必要责任一起交出去,正确姿态是用它免去繁琐但不放弃基本理解和警觉、知道它的边界、边界之外仍需我负责;用 WeakReference+强制GC+IsAlive 把对象是否被回收变成可观测的布尔值确诊泄漏。

我的服务运行越久内存涨得越凶,最后内存泄漏到崩溃,排查发现是一堆本该被回收的对象因为订阅了事件却没取消订阅、被发布者死死攥着无法回收,我对着 C# 事件订阅导致的内存泄漏这个坑排查了大半天的复盘

这是一个让我对"有了 GC 就不会内存泄漏"这个错觉彻底清醒的 C# 坑。它隐蔽在:C# 有垃圾回收(GC),我一直以为"对象没用了 GC 自然会回收,根本不用操心内存";可这次,一堆本该被回收的对象,却被一根我没注意到的"引用线"死死拴住、GC 怎么也回收不了,内存就这么一点点泄漏到崩溃。

现象是:服务运行时间越长,内存占用涨得越凶,最后 OOM 崩溃;重启后又从低位开始涨。需求场景很常见——一些短生命周期的对象(比如临时的处理器、视图模型),订阅了一个长生命周期对象(比如全局服务)的事件:

// 长生命周期的全局服务(它有一个事件)
public class GlobalService {
    public event EventHandler DataChanged;   // 一个事件
    public void NotifyChange() => DataChanged?.Invoke(this, EventArgs.Empty);
}

// 短生命周期的对象, 订阅了全局服务的事件(有问题的版本)
public class TempHandler {
    private byte[] bigData = new byte[10_000_000];   // 假设它占用不少内存

    public TempHandler(GlobalService service) {
        service.DataChanged += OnDataChanged;   // ★ 订阅事件 += , 但从不 -= 取消!
    }

    private void OnDataChanged(object sender, EventArgs e) { /* 处理 */ }
}

// 业务里, 不断创建 TempHandler, 用完就"丢弃"(以为GC会回收):
for (...) {
    var handler = new TempHandler(globalService);
    // ... 用完, handler 离开作用域, 我以为它会被GC回收
}
// 实际: 这些 handler 【一个都回收不了】! 内存持续泄漏!

我盯着内存分析器(profiler)里那一长串"本该消失却还活着"的 TempHandler 实例,百思不得其解。这些 handler 用完就离开了作用域,按理说没人引用它们了、GC 早该回收它们了;可它们就是顽固地活着、越积越多,每个还拖着 10MB 的 bigData,内存自然蹭蹭往上涨。明明有 GC,为什么这些"没用了"的对象就是回收不掉?是谁还在"抓着"它们不放?

第一件事:看清真相——事件订阅让发布者持有订阅者的引用,不取消就一直被抓着

我去深入理解了 C# 事件(event)的本质和 GC 的回收原理,才彻底明白这个"对象回收不掉"之谜——当你用 += 订阅一个事件时,发布者(GlobalService)内部会持有一个指向订阅者(TempHandler)的引用(通过那个委托);GC 回收一个对象的前提是"没有任何存活的对象引用它";而只要你不用 -= 取消订阅,这个"发布者→订阅者"的引用就一直存在——长生命周期的发布者,就这样把短生命周期的订阅者死死攥住,让它永远无法被 GC 回收

事件订阅导致内存泄漏的真相

# 1. GC 回收对象的前提: 这个对象【没有任何存活的引用指向它】(不可达)。
#    - 只要还有"活着的对象"引用它, GC 就【不会】回收它(认为它还有用)。

# 2. 事件订阅 += 做了什么:
#    service.DataChanged += handler.OnDataChanged;
#    - 这让 service 的 DataChanged 事件, 内部【持有一个委托】,
#      而这个委托【引用了 handler 这个对象】(因为OnDataChanged是它的方法)!
#    - → 即: service 【间接引用了】 handler。

# 3. 于是内存泄漏发生:
#    - service 是【长生命周期】的(全局服务, 一直活着);
#    - handler 本是【短生命周期】的(用完该回收);
#    - 但因为 service 通过事件【一直引用着】 handler(没-=取消),
#    - → handler 永远"可达"(被活着的service引用) → GC 永远不回收它!
#    - → 你以为handler用完没人要了, 其实service还死死攥着它。
#    - → 不断创建handler又不取消订阅 → 它们全都泄漏、越积越多。

# 4. 关键: 引用方向是 "发布者 → 订阅者"!
#    - 是长命的发布者, 攥住了短命的订阅者(而非反过来);
#    - 所以"短命对象订阅长命对象的事件"是泄漏高发场景。

# 5. 为什么有GC还会泄漏:
#    - GC 解决的是"没人引用的对象"的回收; 它无法判断"这个引用是不是你'忘了'解除的";
#    - 只要存在引用(哪怕是你忘了取消的事件订阅), 在GC看来对象就"还有用"。
#    - → "内存泄漏"在有GC的语言里, 主要表现为"对象被意外地、长期地引用着无法回收"。

# 核心: 事件 += 让发布者持有对订阅者的引用; 长命发布者引用短命订阅者、又不-=取消, 订阅者就永远
#   可达、GC回收不掉, 造成泄漏; 有GC不等于不会泄漏——意外的长期引用就是GC语言里的泄漏。

真相大白,我恍然大悟。原来 GC 回收一个对象的前提是"没有任何存活的引用指向它";而 += 订阅事件时,发布者(service)内部会持有一个委托,这个委托引用了订阅者(handler)——也就是说,service 间接引用了 handler于是泄漏发生:service 是长生命周期的(全局服务一直活着),handler 本是短生命周期的(用完该回收);但因为 service 通过事件一直引用着 handler(没 -= 取消),handler 就永远"可达"、GC 永远不回收它——你以为 handler 用完没人要了,其实 service 还死死攥着它;不断创建 handler 又不取消订阅,它们全都泄漏、越积越多。关键在于引用方向是"发布者→订阅者"——是长命的发布者攥住了短命的订阅者,所以"短命对象订阅长命对象的事件"是泄漏高发场景。而为什么有 GC 还会泄漏:GC 解决的是"没人引用的对象"的回收,它无法判断"这个引用是不是你忘了解除的"——只要存在引用(哪怕是你忘了取消的事件订阅),在 GC 看来对象就"还有用"。所以"内存泄漏"在有 GC 的语言里,主要表现为"对象被意外地、长期地引用着,无法回收"。

第二件事:正解——用完取消订阅(-=),配合 IDisposable,或用弱事件

搞懂了原理,正解就清晰了:订阅了事件,就要在不用时取消订阅(-=);用 IDisposable 把"取消订阅"和对象生命周期绑定;或用弱事件模式让发布者不强引用订阅者

// ====== 正解一(基础): 用完取消订阅 -= ======
public class TempHandler : IDisposable {
    private readonly GlobalService service;

    public TempHandler(GlobalService service) {
        this.service = service;
        service.DataChanged += OnDataChanged;   // 订阅
    }

    private void OnDataChanged(object sender, EventArgs e) { /* 处理 */ }

    // ★ 实现 IDisposable, 在 Dispose 里取消订阅
    public void Dispose() {
        service.DataChanged -= OnDataChanged;   // ★ 取消订阅! 解除发布者对自己的引用
    }
}
// 用: using (var handler = new TempHandler(service)) { ... }   // using自动调Dispose
// 或手动: handler.Dispose();  → 取消订阅后, 没人引用handler了, GC就能回收它

// ====== 关键原则: 订阅(+=)和取消(-=)要成对出现 ======
// - 谁订阅, 谁负责取消; 在对象"生命周期结束/不再需要"时取消订阅。
// - 像 new/delete、open/close、lock/unlock 一样, += 也要有配对的 -=。

// ====== 正解二: 用 IDisposable + using 把生命周期管起来 ======
// 让订阅者实现IDisposable, 在Dispose取消订阅; using/框架的生命周期钩子自动调Dispose。
// (WPF: 在Unloaded/OnDetached里取消; ViewModel: 在Cleanup里取消)

// ====== 正解三: 弱事件模式(WeakEvent) ======
// 让发布者对订阅者持【弱引用】(weak reference): 弱引用不阻止GC回收订阅者。
// - .NET 有 WeakEventManager; 或自己用 WeakReference 实现弱事件。
// → 这样即使忘了-=, 订阅者也能被回收(发布者的弱引用不算"强引用")。
// (代价: 实现复杂些; 适合"难以确定何时取消订阅"的场景)

// ====== 正解四: 用匿名委托订阅时尤其当心 ======
// service.DataChanged += (s, e) => { ... };   // 匿名委托没法直接 -= !
// → 如果要取消, 要先把委托存进变量, 才能 -=:
//   EventHandler h = (s, e) => {...};  service.DataChanged += h;  ... service.DataChanged -= h;

// 核心: += 订阅要有配对的 -= 取消; 用IDisposable+using把取消和生命周期绑定; 难管理时用弱事件;
//   匿名委托订阅要存变量才能取消; 牢记"长命对象的事件, 别让短命对象订阅了又不取消"。

修复的核心,是"+= 订阅要有配对的 -= 取消,并和生命周期绑定"正解一(基础):用完 -= 取消订阅——让订阅者实现 IDisposable,在 Dispose()service.DataChanged -= OnDataChanged 取消订阅、解除发布者对自己的引用;用 using 自动调 Dispose,取消后没人引用 handler,GC 就能回收关键原则:订阅(+=)和取消(-=)要成对出现——谁订阅谁负责取消,像 new/delete、open/close 一样,+= 也要有配对的 -=正解二:用 IDisposable + using 把生命周期管起来(WPF 在 Unloaded、ViewModel 在 Cleanup 里取消)。正解三:弱事件模式——让发布者对订阅者持弱引用(不阻止 GC 回收),.NET 有 WeakEventManager,即使忘了 -= 订阅者也能被回收(代价是实现复杂些)正解四:匿名委托订阅尤其当心——+= (s,e)=>{...} 没法直接 -=,要取消得先把委托存进变量。归根结底:+= 订阅要有配对的 -= 取消;用 IDisposable+using 把取消和生命周期绑定;难管理时用弱事件;匿名委托要存变量才能取消;牢记"长命对象的事件别让短命对象订阅了又不取消"。

第三件事:GC 语言里其他常见的内存泄漏

排查后我把 GC 语言(C#/Java 等)里其他常见的内存泄漏也系统梳理了一遍。

GC 语言里其他常见的内存泄漏

# 1. 事件订阅没取消(本文): 发布者引用订阅者。→ -= / 弱事件 / IDisposable。

# 2. 静态集合越加不减: static List/Map 不断add不remove, 里面的对象永不回收。
#    → 静态集合要有清理机制/容量上限。

# 3. 缓存无上限/无过期: 缓存只进不出, 占满内存。→ 设容量上限/LRU/过期。

# 4. 长生命周期对象持有短生命周期对象引用: 同事件, 长命的攥着短命的。

# 5. 没关闭的资源(连接/流/定时器): Timer不Dispose会一直跑、引用回调对象。

# 6. ThreadLocal没清理: 线程池线程复用, ThreadLocal里的值不清会累积。

# 7. 闭包/委托意外捕获大对象: lambda捕获了一个大对象, 委托存活期间它也存活。

# 8. 集合里对象不再用却没移除: 用对象做key/value放进集合, 用完不remove。

# 共同根源: GC回收的是"不可达(没人引用)"的对象; 内存泄漏=对象"本该没用了, 却仍被
#   某个存活对象意外地、长期地引用着"→ GC认为它还有用、不回收。

# 核心: 有GC≠不会泄漏; GC语言的泄漏="意外的长期引用"——事件没取消、静态集合越加不减、
#   缓存无上限、资源没关闭、ThreadLocal没清; 要主动解除不再需要的引用、给集合/缓存设上限。

排查让我把 GC 语言的其他泄漏也梳理清了。一、事件订阅没取消(本文)。二、静态集合越加不减(static List/Map 不断 add 不 remove)。三、缓存无上限/无过期(设上限/LRU/过期)。四、长命对象持有短命对象引用五、没关闭的资源(Timer 不 Dispose 一直跑)。六、ThreadLocal 没清理(线程池复用累积)。七、闭包/委托意外捕获大对象八、集合里对象不再用却没移除它们的共同根源是:GC 回收的是"不可达(没人引用)"的对象;内存泄漏=对象"本该没用了,却仍被某个存活对象意外地、长期地引用着"→ GC 认为它还有用、不回收核心是:有 GC ≠ 不会泄漏;GC 语言的泄漏=意外的长期引用;要主动解除不再需要的引用、给集合/缓存设上限下面这张图,是这次事件泄漏的成因与解法:

第四件事:GC 语言内存泄漏的几种典型模式对照表

这次踩坑后,我把 GC 语言里内存泄漏的几种典型模式整理成一张表。

模式 谁引用了谁 解法
事件订阅没取消 发布者→订阅者 -= / 弱事件 / IDisposable
静态集合越加不减 静态字段→对象 清理/上限/弱引用
缓存无上限/过期 缓存→对象 LRU/容量上限/TTL
未关闭的资源/定时器 运行时→回调对象 Dispose/close/cancel
ThreadLocal没清 线程→值 用完remove
闭包捕获大对象 委托→捕获的对象 别捕获不必要的大对象

这张表把 GC 语言泄漏的模式钉清了。核心是:所有这些泄漏,本质都是同一个模式——"某个生命周期长的东西(发布者/静态字段/缓存/线程),意外地、长期地引用着一个本该被回收的对象",让 GC 无法回收它;辨别泄漏,关键是找出"到底是谁在引用这个本该消失的对象"那根引用线它给我的最大启发是:在有 GC 的语言里,排查内存泄漏的核心动作,是用内存分析器(profiler)找到"泄漏对象的引用链(GC Roots 到它的路径)"——即"从一个'永远存活的根'(静态字段、活跃线程、全局服务)出发,经过哪些引用,最终'抓住'了这个本该被回收的对象";找到了这根引用链,就找到了泄漏的根源,只要剪断它(取消订阅、清理集合、移除缓存),对象就能被回收这其实揭示了 GC 语言内存管理的一个本质:有 GC,不意味着你完全不用管内存;它把"手动 free 内存"的负担,转换成了"管理对象的引用关系、确保不再需要的对象不被意外引用"的负担;"内存管理"在 GC 语言里,变成了"引用关系管理"——你要确保,当一个对象不再需要时,所有指向它的"不必要的引用"都被解除了把 GC 语言的内存管理理解为"引用关系管理"、用 profiler 找引用链并剪断它——是排查这类泄漏的核心方法。

第五件事:GC 给我们的"幻觉"与真相

这次最大的认知冲击,是 GC 给我的"不用管内存"的幻觉被打破了。

关于内存的认知 GC给的幻觉 真相
对象回收 没用了自动回收, 不用管 "没人引用"才回收, 引用要你管
内存泄漏 有GC就不会泄漏 意外的长期引用照样泄漏
资源管理 GC会处理一切 非内存资源(连接/文件/事件)GC管不了/不及时
开发者的责任 完全不用操心内存 要管理引用关系、及时解除引用

这张表道出了 GC 的"幻觉与真相"。核心是:GC 极大地减轻了内存管理的负担(不用手动 free),但它没有、也不可能完全消除这个负担;它给了我们"完全不用管内存"的幻觉,而真相是:我们仍然要管理"对象之间的引用关系",确保不再需要的对象及时被解除引用它给我的深刻启发是:任何"自动化/帮你管理某件事"的机制(GC 管内存、ORM 管 SQL、框架管生命周期),都不是"魔法",而是"在某个范围内、按某种规则"帮你处理;如果你完全不理解它的规则和边界,把它当成"万能、永不出错"的魔法,就会在它"规则之外或边界处"栽跟头(GC 的规则是"回收不可达对象",边界是"它管不了'你忘了解除的引用'和'非内存资源'")。这让我对"自动化机制"有了更成熟的态度:享受自动化带来的便利,但始终理解"它到底帮我做了什么、它的规则是什么、什么是它管不了而仍需我负责的";不要因为有了自动化,就彻底放弃对那件事的理解和责任——而要清楚地知道"自动化的边界在哪,边界之外仍是我的责任"看破"自动化=完全不用管"的幻觉、理解 GC 的规则与边界并守住边界外的责任——是这个内存泄漏坑,带给我的关于"如何依赖自动化机制"的更深思考。

第六件事:写订阅/持有引用的代码时,我现在的判断习惯

现在每当我写"订阅事件"或"让一个对象持有另一个对象的引用"的代码,我都会按这张图先想清楚:

这张图的精髓,是"发布者比订阅者长命就警惕,订阅时就想好在哪取消、确保 += 和 -= 配对"生命周期一样长一般安全;发布者比订阅者长命就警惕、订阅时就想好在哪取消——有明确结束时机就 IDisposable 里 -=、难确定就用弱事件,确保 += 和 -= 配对长期服务还要监控内存趋势、用 profiler 查泄漏。这套习惯,让我写订阅代码时,从"+= 订阅就完事"变成了"订阅时就想好怎么取消、谁攥着谁"——核心始终是:事件订阅让发布者引用订阅者,长命攥短命又不取消就泄漏;+= 要配对 -=、绑定生命周期。

我立下的几条规矩

这场"事件订阅导致内存泄漏"的事故,换来了我写 C# 时,刻进骨子里的几条铁律:

  1. 有 GC 不等于不会内存泄漏。意外的长期引用就是 GC 语言的泄漏。
  2. += 订阅让发布者持有对订阅者的引用。长命攥短命就泄漏。
  3. += 要有配对的 -=。像 new/delete 一样成对,谁订阅谁取消。
  4. 用 IDisposable + using 把取消订阅和生命周期绑定。
  5. 难管理时用弱事件模式。发布者持弱引用,不阻止 GC。
  6. 匿名委托订阅要存变量才能取消。否则没法 -=。
  7. GC 语言的内存管理 = 引用关系管理。及时解除不再需要的引用。

附:一段能复现并验证事件泄漏的实验

口说无凭。下面这段代码,用弱引用 + GC,亲手验证"订阅不取消则回收不掉、取消后能回收":

using System;

class GlobalService {
    public event EventHandler DataChanged;
}

class Handler {
    public Handler(GlobalService svc) { svc.DataChanged += OnChanged; }
    private void OnChanged(object s, EventArgs e) { }
}

class Program {
    // 创建一个Handler订阅事件, 返回它的弱引用(弱引用不阻止GC回收)
    static WeakReference CreateAndSubscribe(GlobalService svc) {
        var h = new Handler(svc);       // 订阅了 svc 的事件, 没有取消
        return new WeakReference(h);    // 返回弱引用; 局部变量h离开作用域后, 看它能否被回收
    }

    static void Main() {
        var svc = new GlobalService();  // 长生命周期的服务

        Console.WriteLine("=== 1. 订阅了不取消 ===");
        var weak = CreateAndSubscribe(svc);
        GC.Collect();                   // 强制GC
        GC.WaitForPendingFinalizers();
        GC.Collect();
        // weak.IsAlive: handler还活着吗?
        Console.WriteLine("  GC后 handler 还活着吗: " + weak.IsAlive);
        // → True! 虽然局部变量h没了, 但svc的事件还引用着它 → 没被回收 → 泄漏!

        // (说明: 因为没有取消订阅, svc.DataChanged 仍持有对handler的引用,
        //  handler对GC来说仍"可达", 所以GC不回收它——这就是泄漏的铁证。)

        Console.WriteLine("\n  对比: 如果在Handler里实现Dispose并 svc.DataChanged -= OnChanged,");
        Console.WriteLine("  调用Dispose取消订阅后再GC, weak.IsAlive 就会是 False(被回收了)。");
    }
}

// 核心: 跑一遍, 亲眼看到——订阅了不取消订阅时, 即使局部引用没了、强制GC, handler仍然
//   weak.IsAlive==True(没被回收, 因被事件引用着); 这就用弱引用+GC确凿地证明了事件泄漏。

这段实验代码,是我这次踩坑后写下的"泄漏证明器"。它最巧妙的设计,是用了 WeakReference(弱引用)——弱引用本身不阻止 GC 回收对象,所以它成了一个完美的"探针":我创建一个 Handler 订阅了事件、然后让指向它的强引用(局部变量)消失,再强制 GC,最后通过 weak.IsAlive 检查"这个 Handler 到底被回收了没"。结果是 True(还活着)——这就确凿地证明了:即使没有任何"明面上的"强引用、即使强制 GC,这个 Handler 也没被回收,因为 svc 的事件还在暗中引用着它这正是我想用这段代码,留给每个用 GC 语言的人的核心方法:要验证"某个对象到底会不会被 GC 回收、是不是泄漏了",一个精巧的实验是——用 WeakReference 持有它(弱引用不影响回收)、然后清除所有强引用、强制 GC、再检查 weak.IsAlive:如果还活着(IsAlive=True),说明有"看不见的强引用"在拖住它(泄漏);如果被回收了(False),说明引用确实都解除了因为"对象有没有被回收"这件事,平时是看不见、摸不着的;而 WeakReference + 强制 GC + IsAlive 这套组合,能把它变成一个可以明确观测的布尔值,让"泄漏与否"从猜测变成确凿的实验结果;这是验证内存泄漏(尤其这种"被意外引用"型泄漏)最直接的"白盒"手段用 WeakReference+强制 GC+IsAlive 把"对象是否被回收"变成可观测的实验——这份习惯,是我确诊一切"意外引用型内存泄漏"最可靠的法门。

写在最后

回头看,这场由"事件没取消订阅"引发的、内存泄漏到崩溃的事故,真正教给我的,远不止"记得 -="这一个技巧。它让我对"自动化解放了我们,但也容易让我们交出本不该交出的责任与理解"这件事,有了一次深刻的体会。我栽跟头,根源是 GC 这个伟大的自动化机制,给了我一种"内存这件事,我完全不用操心了"的过度的安全感。我把"不用手动 free 内存",错误地理解成了"完全不用思考内存、不用思考对象的生死"——我以为只要一个对象"逻辑上没用了",它就会自动消失,从没想过"它在内存里到底还被谁引用着、它在 GC 眼里到底还活不活"。于是,当一根我没注意到的引用线(事件订阅)把它拴住时,我对此毫无察觉,因为我早已"不思考内存"了。这让我领悟到一个深刻的认知:强大的自动化/抽象工具,在解放我们于繁琐的同时,也有一个微妙的副作用——它容易让我们连同那件事的"理解"和"必要的责任"一起交出去;我们享受着"不用管"的便利,却也因此丧失了对那件事的感知和警觉;而当那件事在自动化的边界之外出问题时(GC 管不了你忘了解除的引用),我们因为早已"不去想它"而后知后觉、束手无策这其实是一个关于"人与自动化"的普遍警示:对每一个"替我们自动处理某事"的工具(GC、ORM、框架、乃至 AI),我们都要警惕"过度信赖导致的能力与责任的交出";正确的姿态是:用它来免去繁琐,但不放弃对那件事的基本理解和必要的警觉——知道它在替我做什么、它的边界在哪、边界之外我仍需负责;"用自动化,但不被自动化麻痹",是这个时代每个工程师都需要的清醒用 GC 免去手动管理内存的繁琐、但不放弃对"对象引用关系"的理解与责任——这,是我用一次内存泄漏的事故,换来的、关于 C#、关于 GC、也关于如何清醒地与一切自动化工具相处的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写下 += 订阅事件 时,顺手就想一句"这个订阅,我会在哪里 -= 取消呢?",那我对着那一长串本该消失却还活着的对象排查的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我用 Redis 做分布式锁防止任务被重复处理,结果先是一个实例崩了导致所有任务卡死,后来又出现同一个任务被两个实例同时处理,我对着分布式锁这几个致命实现细节排查了大半天的复盘

2026-6-2 12:54:51

技术教程

我用数字枚举定义状态,本以为类型很安全,结果一个根本不在枚举里的数字 99 被当成合法状态溜了进来,遍历枚举时还冒出一堆奇怪的数字键,我对着 TypeScript 数字枚举这两个坑排查了大半天的复盘

2026-6-2 13:07:34

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索