C# 中的循环引用是什么?

来源:站长素材作者:天穹小白头衔:草根站长
导读:本期聚焦于小伙伴创作的《C# 中的循环引用是什么?》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《C# 中的循环引用是什么?》有用,将其分享出去将是对创作者最好的鼓励。

在C#开发过程中,对象之间的引用关系是内存管理的核心内容之一,循环引用指的是两个或多个对象彼此持有对方的引用,最终形成一个闭环的引用结构,这种关系会影响垃圾回收对对象存活状态的判断。

C# 中的循环引用是什么?

循环引用的基本定义

循环引用本质是对象引用链形成了闭环。比如对象A持有对象B的引用,对象B又持有对象A的引用,此时A和B就形成了最简单的双向循环引用。如果还有更多对象参与,比如A引用B,B引用C,C再引用A,就会形成更复杂的多对象循环引用。

我们可以通过一段简单的代码来理解这种关系:

// 定义两个互相引用的类
public class ClassA
{
    // 持有ClassB的引用
    public ClassB BInstance { get; set; }
}

public class ClassB
{
    // 持有ClassA的引用
    public ClassA AInstance { get; set; }
}

public class Program
{
    static void Main()
    {
        // 创建两个对象实例
        ClassA a = new ClassA();
        ClassB b = new ClassB();
        // 互相赋值引用,形成循环引用
        a.BInstance = b;
        b.AInstance = a;
    }
}

循环引用与C#垃圾回收的关系

很多开发者会担心循环引用会导致内存泄漏,实际上C#的垃圾回收机制(GC)采用的是可达性分析算法,而不是简单的引用计数,因此普通的循环引用不会直接导致内存泄漏。

可达性分析的核心逻辑是从一组被称为GC Roots的根对象出发,遍历所有能被根对象直接或间接访问到的对象,这些对象会被标记为存活,剩下的没有被标记的对象就是可回收的垃圾对象。

常见的GC Roots包括:

  • 当前正在执行的方法中的局部变量
  • 静态变量
  • 线程对象
  • GC句柄表中的对象

回到上面的代码示例,当Main方法执行结束之后,局部变量a和b都会离开作用域,此时ClassA和ClassB的实例虽然互相引用,但是已经没有任何GC Roots可以到达它们,因此这两个对象都会被标记为垃圾,在后续的GC过程中被回收。

特殊场景下的循环引用问题

虽然普通的托管对象循环引用不会造成问题,但是在一些特殊场景下,循环引用还是可能引发内存相关的异常:

1. 涉及非托管资源的循环引用

如果循环引用的对象持有非托管资源,并且没有正确实现释放逻辑,就可能导致非托管资源无法及时释放。比如两个对象都持有对方的引用,并且都实现了Finalize方法,GC回收时可能会延长这些对象的生命周期,导致非托管资源释放延迟。

2. 事件订阅导致的隐式循环引用

事件订阅是C#中很容易产生隐式循环引用的场景。比如对象A订阅了对象B的事件,而对象B又持有对象A的引用,此时如果外部没有取消事件订阅,即使外部已经没有其他引用指向A和B,它们也可能因为事件委托的引用关系无法被回收。

下面是一个事件订阅导致循环引用的示例:

public class EventPublisher
{
    // 定义事件
    public event Action MyEvent;
    
    public void TriggerEvent()
    {
        MyEvent?.Invoke();
    }
}

public class EventSubscriber
{
    // 持有发布者的引用
    public EventPublisher Publisher { get; set; }
    
    public EventSubscriber(EventPublisher publisher)
    {
        Publisher = publisher;
        // 订阅事件,此时发布者的事件委托会持有订阅者的引用
        Publisher.MyEvent += HandleEvent;
    }
    
    private void HandleEvent()
    {
        Console.WriteLine("事件触发");
    }
}

public class Program
{
    static void Main()
    {
        EventPublisher publisher = new EventPublisher();
        EventSubscriber subscriber = new EventSubscriber(publisher);
        // 此时publisher持有subscriber的引用(通过事件委托),subscriber持有publisher的引用,形成循环引用
        // 如果后续没有取消事件订阅,两个对象可能无法被回收
    }
}

如何避免不必要的循环引用

虽然C#的GC可以处理大部分循环引用,但是合理设计对象关系还是能减少不必要的内存开销,避免潜在的问题:

  • 尽量减少对象之间的双向强引用,优先使用单向引用
  • 事件订阅使用后及时取消订阅,比如在订阅者销毁前移除事件委托
  • 对于需要互相引用的场景,可以考虑使用弱引用WeakReference来持有对方的引用,弱引用不会阻止对象被GC回收
  • 合理设计对象的生命周期,避免长生命周期的对象持有短生命周期对象的强引用

下面是一个使用弱引用避免循环引用的示例:

public class ClassA
{
    // 使用弱引用持有ClassB的实例,不会阻止B被回收
    private WeakReference<ClassB> _bRef;
    
    public void SetB(ClassB b)
    {
        _bRef = new WeakReference<ClassB>(b);
    }
    
    public void UseB()
    {
        if (_bRef.TryGetTarget(out ClassB b))
        {
            // 成功获取到B的实例,执行操作
            Console.WriteLine("获取到B实例");
        }
        else
        {
            // B已经被回收
            Console.WriteLine("B实例已被回收");
        }
    }
}

public class ClassB
{
    // 持有ClassA的强引用
    public ClassA AInstance { get; set; }
}

总结

循环引用是C#中对象之间的一种闭环引用关系,由于C#采用可达性分析的垃圾回收机制,普通的托管对象循环引用不会直接导致内存泄漏。但是在事件订阅、非托管资源持有等特殊场景下,循环引用还是可能引发内存释放延迟等问题。开发者需要在编码过程中合理设计对象引用关系,及时释放不必要的引用,避免隐式的循环引用产生,从而保证程序的内存使用效率。

C#循环引用垃圾回收内存泄漏引用计数修改时间:2026-07-04 15:57:31

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。