简介

CLR的GC模式与JVM的GC模式理念不同,相对JVM的各种小参显得比较简陋,CLR的理念是约定优于配置,并根据程序类型来分提供了几个默认的选项给大家选择。

  1. CS程序默认使用的工作站模式(WorkStation Mode)
  2. BS程序默认使用的服务器模式(Server Mode)



不同的模式,堆的默认大小也有不同。

但在.NET 8 之后,64位程序不管使用什么模式,都采用region管理法, 4M为一个segment。

https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals

WorkStation Mode

工作站模式主要是为了满足基于UI的程序所需的响应性而设计的,因此意味着GC的停顿要尽可能的短。

为了实现GC少停顿,工作站模式有如下几个特征:

  1. 只有一个托管堆

    因为系统不可能只运行一个程序,每一个程序都会占用cpu与内存。因此多个堆反而会降低GC处理效率。从一开始工作站模式就被设定为只能由一个线程处理一个托管堆

  2. segment更小,GC触发频率更高

    高频次的GC,会让GC的处理时间变短,因为对象变少了。GC需要做的准备工作也变少。用小步快跑的方式均衡因 gc 而暂停程序的时间

眼见为实



在WorkStation模式下,托管堆只有一个,且段空间很小(256M)

Server Mode

服务器模式是为了能满足处理并发请求的程序而设计的,这意味着它更看重吞吐量而不是GC停顿

为了平衡吞吐量与GC停顿,服务器模式有如下几个特征:

  1. 堆的数量与CPU 核心数保持一致

    大多数情况下,为了保证吞吐量,服务器只会部署一个程序,因此该程序基本上能“独享”整个CPU与内存。此时多个堆的并行处理能力比单个大堆处理更快。
  2. segment更大,GC频率相对降低

眼见为实



在 Server Mode下,托管堆数量与CPU保持一致,且段空间较大(1GB)

眼见为实:.NET 8 之后,统一采用region管理法, 4M为一个segment。



并发模式与非并发模式

从线程维度来看,GC还分为发模式与非并发模式。

  1. 非并发模式

    顾名思义,不能并发。在GC期间,所有托管线程都被暂停,待GC执行完后,再恢复线程的执行

  2. 并发模式

    顾名思义,支持并发。在GC期间,线程在特定情况下不会被暂停,从而提高吞吐量。

因此CLR提供的总共是2x2=4 ,4种GC模式供人选择。

JVM调优与CLR调优的差别

在JVM的世界中,JVM以GC为中心,提供了非常细颗粒度的配置让用户来自主选择,自由度非常高,但也需要非常了解参数背后的意义,上限很高的同时,下限也很低。

在CLR的世界中,CLR以Application为中心,提供4种模式让用户来自主选择,自由度相对较低,但提高了程序的下限。

举个例子: JVM是SSM,CLR是Spring Boot/Spring Cloud。 将开发人员从配置地狱中拉出来

由于C#支持值类型,因此优化方向主要在代码层面,而不是在CLR层面

CLR调优

虽然说约定大于配置,但也不是完全不能改。

CLR提供了少量参数,能让你调整堆数量,堆大小,是否启用并发GC等。

相对JVM来说,还是偏少。但是对比Framework,已经有了非常显著的进步。

https://docs.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector

CLR调优的重点是使用栈分配来降低堆分配,而不是调整堆配置。

JVM认为一切皆对象,因此绝大多数对象都分配在堆上,所以需要调整堆配置,来达到GC的平衡点。

非并发工作站模式

作为最简单的一种GC模式,前文已经介绍过,不再赘述。

  1. GC发生在用户线程上,没有额外的GC线程
  2. 所有线程都被挂起

眼见为实:GC操作在调用它的线程上





4号线程触发了GC,GC操作直接在4号线程上运行。

眼见为实:其它线程也被暂停



可以看到,6号线程被“劫持”,进入了暂停状态。

202a0:通常表示线程正在运行或可运行状态。

2b220:可能表示线程处于等待状态,比如等待某个资源或事件。

非并发GC服务器模式

  1. 相对WorkStation,最大的不同就是有专门的GC线程来处理,GC线程数与heap数一致,大多数情况下,它们被挂起以等待GC工作。
  2. 所有回收都是非并发GC,多个GC线程并行回收,相对非并发工作站模式。STW的暂停时间要短得多。
  3. 标记阶段也是多个GC线程并行完成,所以标记阶段的阻塞耗时也相对较短。


//用户线程触发GC
GarbageCollectGeneration()
{
//wake up an event
ee_suspend_event.set();
wait_for_gc_done();
} //gc线程:轮询处理
gc_thread_function()
{
while (1)
{
ee_suspend_event.Wait(); SuspendEE();
garbage_collect();
RestartEE();
}
} garbage_collect()
{
generation_to_condemn();
gc1();
} gc1()
{
mark_phase();
plan_phase();
} plan_phase()
{
// actual plan phase work to decide to
// compact or not
if (compact)
{
relocate_phase();
compact_phase();
}
else
make_free_lists();
}

眼见为实:GC线程数与Heap数一致



16核CPU,会创建16个Heap,从而创建16个GC 线程

眼见为实:GC发生在GC线程上

后台GC工作站模式

相对非并发工作站模式,后台GC使用一个单独的线程

前文讲到,如果GC决定压缩回收,因为要移动对象,所以这个时候STW是无法避免的,但如果GC决定标记清除,那就不一样了,完全可以做到托管线程不暂停或者少暂停来减少 STW 的时间,这就是后台GC要解决的问题。

简单来说,当FullGC决定标记清除时,后台GC可以让托管线程的绝大多数时间都处于可运行状态

因为临时回收速度很快,使用并发模式反而性能不高。因此只有在FullGC下才使用并发GC

//线程触发GC
GarbageCollectGeneration()
{
SuspendEE();
garbage_collect();
RestartEE();
} //CLR 发现当前是fullgc 而且是标记清除,觉得有必要走bgc逻辑,接下来会生成 bgc 线程
garbage_collect()
{
generation_to_condemn();
// decide to do a background GC
// wake up the background GC thread to do the work
do_background_gc();
} //初始化 bgc 线程,通过 ee_proceed_event事件 激活 bgc 线程。
do_background_gc()
{
init_background_gc();
start_c_gc (); //wait until restarted by the BGC.
wait_to_proceed();
} //bgc线程执行 gc1 函数,开始做 标记阶段 和 清扫阶段。
bgc_thread_function()
{
while (1)
{
// wait on an event
// wake up
gc1();
}
} gc1()
{
//此时的mark_phase会进入二阶段暂停,而不是暂停所有线程
background_mark_phase();
//bgc在后台做标记清除,此时的托管线程在忙自己的活,处于一种并发状态
background_sweep();
}

如何不干扰正常线程的同时,标记对象?

在正常情况下(STW),标记一个对象,可以利用MethodTable来写入额外信息,但在并发情况下,线程正在使用时,修改MethodTabl是非常危险的,因此并发标记将有关的信息存储在专用的标记数组中,由于GC是该数组的唯一写入/访问者,因此不会出现同步问题。

如何保持root根的一致性?mark_phase二阶段暂停



在并发模式下,之前被标记过的内存,也可能发生变化。此时需要重新标记发生变化的内存来确保无误。此时需要借助WriteWatch机制的来实现重新标记

WriteWatch类似卡表/卡表,只修改单个对象也会导致整个内存页失效。

mark_phase二阶段暂停大致分为如下几个步骤

  1. 线程挂起期间,仅遍历线程栈与终结器的根,遍历完成后进入初始标记阶段
  2. 通过restart_vm()恢复所有托管线程的运行状态,bgc线程继续提取根对象(句柄表),进入并发标记阶段
  3. 再次冻结线程,通过WriteWatch,又回过头重新扫描被修改的内存,进入最终标记阶段
  4. 再次恢复托管线程,bgc在后台完成Free标记

眼见为实:初始标记阶段,GC线程处于SuspendEE

点击查看代码
    internal class Program
{
static void Main(string[] args)
{
Debugger.Break();
Alloc();
Console.ReadLine();
} static List<string> list = new List<string>();
static Random rand = new Random(); static void Alloc()
{
for (int i = 0; i < int.MaxValue; i++)
{
list.Add(string.Join(",", Enumerable.Range(1, 1000))); if (i > 100)
{
list[rand.Next(0, i)] = null;
}
}
}
}

使用bp coreclr!WKS::gc_heap::background_mark_phase 下断点

主线程触发GC,触发了SuspendEE

眼见为实:并发标记阶段,托管线程处于正常状态

使用bp coreclr!WKS::gc_heap::revisit_written_pages 下断点

主线程正常运行,移除了触发线程SuspendEE的标记

眼见为实:最终标记阶段,托管线程处于SuspendEE

使用bp coreclr!WKS::gc_heap::background_sweep 下断点

GC线程又触发了SuspendEE,实现最终标记。



其它线程被暂停

眼见为实:清扫阶段,又将托管线程解冻

使用bp coreclr!WKS::gc_heap::compute_new_dynamic_data 下断点

SuspendEE标记又被清除



其它线程恢复正常运行

后台GC服务器模式

此GC模式是最复杂的一种,但有了前三种GC模式的铺垫,相信你已经摸清了它的套路。

与后台工作站模式类似,但不同的是

  1. 每个托管堆有两个专门用于GC的线程

    1.1 一个是服务器GC线程,负责执行堆中阻塞式GC,对应gc_thread_function函数

    1.2 一个是后台GC线程,负责执行堆中后台式GC,对应bgc_thread_function函数

0,1代等临时回收使用阻塞式GC,因为临时回收速度足够快

FULLGC根据实际情况采用后台GC或者阻塞式GC


GarbageCollectGeneration()
{
//wake up an event
ee_suspend_event.set();
wait_for_gc_done();
} gc_thread_function()
{
while (1)
{
ee_suspend_event.Wait(); SuspendEE();
garbage_collect();
RestartEE();
}
} garbage_collect()
{
generation_to_condemn();
// decide to do a background GC
// wake up the background GC thread to do the work
do_background_gc();
} do_background_gc()
{
init_background_gc();
start_c_gc(); //wait until restarted by the BGC.
wait_to_proceed();
} bgc_thread_function()
{
while (1)
{
bgc_start_event.Wait(); gc1();
}
} gc1()
{
background_mark_phase();
background_sweep();
}

眼见为实

LowLatencyGC

除了以上4种GC模式外,还可以代码来设置延迟模式,来控制GC导致的阻塞频次。

    //
// 摘要:
// Adjusts the time that the garbage collector intrudes in your application.
public enum GCLatencyMode
{
//
// 摘要:
// Disables garbage collection concurrency and reclaims objects in a batch call.
// This is the most intrusive mode. This mode is designed for maximum throughput
// at the expense of responsiveness.
//该模式下,垃圾回收器以批量方式工作,尽可能提高吞吐量。
//在这种模式下,垃圾回收器会等待更多的内存被分配后才进行回收操作,以减少垃圾回收的频率,从而提高应用程序的整体吞吐量。不过,这可能会导致较长的垃圾回收停顿时间
//适用于对响应时间要求不高,但对吞吐量有较高要求的应用程序,例如批量数据处理任务、长时间运行的后台作业等。这些应用可以容忍较长的垃圾回收停顿,因为它们主要关注的是在单位时间内处理更多的数据。
Batch = 0,
//
// 摘要:
// Enables garbage collection concurrency and reclaims objects while the application
// is running. This is the default mode for garbage collection on a workstation
// and is less intrusive than System.Runtime.GCLatencyMode.Batch. It balances responsiveness
// with throughput. This mode is equivalent to garbage collection on a workstation
// that is concurrent.
//这是默认的垃圾回收模式。它在吞吐量和响应时间之间进行了平衡,允许垃圾回收器在应用程序运行过程中进行并发的垃圾回收操作,以减少应用程序的停顿时间。
//在这种模式下,垃圾回收器会更频繁地进行小规模的回收操作,从而保持应用程序的响应性。
//适用于大多数交互式应用程序,如桌面应用程序、Web 应用程序等。这些应用需要及时响应用户的操作,因此不能容忍过长的垃圾回收停顿时间。
Interactive = 1,
//
// 摘要:
// Enables garbage collection that is more conservative in reclaiming objects. Full
// collections occur only if the system is under memory pressure, whereas generation
// 0 and generation 1 collections might occur more frequently. This mode is not
// available for the server garbage collector.
//该模式着重于减少垃圾回收的停顿时间,以提供低延迟的响应。垃圾回收器会更频繁地进行小规模的回收操作,并且会尽量避免长时间的 “STW” 停顿。
//不过,这种模式可能会降低应用程序的吞吐量,因为垃圾回收操作会更频繁地打断应用程序的执行。
//适用于对延迟非常敏感的应用程序,如实时游戏、金融交易系统等。这些应用需要在短时间内对用户输入或外部事件做出响应,因此对垃圾回收的停顿时间有严格的要求
LowLatency = 2,
//
// 摘要:
// Enables garbage collection that tries to minimize latency over an extended period.
// The collector tries to perform only generation 0, generation 1, and concurrent
// generation 2 collections. Full blocking collections may still occur if the system
// is under memory pressure.
//此模式旨在提供持续的低延迟性能,尤其适用于需要长时间保持低延迟的应用场景。与 LowLatency 模式相比,SustainedLowLatency 模式会更加保守地进行垃圾回收,尽量避免触发可能导致长时间停顿的大型垃圾回收操作。
//不过,这也可能会导致内存使用量逐渐增加,因为垃圾回收器不会及时回收所有的垃圾对象。
//用于需要长时间保持低延迟的应用程序,如高频交易系统、实时数据分析系统等。这些应用在长时间运行过程中都需要快速响应,不能容忍明显的垃圾回收停顿。
SustainedLowLatency = 3,
//
// 摘要:
// Indicates that garbage collection is suspended while the app is executing a critical
// path. System.Runtime.GCLatencyMode.NoGCRegion is a read-only value; that is,
// you cannot assign the System.Runtime.GCLatencyMode.NoGCRegion value to the System.Runtime.GCSettings.LatencyMode
// property. You specify the no GC region latency mode by calling the Overload:System.GC.TryStartNoGCRegion
// method and terminate it by calling the System.GC.EndNoGCRegion method.
// 迄今为止可以设置的最强p配置,只要内存足够,该模式会在代码执行期间尝试禁止垃圾回收,
NoGCRegion = 4
}

眼见为实

举个例子1:

    static void Main()
{
// 设置垃圾回收模式为 LowLatency
System.GC.LatencyMode = System.Runtime.GCLatencyMode.LowLatency; // 模拟一些工作
for (int i = 0; i < 1000000; i++)
{
// 创建一些对象
var obj = new byte[1024];
Thread.Sleep(1);
} // 恢复默认的垃圾回收模式
System.GC.LatencyMode = System.Runtime.GCLatencyMode.Interactive;
}

举个例子2:

using System;
using System.Runtime; class Program
{
static void Main()
{
// 创建一个无垃圾回收区域
using (NoGCRegion noGC = new NoGCRegion(NoGCRegionOptions.BestEffort))
{
// 在这个代码块内,尽量避免进行完整的垃圾回收
// 执行对延迟敏感的操作,例如处理高频交易订单
for (int i = 0; i < 1000; i++)
{
// 模拟一些对延迟敏感的工作
Console.WriteLine($"Processing item {i}");
}
}
// 离开无垃圾回收区域后,垃圾回收器恢复正常工作
}
}

https://www.cnblogs.com/cdaniu/p/15927791.html

https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.gclatencymode?view=net-9.0

总结

GC 模式 目标场景 特点 适用场景
Workstation GC 客户端应用 低延迟,单线程或并发 桌面应用、UI 应用
Server GC 服务器应用 高吞吐量,多线程 ASP.NET、Web API 等服务器应用
Background GC 低延迟 + 高吞吐量 允许后台回收,减少停顿 对延迟敏感的服务器或客户端应用
SustainedLowLatency 极低延迟 避免完全垃圾回收,减少停顿 实时系统、游戏、金融交易系统

https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/garbage-collection.md

.NET Core GC模式(gc mode)底层原理浅谈的更多相关文章

  1. Java线上问题排查神器Arthas快速上手与原理浅谈

    前言 当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题: 程序在稳定运行了,可是实现的功能点了没反应. 为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题? 想到可 ...

  2. CSRF漏洞原理浅谈

    CSRF漏洞原理浅谈 By : Mirror王宇阳 E-mail : mirrorwangyuyang@gmail.com 笔者并未深挖过CSRF,内容居多是参考<Web安全深度剖析>.& ...

  3. JAVA CAS原理浅谈

    java.util.concurrent包完全建立在CAS之上的,没有CAS就不会有此包.可见CAS的重要性. CAS CAS:Compare and Swap, 翻译成比较并交换. java.uti ...

  4. 如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈

    在日常业务代码开发中,我们经常接触到AOP,比如熟知的Spring AOP.我们用它来做业务切面,比如登录校验,日志记录,性能监控,全局过滤器等.但Spring AOP有一个局限性,并不是所有的类都托 ...

  5. CAS+SSO原理浅谈

    http://www.cnblogs.com/yonsin/archive/2009/08/29/1556423.htmlSSO 是一个非常大的主题,我对这个主题有着深深的感受,自从广州 UserGr ...

  6. Java中的SPI原理浅谈

    在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则& ...

  7. Mysql锁原理浅谈

    锁类型/引擎 行锁 表锁 页锁 MyISAM 有 InnoDB 有 有 BDB(被InnoDB取代) 有 有 锁的分类 表锁:开销小,加锁快,不会死锁,粒度大,冲突率高,并发低. 行锁:开销大,加锁慢 ...

  8. php模板原理PHP模板引擎smarty模板原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  9. PHP的模板引擎smarty原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  10. Docker 基础底层架构浅谈

    docker学习过程中,免不了需要学习下docker的底层技术,今天我们来记录下docker的底层架构吧! 从上图我们可以看到,docker依赖于linux内核的三个基本技术:namespaces.C ...

随机推荐

  1. 基于 MongoTemplate 实现MongoDB的复杂查询

    MongoDB是典型的非关系型数据库,但是它的功能越来越复杂,很多项目中,我们为了快速拓展,甚至直接使用Mongo 来替代传统DB做数据持久化.虽然MongoDB在支持具体业务时没有问题,但是由于它是 ...

  2. JavaScript 绑定this

    1.临时改变函数调用时this的指向 方法:call()与apply(),第一个参数为此次调用时的this指向,如果不传,则则等同于指定全局对象,后面的参数为函数原本的参数 区别:apply()方法传 ...

  3. Nuxt.js 应用中的 beforeResponse 事件钩子

    title: Nuxt.js 应用中的 beforeResponse 事件钩子 date: 2024/12/5 updated: 2024/12/5 author: cmdragon excerpt: ...

  4. vue 使用 application/x-www-form-urlencoded格式提交数据

    const params = new URLSearchParams();//前端在传参时需要先新建一个URLSearchParams对象,然后将参数append到这个对象中 params.appen ...

  5. ChatGPT自动生成功能测试用例的步骤

    在上一节,我们一起探讨了ChatGPT在功能测试用例生成方面的优势.接下来,我们将探讨ChatGPT自动生成功能测试用例的步骤. 1)    问题定义:让ChatGPT自动生成功能测试用例的第一步是清 ...

  6. 题解:P11007 『STA - R7』Odtlcsu

    有个很显然的结论,题目中的 $x$ 与 $y$ 奇偶性相同. 有个更简单的证明,奇数的平方为奇数,偶数的平方为偶数,所以 $x$ 与 $y$ 奇偶性相同. 思路就显而易见了,考虑构造一个长度为 $y$ ...

  7. Llama 3.2 900亿参数视觉多模态大模型本地部署及案例展示

    Llama 3.2 900亿参数视觉多模态大模型本地部署及案例展示 本文将介绍如何在本地部署Llama 3.2 90B(900亿参数)视觉多模态大模型,并开发一些Use Case,展示其强大的视觉理解 ...

  8. Server check fail, please check server xxx ,port 9848 is available

    [1]如果使用docker安装的nacos服务,2.x版本后增加了 grpc 通信并且增加nacos的集群端口上下偏移1000,创建容器时除了8848还需要把7848.9848都暴露出来.如:-p 7 ...

  9. 动态改变shiro的Principal属性

    因为要保存一些用户名之外的内容在shiro中,所以创建了一个ShiroUser的类,当用户修改了某些属性后,如何动态保存到shiro中: Subject subject = SecurityUtils ...

  10. 有邻App覆盖3000多个小区成杭州用户量最大的邻里分享经济平台 杨仁斌:开创新社区时代

    [浙商创业青云榜] 当下中国大多数的城市社区里,邻居这个词是个淡薄的概念. 2014年,一名阿里高管决心改变现状,辞职创业,深挖社区分享经济,准备用一款手机App"有邻",去敲开陌 ...