1.概要
这篇文章主要概括的聊一聊GC,大概知道有哪些知识点或使用的时候需要注意什么。讲GC的文章一抓一大把,我就挑几个我个人比较有兴趣的地方分享一下。
1.1什么是GC?
C#中,GC代表"垃圾收集器"(Garbage Collector)。垃圾收集其实是.NET框架的一部分,它负责管理系统内存,自动回收不再使用的对象所占用的内存。开发者无需手动释放他们创建的对象占用的内存,减少了由于忘记释放内存而造成的内存泄漏等问题。
1.2 GC的组成部分
- Managed Heap:这是.NET中所有对象都被分配空间的地方。当你创建一个新的对象时,它就会在managed heap中被分配一块空间。
- GC Handles: 这些是一种特殊类型的指针,用于引用heap上的对象。GC handle可以防止引用的对象被垃圾回收。
- 堆:堆是一个内存区域,用于存放所有的对象实例。在.NET中,GC堆被分为三个区域或生成:
- 第0代:新创建的对象首先被分配到此处。当GC运行时,这是首先被考虑的区域。
- 第1代:第0代中“幸存”的对象会被移动到此处。这些对象比新创建的对象更不容易被回收。
- 第2代:第1代中“幸存”的对象会被移动到此处。这些是最长寿命的对象。
- Finalizers: 当一个对象即将被GC回收前,如果该对象定义了finalizer方法,那么此方法将被调用。这给对象提供了一个机会去清理任何非托管资源。
- Large Object Heap (LOH): 这是用于存储大型对象(大于85000字节)的特殊堆。LOH上的对象只有在full GC时才会被回收。
- Roots: GC roots是从代码中可直接或间接访问的对象。在开始垃圾回收时,GC会遍历所有roots以找出在heap上的哪些对象仍然被需要。未被root引用的对象会被视为垃圾并被回收。
- Mark-and-Sweep Algorithm:这是GC的核心算法,用于标记和清理不再需要的对象。所有从GC根开始可达的对象都被认为是“活动的”,而无法到达的对象被视为“死亡的”,可以被回收。
- Finalization Queue:这里保存了需要终结(finalize)的对象列表。当一个对象有终结器,并且该对象没有引用时,此对象就会被放入Finalization Queue。
- FReachable Queue:当GC找到Finalization Queue中的对象并调用其终结器之后,这些对象被移动到 FReachable Queue。从这个队列中的对象可以被再次清理,除非它们在终结器执行过程中复活。
1.3 GC的缺点有哪些?
优点大家都知道,我们直接看看缺点有哪些。
- 性能开销:GC需要在后台运行以查找和清理未使用的对象,这会占用一定的CPU资源。尤其是当GC进行完整的堆清理时,所有的应用线程可能都需要暂停,这被称为"Stop-The-World",可能导致应用程序的响应延迟。
- 内存开销:GC通常会预分配大量内存,以避免频繁执行收集操作。这种预分配策略可能会导致实际使用的内存小于总分配的内存,从而增加内存开销。
- 不确定性:GC的运行时间并不确定。你无法预测何时将开始垃圾回收,或者回收过程需要多长时间。这种不确定性可能对需要实时响应的系统产生影响。
- 碎片化:随着GC的连续工作,内存可能会变得碎片化。虽然.NET的垃圾收集器设计得足够智能,可以减少内存碎片,但在一些情况下,仍可能出现此问题。
- 管理非托管资源:GC主要处理托管对象的内存。对于非托管资源,如文件句柄、数据库连接等,GC不能自动管理,需要开发者显式地释放这些资源。
- Finalizer的问题:如果对象实现了终结器方法(finalizer),则GC必须两次处理该对象,一次是调用其finalizer,一次是清理对象。这增加了垃圾收集的复杂性,并可能导致延迟的内存释放。
日常编码的时候使用GC需要注意什么?
- 避免频繁GC: 频繁地调用 GC.Collect() 可能会导致CPU资源的浪费,因此应尽量避免。只有在必要的情况下(例如,明确知道应用程序即将进入一个内存消耗较少的阶段)才主动触发GC。
- 理解代际收集: .NET GC使用代际收集策略,对象分为0、1、2三代,新创建的对象为0代,经历过一次GC仍然存活的对象则升为更高一代。高代的对象GC的频率更低,所以尽可能避免长寿对象频繁变化。
- Dispose模式: 一些对象(如文件流、数据库连接等)持有非托管资源,虽然它们会在被GC回收时释放资源,但这种时间点不可控,因此对于这类对象需手动调用Dispose方法及时释放资源。也可以利用using代码块或C# 8.0引入的using声明在离开作用域时自动调用Dispose方法。
- Finalizers and SuppressFinalize: 如果一个对象有析构函数(Finalizer),那么在GC回收该对象时需要先调用其Finalizer, 然后在下一轮GC中才会真正回收该对象。这样会导致对象的生存周期延长,增加了内存压力。如果已经手动释放了对象的资源,需要调用GC.SuppressFinalize取消析构函数的调用,加速对象回收。
- 大对象堆: 大小超过85000字节的对象会被直接分配到大对象堆上,多余的大对象会占用大量内存且清理成本高昂。尽量避免创建大对象,特别是生命周期长或者经常改变的大对象。
- 弱引用和条件弱引用: 使用WeakReference类可以创建一个"弱引用",当对象只被弱引用引用时,可以被GC回收。适合用于缓存等场景。
- 注意线程安全问题: GC可能随时发生,编写代码时需要注意多线程下的对象访问安全问题。
GC的工作原理是什么?
- 内存分配:当你创建对象时,.NET运行时会分配一块内存来存储该对象的数据。这些内存块通常分为三代(Generation 0、Generation 1和Generation 2),根据对象的生命周期将其放入不同的代际中。
- 引用计数:.NET并不使用引用计数来跟踪对象的引用关系。相反,它使用一种称为“根”的数据结构来确定哪些对象可以被访问。根包括全局静态变量、本地变量、活动线程的堆栈等。
- 垃圾检测:垃圾回收器定期扫描内存中的对象,从根开始,查找可达对象。所有不可达的对象都被标记为垃圾,可以被回收。
- 标记阶段:在垃圾检测过程中,GC会遍历对象图,标记所有可达的对象。这是一个递归过程,从根开始,沿着引用链一直到达所有可达对象。
- 清除阶段:在标记阶段之后,GC会遍历堆,将所有未被标记为可达的对象清除(即回收)。这些对象的内存将被释放,以便将来的对象分配。
- 压缩(可选):在某些GC算法中,可以选择进行内存压缩。这意味着将存活的对象移到一起,以减少堆的碎片化。这有助于提高内存使用效率。
- 代际回收:GC采用了代际垃圾回收策略。新创建的对象被放入Generation 0,经过一次或多次GC后,仍然存活的对象会晋升到较老的代际(Generation 1或Generation 2)。较老的代际垃圾回收发生得更少,这有助于提高性能,因为大多数对象都是短暂的。
- 性能考虑:GC的工作会在后台异步进行,以最大程度地减少对应用程序性能的干扰。然而,当GC运行时,它可能会导致一些延迟,这是需要注意的性能问题。
总的来说,GC的工作原理是通过标记和清除不可达的对象来回收内存,以便将其用于将来的对象分配。它是.NET框架中的一项关键功能,可以减少内存泄漏的风险,但需要开发人员编写高效的代码以确保良好的性能。
GC是如何标记的?
垃圾回收(GC)标记需要回收的对象的过程通常是通过标记-清除(Mark and Sweep)算法来实现的,以下是该算法的工作方式:
- 初始化标记阶段:在进行标记之前,垃圾回收器会将所有对象标记为“未标记”。这意味着所有对象都被视为候选对象,可能需要被回收。
- 标记可达对象:GC从根对象开始,根对象包括全局变量、本地变量、活动线程的堆栈和静态对象引用。这些根对象被认为是可达对象,它们被标记为“已标记”。
- 遍历引用链:一旦根对象被标记为“已标记”,GC会遍历这些对象引用的其他对象,并继续遍历这些对象引用的对象,以此类推,递归地沿着引用链标记所有可达对象。这个过程通常使用深度优先搜索或广度优先搜索算法来实现。
- 清除未标记对象:一旦所有可达对象都被标记为“已标记”,GC会进入清除阶段。在清除阶段,GC会扫描堆中的所有对象,将未标记的对象(即不可达对象)标记为“待回收”。
- 回收内存:最后,GC会回收所有被标记为“待回收”的对象所占用的内存。这些对象的内存将被释放,以供将来的对象分配使用。
需要注意的是,标记-清除算法存在一些问题,例如会产生内存碎片,并且可能需要多次GC周期来完全回收所有不可达对象。为了解决这些问题,一些GC实现使用标记-整理(Mark and Compact)算法,它会在清除阶段将存活的对象移动到一起,以减少内存碎片。
总之,GC通过标记可达对象并清除不可达对象来回收内存。标记阶段是关键的,因为它确定了哪些对象应该被回收。这个过程确保了内存的有效使用和管理。不同的GC实现可能使用不同的算法来执行这些步骤,但基本的思想是相似的。
GC是如何计划的?
- 触发条件:
- 分配新对象时,如果没有足够的内存。
- 堆中的某一代际(通常是Generation 0或Generation 1)达到一定的阈值。
- 应用程序请求手动触发垃圾回收。
- 垃圾回收不是随机发生的,而是在满足一定条件下触发的。
- 触发条件通常包括:
- GC算法和策略:
- 不同的GC算法和策略决定了垃圾回收的计划和行为。常见的算法包括标记-清除、标记-整理、分代回收等。
- GC策略包括何时执行GC、选择哪个代际进行回收、是否执行并发或并行垃圾回收等。
- 性能目标:
- GC的计划还受到应用程序的性能目标的影响。一些应用程序对低延迟非常敏感,因此可能更频繁地执行小规模的垃圾回收,以减小停顿时间。其他应用程序可能更关注吞吐量,因此可能较少频繁地执行大规模的垃圾回收,以减少回收的开销。
- 手动触发:
- 在某些情况下,应用程序可以根据需要手动触发垃圾回收,以便更好地控制回收的时机。
- 手动触发垃圾回收通常用于特定的场景,例如在内存敏感的操作之前或在应用程序的闲置期间执行回收。
GC是如何清除和压缩的?
- 清除(Collection):
a. 标记阶段:
b. 清理阶段:
- 在清理阶段,GC会遍历整个堆,将未标记的对象标记为“待回收”或“垃圾”。
- GC会释放那些被标记为“垃圾”的对象所占用的内存,使其可以用于将来的对象分配。
- 在标记阶段,GC会从根集合(如全局变量、本地变量、堆栈、静态对象引用等)出发,递归遍历对象图,标记所有可达对象。
- 可达对象被标记为“活动”或“已标记”,而不可达对象保持未标记状态。
- 清除是垃圾回收的核心步骤,用于回收不再被引用的对象占用的内存。
- 清除分为两个主要阶段:标记(Mark)和清理(Sweep)。
- 压缩(Compaction):
a. 移动存活对象:
b. 减少碎片:
- 移动存活对象有助于减少内存碎片,从而降低了分配新对象时发生内存碎片的概率。
- 内存碎片可能导致内存不连续,影响性能和内存使用效率。
- 在压缩阶段,GC会扫描堆中的存活对象,并将它们移动到堆的一侧或另一个连续的内存区域。
- 移动对象需要更新所有引用该对象的指针,以便指向对象的新位置。
- 压缩是一种可选的步骤,通常在标记和清理之后执行,用于减少内存碎片,以提高内存使用的效率。
- 压缩不仅会清理不可达对象,还会将存活的对象移动到一起,以便在内存中形成更大的连续块。
需要注意的是,不是所有的GC实现都执行压缩步骤。压缩阶段通常会引入额外的开销,因此是否执行压缩取决于GC算法和应用程序的性能需求。一些GC算法会选择跳过压缩阶段,而只执行标记和清理以减少停顿时间。其他GC实现会使用标记-整理算法来同时清理和压缩内存。选择合适的策略取决于平衡性能和内存利用的优先级。
GC有哪些模式?
垃圾回收(GC)可以在不同的工作模式下运行:工作站模式(Workstation Mode)和服务器模式(Server Mode)。这两种模式的主要作用是优化垃圾回收的性能,以满足不同类型应用程序的需求。
- 工作站模式(Workstation Mode):
- 工作站模式适用于轻量级应用程序和客户端应用程序。
- 在工作站模式下,GC的主要目标是尽可能快速地回收垃圾,以减少应用程序的停顿时间。
- 这种模式通常使用单个线程执行垃圾回收操作,因此适用于较小的工作负载和单个CPU核心。
- 服务器模式(Server Mode):
- 服务器模式适用于高性能服务器应用程序和多核处理器环境。
- 在服务器模式下,GC的主要目标是最大化吞吐量,即在一段时间内执行尽可能多的工作。
- 这种模式通常使用多个线程来执行垃圾回收操作,以充分利用多核CPU,从而提高性能。
- 服务器模式也使用更复杂的优化技术,如并行和并发垃圾回收,以减少垃圾回收的停顿时间。
选择工作站模式或服务器模式通常取决于应用程序的性质和性能需求。如果你的应用程序是一个简单的桌面应用程序或轻量级服务,工作站模式可能足够了,因为它可以减少垃圾回收的停顿时间。但是,对于高吞吐量的服务器应用程序,服务器模式通常更合适,因为它可以充分利用多核CPU,提高整体性能。
在实际应用中,你可以通过配置来选择使用哪种模式,或者让.NET运行时根据硬件环境和应用程序特性自动选择。这可以通过应用程序的配置文件或代码中的GC设置来实现。例如,在应用程序配置文件中,可以使用 <runtime> 元素的 <gcServer> 和 <gcConcurrent> 子元素来配置垃圾回收的工作模式。
并发模式和非并发模式是两种不同的垃圾回收策略,它们的主要作用是在不同场景下平衡性能和应用程序响应时间。
- 并发模式:
- 并发垃圾回收模式的主要目标是减少对应用程序的停顿时间。
- 在并发模式中,垃圾回收器会与应用程序并行运行,即在应用程序执行的同时进行垃圾回收工作。
- 这意味着应用程序的执行不会因为垃圾回收而停滞,从而提高了应用程序的响应性。
- 并发垃圾回收模式通常在多核处理器上发挥最大优势,因为可以利用多个线程来执行垃圾回收操作,加速回收过程。
- 非并发模式:
- 非并发垃圾回收模式的主要目标是最大化垃圾回收的吞吐量,即在一段时间内执行尽可能多的垃圾回收工作。
- 在非并发模式中,垃圾回收器可能会导致应用程序的停顿,因为它需要在执行回收操作时阻塞应用程序的运行。
- 这种模式通常用于性能要求非常高的服务器应用程序,其中吞吐量更为重要,而不太关心应用程序的停顿时间。
选择并发模式或非并发模式取决于应用程序的性能需求和响应时间要求。以下是一些考虑因素:
- 并发模式适用于:
- 用户界面应用程序:以确保良好的用户体验,不希望出现长时间的停顿。
- 高并发服务器应用程序:以确保应用程序能够处理多个并发请求。
- 非并发模式适用于:
- 高吞吐量服务器应用程序:其中最大化处理请求的数量更为重要,而不太关心少量的停顿时间。
- 启动或初始化阶段:在应用程序启动或初始化时,短暂的停顿可能是可接受的。
GC用到了哪些数据结构、算法和设计模式?
- 数据结构:
- 根集合(Root Set):这是一组数据结构,用于保存全局变量、本地变量、活动线程的堆栈以及静态对象引用,以便确定哪些对象是可达的。
- 对象图(Object Graph):对象之间的引用关系通常以图的形式表示。这可以使用链表、树或其他数据结构来表示。
- 位图(Bitmaps):位图通常用于标记对象的状态,例如标记某个对象是否已被访问过。
- 队列(Queues):队列数据结构常用于存储待处理的对象,例如待标记的对象或待清除的对象。
- 算法:
- 标记-清除(Mark and Sweep):这是GC中最基本的算法之一,用于标记可达对象,然后清除不可达对象。
- 标记-整理(Mark and Compact):这个算法在清除阶段不仅清除不可达对象,还将存活的对象移动到一起,以减少内存碎片。
- 分代回收(Generational Garbage Collection):这个算法根据对象的生命周期将它们分为不同的代际,使用不同的回收策略来管理不同代际的对象。
- 并发和并行垃圾回收(Concurrent and Parallel Garbage Collection):这些算法允许垃圾回收与应用程序的执行并行或并发进行,以减少对应用程序性能的干扰。
- 设计模式:
- 单例模式:垃圾回收器通常是一个单例对象,全局管理内存回收。
- 观察者模式:某些GC实现可能使用观察者模式,允许应用程序监听垃圾回收事件。
- 策略模式:不同的GC实现可能使用不同的回收策略,例如分代回收、标记-清除、标记-整理等,这可以通过策略模式来实现。