Lección 2: El recolector de basura
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
La lección 1 contó los bytes de cada asignación. Esta lección sigue a esos bytes después de su asignación: qué montón los recibe, cuándo vuelve a mirarlos el recolector de basura (GC), qué hace con los objetos que sobreviven y cómo su configuración cambia todo eso. Cada comportamiento se observa desde dentro del programa con la clase GC, y después se miden con BenchmarkDotNet las asignaciones de código real de GA.
Los enlaces de GA apuntan al commit a826864; los enlaces del runtime apuntan al commit 4271d88 de dotnet/runtime, con la etiqueta v10.0.12.
Ejecutar el programa de la lección
Sección titulada «Ejecutar el programa de la lección»bash code/csharp-advanced/check.sh # cada lección, comparada con expected/dotnet run --project code/csharp-advanced/Advanced -c Release -- l2 # solo esta lección, después de check.shEl código está en Advanced/Lesson2.cs. Las líneas que empiezan por # dependen de la máquina (tamaños de montón, tiempos de pausa, número de procesadores): la lección las cita tal como salen en la máquina del autor, un Intel Core Ultra 9 285K con 24 núcleos y 64 GB de memoria, y en los runners de la CI cuando difieren de forma interesante.
Generaciones
Sección titulada «Generaciones»El GC de .NET es generacional: supone que la mayoría de los objetos mueren jóvenes, y recolecta los objetos jóvenes con mucha más frecuencia que los viejos (Fundamentos de la recolección de elementos no utilizados). Los objetos nuevos van a la generación 0. Un objeto que sigue referenciado cuando se recolecta su generación se promueve a la siguiente, hasta la generación 2. Una recolección gen0 solo examina gen0 (más las referencias que los objetos viejos tienen hacia los jóvenes, registradas por la barrera de escritura); una recolección gen2, también llamada recolección completa, lo examina todo.
El programa asigna un acorde de tres notas y llama a GC.Collect(), que fuerza una recolección completa y bloqueante (Lesson2.cs#L58-L73):
== Generations: an object that survives a collection is promotednew int[3] generation 0after GC.Collect() generation 1after a second one generation 2after a third one generation 2GC.Collect(0) then new int[3]: generation 0Dos recolecciones bastaron para que el acorde envejeciera. Esa es la trampa de llamar a GC.Collect() en el código de una aplicación: no libera memoria antes de ninguna forma útil, pero promueve todos los objetos vivos, que después esperan a la siguiente recolección completa, la más cara, para liberarse. El programa del curso solo lo llama para hacer visible el comportamiento del GC.
El montón de objetos grandes, al byte
Sección titulada «El montón de objetos grandes, al byte»Los objetos de 85,000 bytes o más van al montón de objetos grandes (LOH), que solo se recolecta con la generación 2 y no se compacta de forma predeterminada (El montón de objetos grandes). La documentación dice «85,000 bytes»; el programa encuentra el límite exacto.
== Large object heap: 85,000 bytes and more, counted with the object headernew byte[ 84,975] object size 85,000 generation 0new byte[ 84,976] object size 85,000 generation 2new byte[1,000,000] object size 1,000,024 generation 2new double[10,622] object size 85,000 generation 2GC.GetGeneration indica que los objetos del LOH son de generación 2. Los dos primeros arrays cuestan 85,000 bytes cada uno, pero solo el segundo es grande. El runtime decide antes de redondear: en gchelpers.cpp#L644-L659, el totalSize de un array es su número de elementos multiplicado por el tamaño de un elemento, más el tamaño base de 24 bytes, y el array va al LOH cuando totalSize >= LARGE_OBJECT_SIZE, que vale 85,000 según gc.h#L105. 24 + 84,975 = 84,999 bytes sigue siendo pequeño, y después se redondea a 85,000. 24 + 84,976 = 85,000 es grande. Un array de 10,622 double llega exactamente a 85,000.
El umbral se puede configurar con GCLOHThreshold (gcconfig.h#L82, configuración del GC), y el programa lee su valor actual en la sección siguiente. Los búferes grandes que se asignan y se descartan en un bucle son una causa clásica de recolecciones gen2; ArrayPool<T>.Shared existe precisamente para reutilizarlos.
El montón de objetos fijados
Sección titulada «El montón de objetos fijados»El código nativo, o una operación de E/S en curso, puede necesitar un array que el GC no debe mover mientras compacta el montón. Fijar un array normal con fixed o con un GCHandle bloquea la compactación a su alrededor y fragmenta la generación 0. Desde .NET 5, GC.AllocateArray<T>(length, pinned: true) asigna en su lugar en un montón aparte, el montón de objetos fijados (POH), que, como el LOH, se recolecta con la generación 2.
== Pinned object heap: GC.AllocateArray(pinned: true)pinned byte[1024] generation 2ordinary byte[1024] generation 0heaps in GCGenerationInfo: 5 (gen0, gen1, gen2, LOH, POH)GCMemoryInfo.GenerationInfo describe cinco montones: las tres generaciones, el LOH y el POH. En la máquina del autor, después de la recolección, el POH contenía 9,232 bytes: el propio runtime ya había colocado ahí 8,184 bytes antes del kilobyte del programa.
Objetos alcanzables: referencias débiles y finalizadores
Sección titulada «Objetos alcanzables: referencias débiles y finalizadores»El GC libera un objeto cuando ya nada alcanzable hace referencia a él: ninguna variable local de un método en ejecución, ningún campo estático, ningún campo de otro objeto alcanzable. Una WeakReference apunta a un objeto sin mantenerlo vivo (Lesson2.cs#L99-L135). Los objetos se crean en un método aparte que no se inserta en línea: en el método que ejecuta GC.Collect(), una variable local podría seguir conteniéndolos, sobre todo en código sin optimizar.
== Reachability: a weak reference does not keep an object aliveonly weakly reachable: IsAlive Falsestill referenced: IsAlive True (4 notes)Una clase con un finalizador (~Finalizable()) sigue un camino más largo. Cuando el GC encuentra inalcanzable uno de sus objetos, todavía no puede liberarlo: lo mueve a una cola, y un subproceso finalizador dedicado ejecuta su finalizador. El objeto, de nuevo alcanzable desde esa cola, lo libera una recolección posterior. Una referencia débil corta deja de seguir al objeto en cuanto es inalcanzable; una referencia débil larga, creada con trackResurrection: true, lo sigue hasta que desaparece de verdad.
== Finalizers: a finalizable object is freed one collection laterafter 1 collection: finalized 1, short weak IsAlive False, long weak IsAlive Trueafter 2 collections: finalized 1, short weak IsAlive False, long weak IsAlive Falsebytes of new Finalizable(): 24, of new object(): 24El objeto finalizable no cuesta más bytes, pero sobrevive a una recolección más, con la posibilidad de ser promovido por el camino, y su registro en la cola de finalización hace más lenta la asignación. Por eso las clases IDisposable que tienen un finalizador llaman a GC.SuppressFinalize(this) en Dispose(), y por eso la mayoría de las clases no deberían tener ningún finalizador: envuelve los identificadores nativos en un SafeHandle, que ya tiene uno.
GC de estación de trabajo, de servidor y concurrente
Sección titulada «GC de estación de trabajo, de servidor y concurrente»El runtime incluye un único GC con varios modos (Recolección de elementos no utilizados de estación de trabajo y de servidor):
- El GC de estación de trabajo, el predeterminado para las aplicaciones de consola y de escritorio, usa un solo montón y recolecta en el subproceso que desencadenó la recolección.
- El GC de servidor, el predeterminado para ASP.NET Core, usa un montón y un subproceso de GC por procesador lógico, con presupuestos de gen0 mucho mayores: más capacidad de proceso, más memoria. Desde .NET 9, DATAS (adaptación dinámica al tamaño de las aplicaciones) está activado de forma predeterminada con el GC de servidor y ajusta el número de montones a la carga.
- El GC concurrente (en segundo plano), activado de forma predeterminada en ambos, ejecuta la mayor parte de una recolección gen2 mientras la aplicación sigue ejecutándose.
Los eliges en el archivo del proyecto (<ServerGarbageCollection>, <ConcurrentGarbageCollection>), en runtimeconfig.json (System.GC.Server, System.GC.Concurrent) o con variables de entorno (configuración del GC). check.sh ejecuta el programa tres veces: con los valores predeterminados, con DOTNET_gcServer=1 y con DOTNET_gcConcurrent=0. GC.GetConfigurationVariables() devuelve la configuración que usa realmente el GC.
== GC modeGCSettings.IsServerGC: FalseGCSettings.LatencyMode: InteractiveGC.MaxGeneration: 2GC.GetConfigurationVariables()["ServerGC"]: FalseGC.GetConfigurationVariables()["ConcurrentGC"]: TrueGC.GetConfigurationVariables()["LOHThreshold"]: 85000GC.GetConfigurationVariables()["GCDynamicAdaptationMode"]: 1== GC modeGCSettings.IsServerGC: TrueGCSettings.LatencyMode: InteractiveGC.MaxGeneration: 2GC.GetConfigurationVariables()["ServerGC"]: TrueGC.GetConfigurationVariables()["ConcurrentGC"]: TrueGC.GetConfigurationVariables()["LOHThreshold"]: 85000GC.GetConfigurationVariables()["GCDynamicAdaptationMode"]: 1== GC modeGCSettings.IsServerGC: FalseGCSettings.LatencyMode: BatchGC.MaxGeneration: 2GC.GetConfigurationVariables()["ServerGC"]: FalseGC.GetConfigurationVariables()["ConcurrentGC"]: FalseGC.GetConfigurationVariables()["LOHThreshold"]: 85000GC.GetConfigurationVariables()["GCDynamicAdaptationMode"]: 1GCSettings.LatencyMode refleja la concurrencia: Interactive con el GC en segundo plano, Batch sin él. GCDynamicAdaptationMode vale 1 en las tres ejecuciones: la opción está activada, pero solo se aplica cuando lo está el GC de servidor. Las líneas que dependen de la máquina muestran lo que cuesta en memoria el GC de servidor:
| Máquina | Procesadores | Montones de estación de trabajo | GCGen0MaxBudget de estación de trabajo |
Montones de servidor | GCGen0MaxBudget de servidor |
|---|---|---|---|---|---|
| Autor (Windows, x64) | 24 | 1 | 18,874,368 | 24 | 209,715,200 |
| CI Linux (x64) | 4 | 1 | 16,777,216 | 4 | 209,715,200 |
| CI Windows (x64) | 4 | 1 | 25,165,824 | 4 | 209,715,200 |
| CI macOS (Arm64) | 3 | 1 | 6,291,456 | 3 | 209,715,200 |
El presupuesto de gen0 es la cantidad de memoria asignada tras la cual empieza una recolección gen0; con el GC de estación de trabajo, el runtime lo deriva del tamaño de la caché del procesador, así que cambia de una máquina a otra. El GC de estación de trabajo no concurrente indicó 134,217,728 bytes en las cuatro máquinas.
Lo que informa GC.GetGCMemoryInfo
Sección titulada «Lo que informa GC.GetGCMemoryInfo»GC.GetGCMemoryInfo() describe la última recolección: su generación, si compactó, sus tiempos de pausa y el tamaño de cada montón antes y después. El programa fuerza una recolección gen2 bloqueante y con compactación mediante GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true), y luego la lee:
== GC.GetGCMemoryInfo after an induced, blocking, compacting collectionGeneration 2, Compacted True, Concurrent False# Index 11, PauseDurations[0] 0.156 ms, PauseTimePercentage 4.3%# HeapSizeBytes 275,648, FragmentedBytes 528, TotalCommittedBytes 1,495,040# TotalAvailableMemoryBytes 68,403,589,120, HighMemoryLoadThresholdBytes 61,563,230,208# gen0: before 4,200, after 560# gen1: before 416, after 0# gen2: before 277,504, after 266,904# LOH: before 0, after 0# POH: before 8,184, after 8,184Esta fue la undécima recolección del programa (Index), pausó el proceso durante 0.16 ms y dejó un montón de 276 KB: un programa pequeño, incluso con las tablas estáticas de GA cargadas. TotalAvailableMemoryBytes es la memoria física que el GC cree que puede usar, o el límite del contenedor cuando lo hay; por encima de HighMemoryLoadThresholdBytes (90% de forma predeterminada), recolecta de forma más agresiva. Los mismos valores se exportan como métricas del runtime, como dotnet.gc.last_collection.heap.size, que un panel de supervisión puede seguir en producción; la lección 19 las usa.
Los objetos de vida corta son baratos, hasta que dejan de serlo
Sección titulada «Los objetos de vida corta son baratos, hasta que dejan de serlo»La hipótesis generacional hace baratos los objetos de vida corta: una recolección gen0 solo examina los objetos vivos, y saltarse los muertos no cuesta nada. El programa asigna un millón de arrays pequeños, y cada uno se conserva solo hasta que los dieciséis siguientes lo reemplazan (Lesson2.cs#L160-L177):
== Allocation budget: many short-lived objects, few collectionsallocated 32,000,152 bytes for 1,000,000 arrays of 2 ints, 16 still referenced32 MB de basura provocaron una recolección gen0 en la máquina del autor, en la CI de Linux y en la CI de Windows, y cinco en la CI de macOS, cuyo presupuesto de gen0 es de 6 MB. No se promovió ninguno. El coste vuelve cuando los objetos de vida corta se hacen grandes (el LOH), o viven justo lo suficiente para ser promovidos a gen1 o gen2: una caché con ámbito de solicitud, un búfer que sobrevive a un await. Entonces el GC tiene que copiarlos y, tarde o temprano, ejecutar una recolección completa para liberarlos.
Medir las asignaciones con BenchmarkDotNet
Sección titulada «Medir las asignaciones con BenchmarkDotNet»GC.GetAllocatedBytesForCurrentThread() sirve para una llamada. Para código que se ejecuta millones de veces, el [MemoryDiagnoser] de BenchmarkDotNet informa, por operación, de los bytes asignados y del número de recolecciones gen0, gen1 y gen2 por cada 1,000 operaciones. Benchmarks/AllocationBenchmarks.cs mide las asignaciones de la lección 1, incluido PitchClassSetId.ItemsSpan de GA frente a un array en caché:
[MemoryDiagnoser]public class AllocationBenchmarks{ private static readonly PitchClassSetId[] CachedIds = [.. PitchClassSetId.Items];
[Benchmark(Baseline = true)] public int GaPitchClassSetIdItemsSpan() => PitchClassSetId.ItemsSpan.Length;
[Benchmark] public int CachedArraySpan() => new ReadOnlySpan<PitchClassSetId>(CachedIds).Length;cd code/csharp-advanceddotnet run -c Release --project Benchmarks -- --filter "*AllocationBenchmarks*"En la máquina del autor, sin nada más en ejecución:
BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.9445/25H2/2025Update/HudsonValley2)Intel Core Ultra 9 285K 3.70GHz, 1 CPU, 24 logical and 24 physical cores.NET SDK 10.0.112 [Host] : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v3 DefaultJob : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v3| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|
| GaPitchClassSetIdItemsSpan | 1,158.0046 ns | 70.7784 ns | 208.6918 ns | 1,173.2269 ns | 1.035 | 0.28 | 0.8698 | 16408 B | 1.000 |
| CachedArraySpan | 0.0899 ns | 0.0292 ns | 0.0860 ns | 0.0770 ns | 0.000 | 0.00 | - | - | 0.000 |
| GaPitchClassItemsSpan | 0.0543 ns | 0.0258 ns | 0.0761 ns | 0.0130 ns | 0.000 | 0.00 | - | - | 0.000 |
| VoicingWithSplit | 60.0266 ns | 1.2168 ns | 2.3151 ns | 60.1405 ns | 0.054 | 0.01 | 0.0114 | 216 B | 0.013 |
| VoicingWithSpans | 46.0624 ns | 0.9406 ns | 2.2536 ns | 46.1613 ns | 0.041 | 0.01 | - | - | 0.000 |
| Format | 19.1902 ns | 0.4152 ns | 0.8759 ns | 19.1456 ns | 0.017 | 0.00 | 0.0030 | 56 B | 0.003 |
| Interpolate | 12.4961 ns | 0.3073 ns | 0.7479 ns | 12.6991 ns | 0.011 | 0.00 | 0.0017 | 32 B | 0.002 |
Los tiempos proceden de una sola máquina y una sola ejecución, y la CI nunca los compara. Las asignaciones no dependen de la máquina:
GaPitchClassSetIdItemsSpanasigna 16,408 bytes en cada llamada, la copia de 4,096 identificadores que encontró la lección 1, y provoca 0.87 recolecciones gen0 por cada mil llamadas: un bucle que leeItemsSpanmil veces causa casi una recolección. Además tarda alrededor de un microsegundo, y sus tiempos están dispersos (BenchmarkDotNet advierte de que la distribución es multimodal), porque algunas llamadas pagan una recolección y otras no.CachedArraySpany el propioPitchClass.ItemsSpande GA, que devuelve un array en caché, no asignan nada. Sus medias, una décima de nanosegundo, están por debajo de lo que BenchmarkDotNet puede medir: indica ZeroMeasurement, «indistinguible del método vacío». El JIT los redujo a la lectura de una longitud.- Analizar el voicing
"x 3 2 0 1 0"constring.Splitasigna 216 bytes (el array y seis cadenas); la versión con spans no asigna nada y es alrededor de una cuarta parte más rápida. Formatasigna 56 bytes, el objeto con boxing y la cadena, eInterpolate32, solo la cadena, como se midió en la lección 1. La columnaGen0convierte esos bytes en recolecciones: 3 por millón de llamadas paraFormat.
Lee primero Allocated: es exacto y reproducible. Lee Mean con su Error y su StdDev, y con las advertencias que BenchmarkDotNet imprime debajo de la tabla. La lección 4 trata de cómo obtener tiempos en los que puedas confiar.
Si conoces Spring y Reactor
Sección titulada «Si conoces Spring y Reactor»Las dos plataformas son generacionales, ambas sustituyeron la mayoría de sus opciones de ajuste por ergonomía, y no comparten casi nada de vocabulario. La tabla conecta lo que esta lección midió con lo que leerías en una guía de ajuste del GC.
| .NET | HotSpot |
|---|---|
| generaciones 0, 1 y 2 | una generación joven (edén y supervivientes) y una generación antigua; G1 asigna a cada región uno de esos papeles |
| el montón de objetos grandes, desde 85 000 bytes | los objetos humongous de G1, «larger or equal the size of half a region», asignados en regiones contiguas de la generación antigua. El tamaño de región es ergonómico: unas 2 048 regiones, de hasta 32 MB cada una |
| el montón de objetos fijados, un montón aparte | nada equivalente. La fijación es transitoria y se limita a una región crítica de JNI; desde la JEP 423 (JDK 22), G1 fija la región en vez de desactivar la recolección, y registra un fallo de evacuación con el motivo Pinned |
| GC de estación de trabajo o de servidor, elegido por configuración | un recolector entre G1 —el predeterminado, y desde la JEP 523 en todos los entornos, no solo en máquinas de tipo servidor—, Parallel, Serial y ZGC |
| DATAS dimensiona el montón según la carga | la ergonomía de cada recolector, más el -XX:SoftMaxHeapSize de ZGC: un límite blando que intenta respetar, «but is still allowed to grow beyond this limit up to the maximum heap size» |
las duraciones de pausa leídas en GC.GetGCMemoryInfo() |
un objetivo declarado por recolector: el de ZGC es «Pause times should not exceed 1 millisecond» (JEP 439), más estricto que los 10 ms de su primera JEP |
| los finalizadores, y el hilo de finalización | finalize(), obsoleto y marcado para eliminación desde el JDK 18 (JEP 421) y todavía activo por defecto; Cleaner y PhantomReference en su lugar |
WeakReference<T>, ConditionalWeakTable |
WeakReference, PhantomReference, y SoftReference, «cleared at the discretion of the garbage collector in response to memory demand» — .NET no tiene referencia blanda |
GC.GetGCMemoryInfo() y GC.GetConfigurationVariables(), desde dentro del proceso |
GarbageCollectorMXBean con getCollectionCount() y getCollectionTime(), -Xlog:gc, y JDK Flight Recorder |
[MemoryDiagnoser]: bytes y recolecciones de gen0 por operación |
el -prof gc de JMH, cuyo gc.alloc.rate.norm son los bytes por operación |
Una fila merece cuidado, porque la lección 1 la midió aquí. .NET 9 y 10 asignan en la pila las cajas y los objetos pequeños que no escapan. El análisis de escape de HotSpot está documentado de forma más estrecha: para un objeto que «does not escape», el compilador de servidor «eliminates the scalar replaceable object allocations and the associated locks from generated code» — el objeto se descompone en sus campos en vez de trasladarse a la pila. El resultado medido es el mismo, cero bytes asignados; la redacción de la garantía, no.
Para un servicio reactivo, el número en el que insiste esta lección —los bytes por operación— importa aún más que aquí. Un pipeline de Reactor asigna un objeto suscriptor por operador en cada suscripción, y un servidor WebFlux se suscribe una vez por petición, así que las asignaciones crecen con el tráfico de un modo que un benchmark aislado no muestra. Reactor no publica cifras de asignación propias; -prof gc sobre tu propio pipeline es la manera de obtenerlas.
Ejercicios
Sección titulada «Ejercicios»- ¿Cuál es el
char[]más pequeño que va al montón de objetos grandes? GC.TryStartNoGCRegionpide al GC que no recolecte mientras una sección crítica asigna menos de una cantidad dada. Inicia una región de 1 MB, asigna mil arrays de 100 bytes e indica el modo de latencia y el número de recolecciones gen0 dentro de la región.GC.GetGeneration("C major")no devuelve ni 0, ni 1, ni 2. ¿Qué devuelve y por qué? Prueba también contypeof(PitchClass).
Soluciones
-
Un
charocupa 2 bytes, y el tamaño base del array es de 24 bytes: 24 + 2 × n ≥ 85,000 da n = 42,488. Con un elemento menos, el array se queda en la generación 0.1. new char[42,487] generation 0, new char[42,488] generation 2 -
La región se inicia, el modo de latencia pasa a ser
NoGCRegion, y mil arrays de 128 bytes cada uno (24 + 100, redondeado) caben en el 1 MB reservado: ninguna recolección.EndNoGCRegionrestaura el modo anterior. Asignar más de la cantidad solicitada termina la región en silencio, y entoncesEndNoGCRegionlanzaInvalidOperationException.2. TryStartNoGCRegion(1 MB) True, LatencyMode NoGCRegion, gen0 collections for 1,000 arrays 0after EndNoGCRegion: LatencyMode Interactive, 16 arrays kept -
Devuelve
int.MaxValue. Desde .NET 8, los literales de cadena, los objetosRuntimeTypey algunos otros objetos que el runtime sabe que vivirán para siempre se asignan en un montón congelado, ajeno al GC, que el GC nunca examina (nota sobre el cambio importante). Una cadena construida en tiempo de ejecución con los mismos caracteres es un objeto gen0 normal. El código que usaba la generación como índice de un array falla con estos objetos.3. GC.GetGeneration("C major") 2147483647, of new string('C', 7) 0, of typeof(PitchClass) 2147483647
Puntos clave
Sección titulada «Puntos clave»- Los objetos nuevos empiezan en la generación 0; cada recolección a la que sobreviven los promueve, hasta la generación 2.
GC.Collect()en el código de una aplicación sobre todo promueve objetos vivos. - Un array va al montón de objetos grandes cuando su tamaño sin redondear, 24 bytes más sus elementos, llega a 85,000 bytes:
byte[84,976],char[42,488],double[10,622]. El LOH y el montón de objetos fijados se recolectan con la generación 2. - Un finalizador retrasa la liberación al menos una recolección. Prefiere
IDisposableySafeHandle. - El GC de servidor cambia memoria por capacidad de proceso: un montón por procesador y un presupuesto de gen0 de 200 MB, ajustado por DATAS desde .NET 9. Los presupuestos del GC de estación de trabajo dependen de la máquina.
GC.GetGCMemoryInfo()yGC.GetConfigurationVariables()muestran lo que hizo el GC y cómo está configurado, desde dentro del proceso.[MemoryDiagnoser]convierte «esto asigna memoria» en bytes y recolecciones por operación.PitchClassSetId.ItemsSpande GA aparece ahí como 16,408 bytes por llamada y casi una recolección gen0 por cada mil llamadas.
Fuentes
Sección titulada «Fuentes»- Microsoft Learn: Fundamentos de la recolección de elementos no utilizados, El montón de objetos grandes, GC de estación de trabajo y de servidor, DATAS, Opciones de configuración del GC, Referencias débiles, Finalizadores,
GC.GetGenerationpuede devolverInt32.MaxValue, Métricas del runtime de .NET. - dotnet/runtime en
v10.0.12(commit4271d88):gchelpers.cpp,gc.h,gcconfig.h. - BenchmarkDotNet: Diagnosticadores.