返回题库高级 .NET 刷题C# 选择题 · 第 38 / 50 篇

C# 试题 38:GC 与分配

0741 关于 GC 分代,哪项正确?

难度: 进阶

  • A. 新分配的小对象进入第 0 代,第 0 代回收最频繁、成本最低
  • B. 新对象直接分配到第 2 代
  • C. 所有对象都进入 LOH
  • D. 分代与回收频率无关
查看答案与解析

正确答案A

解析: 托管堆按对象年龄分代:新分配的小对象进入第 0 代,第 0 代回收最频繁且只扫描少量对象,成本最低;存活对象逐步晋升到第 1、2 代。LOH 只收大对象,第 2 代是完整回收。

关键点: 短命对象在第 0 代被快速回收,晋升取决于存活。

0742 以下代码哪项正确?

难度: 实战

var obj = new object();
Console.WriteLine(GC.GetGeneration(obj));
  • A. 一定输出 2
  • B. 一定输出 0,永不变化
  • C. 通常输出 0(新对象在第 0 代),但代数是运行时可变的——回收后存活的短命对象会晋升;不能把“当前代数”当成固定契约
  • D. 编译失败:GetGeneration 参数必须可回收
查看答案与解析

正确答案C

解析: 新分配的对象通常落在第 0 代,但一旦发生回收且对象存活,它就会晋升到更高代。GC.GetGeneration 返回的是“此刻”的代数,依赖它做业务判断属于过度耦合 GC 实现细节。

关键点: 代数随回收动态晋升,不是稳定契约。

0743 以下代码哪项正确?

难度: 实战

object o = new object();
GC.Collect(0);
Console.WriteLine(GC.GetGeneration(o));
  • A. 仍为 0
  • B. 为 1:o 仍被局部变量引用,第 0 代回收后存活对象晋升到第 1 代
  • C. 一定为 2
  • D. o 已被回收,访问抛异常
查看答案与解析

正确答案B

解析: GC.Collect(0) 只回收第 0 代。o 仍被局部变量强引用,不会被回收,而是作为存活对象晋升到第 1 代,因此 GetGeneration 返回 1。注意 JIT 可能提前认为引用已死,此处 o 在调用后仍被使用,故保持存活。

关键点: 存活的第 0 代对象在 0 代回收后晋升到第 1 代。

0744 以下代码哪项正确?

难度: 进阶

class Foo
{
    ~Foo() { }
}

void Demo()
{
    new Foo();
    GC.Collect();
    GC.WaitForPendingFinalizers();
}
  • A. Foo 在第 0 代回收时立即释放内存
  • B. 编译失败:析构函数不能定义
  • C. 带终结器的对象永远不会被回收
  • D. 带终结器的对象首次回收时进入终结队列,对象会存活到终结器运行之后才真正可回收;Collect 之后通常还需要 WaitForPendingFinalizers 才能看到终结完成
查看答案与解析

正确答案D

解析: 有终结器的对象首次被判定不可达时并不立即回收:它被放入终结队列,等待终结器线程运行 ~Foo(),之后才真正可回收。GC.Collect() 只把对象送入终结队列,通常需要 WaitForPendingFinalizers() 等待终结完成。

关键点: 终结器让对象多存活一代,回收被推迟。

0745 以下代码输出什么?

难度: 实战

byte[] small = new byte[10_000];
byte[] big = new byte[100_000];

Console.WriteLine(GC.GetGeneration(small) + " " + GC.GetGeneration(big));
  • A. 0 2:大数组进 LOH(大对象堆),LOH 对象按第 2 代处理;小数组先在第 0 代
  • B. 0 0
  • C. 2 2
  • D. 抛 OutOfMemoryException
查看答案与解析

正确答案A

解析: 约 85,000 字节及以上的对象进入 LOH。10,000 字节的数组走小对象堆,初始在第 0 代;100,000 字节的数组在 LOH,GetGeneration 对 LOH 对象返回 2。LOH 默认不压缩。

关键点: 大对象进 LOH 且按第 2 代处理。

0746 以下代码哪项正确?

难度: 进阶

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
  • A. LOH 每次 GC 都会自动压缩
  • B. 默认 LOH 不压缩;设置 CompactOnce 后,下一次完整阻塞式 GC 压缩一次 LOH,随后恢复为不压缩
  • C. 编译失败:LargeObjectHeapCompactionMode 不存在
  • D. CompactOnce 设置后永久生效
查看答案与解析

正确答案B

解析: 移动大对象成本高,LOH 默认不压缩,因此可能碎片化。CompactOnce 让下一次完整阻塞式 GC 压缩一次 LOH 后自动恢复默认;CompactAlways 才持续压缩(吞吐代价大)。

关键点: CompactOnce 是一次性开关,用后自动复位。

0747 以下代码哪项正确?

难度: 进阶

// 服务反复分配并释放 100_000 字节左右的数组
for (long i = 0; ; i++)
{
    var buf = new byte[100_000];
    // 使用后丢弃
}
  • A. LOH 不会碎片化,这段代码永远稳定
  • B. 编译失败:数组长度必须是常量
  • C. LOH 默认不压缩,反复分配/释放大对象可能产生碎片,极端情况下分配大对象抛 OutOfMemoryException;可用 ArrayPool 或定期压缩缓解
  • D. 大数组会被自动放入第 0 代
查看答案与解析

正确答案C

解析: LOH 对象大小不一,分配/释放交错会留下空洞;由于默认不压缩,空洞无法合并,后续大对象分配可能因找不到连续空间而抛 OutOfMemoryException(即使总空闲内存充足)。复用大缓冲(ArrayPool)或按需压缩是常见缓解手段。

关键点: LOH 碎片是真实风险,默认不压缩。

0748 以下代码哪项正确?

难度: 实战

byte[] buffer = ArrayPool<byte>.Shared.Rent(100_000);
// 使用 buffer
ArrayPool<byte>.Shared.Return(buffer);
  • A. Rent 保证返回长度恰好 100_000 的数组
  • B. Rent 返回长度至少为请求值的数组(可能更大),使用后必须 Return 归还,池才能复用;不归还会导致池无法复用并增加 GC 压力
  • C. Rent 的数组使用后会被 GC 自动回收,无需归还
  • D. 编译失败:ArrayPool 不支持大数组
查看答案与解析

正确答案B

解析: ArrayPool.Rent 的契约是“长度至少为请求值”,实际可能更大,调用方必须按 buffer.Length 使用。数组归还后池才能复用;不归还等于泄漏池内缓冲,长期运行会不断向堆申请新数组。

关键点: Rent 可能给更长数组,Return 是池的归还义务。

0749 以下代码哪项正确?

难度: 实战

long before = GC.GetAllocatedBytesForCurrentThread();
var s = new string('a', 100);
long after = GC.GetAllocatedBytesForCurrentThread();

Console.WriteLine(after - before);
  • A. 一定输出 0
  • B. 一定输出 100
  • C. 一定输出 200
  • D. 输出通常大于 0:GetAllocatedBytesForCurrentThread 统计当前线程的托管分配,可用于测量单线程分配;但具体差值受运行时内部分配影响,不能当作单次分配的精确实数
查看答案与解析

正确答案D

解析: new string('a', 100) 确实在堆上分配(字符串内容 + 头部),差值通常为正。但该方法统计的是当前线程累计的托管分配,可能包含运行时顺带发生的分配,因此把它当“单次分配精确值”是过度解读,基准测试应多次采样取稳定值。

关键点: 分配测量看趋势与数量级,别把差值当精确单次分配。

0750 要可靠测量一段代码的 GC 分配,哪项做法最合适?

难度: 进阶

  • A. 使用 BenchmarkDotNet 的 [MemoryDiagnoser],或对比 GC.GetAllocatedBytesForCurrentThread 前后差值并注意 JIT 预热与多次采样
  • B. 用 Stopwatch 测耗时即可
  • C. 查看任务管理器的进程内存
  • D. 用 GC.Collect 的次数代替
查看答案与解析

正确答案A

解析: 分配量是热路径优化的重要指标,测量时先做 JIT 预热,再多次采样取稳定差值;BenchmarkDotNet 的 [MemoryDiagnoser] 内置了这一套流程。进程内存和耗时都无法直接反映 GC 分配压力。

关键点: 分配测量要预热 + 采样,避免把耗时当分配。

0751 以下代码哪项正确?

难度: 实战

int x = 42;
object o = x;    // 装箱

long before = GC.GetAllocatedBytesForCurrentThread();
object o2 = x;   // 再次装箱
long after = GC.GetAllocatedBytesForCurrentThread();

Console.WriteLine(after - before);
  • A. 一定输出 0:int 装箱不分配
  • B. 输出通常大于 0:每次 int 到 object 的装箱都在堆上分配新对象
  • C. 一定输出 4
  • D. 编译失败:int 不能装箱
查看答案与解析

正确答案B

解析: 与 Java 不同,.NET 没有小整数装箱缓存:每次 intobject 都会在堆上分配一个装箱对象。同一值重复装箱也各自独立分配,这正是热路径上应避免的隐式分配。

关键点: 装箱每次都分配,没有整数缓存。

0752 以下代码哪项正确?

难度: 进阶

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

long mem = GC.GetTotalMemory(false);
  • A. 一定为 0
  • B. 抛 InvalidOperationException
  • C. GetTotalMemory(false) 不触发回收,返回托管堆当前已分配字节数的近似值;前面两次 Collect 只是清理现场,不能保证 0
  • D. 编译失败:GetTotalMemory 不存在
查看答案与解析

正确答案C

解析: GC.GetTotalMemory(false) 返回托管堆已分配内存的近似值,不会触发收集。两次 Collect 加 WaitForPendingFinalizers 是“回收尽量干净”的惯用组合,但运行时自身也会占用内存,结果不会精确为 0,生产代码不应依赖此数字。

关键点: GetTotalMemory 是近似值,forceFullCollection 参数决定是否强制回收。

0753 以下代码输出什么?

难度: 实战

int[] arr = ArrayPool<int>.Shared.Rent(100);
ArrayPool<int>.Shared.Return(arr, clearArray: false);

Console.WriteLine(arr.Length);
  • A. 100 或更大的池化数组长度:Return 不会把局部引用置空,数组对象仍可访问
  • B. 抛 NullReferenceException:Return 后引用被清空
  • C. 输出 0
  • D. 编译失败:Return 没有 clearArray 参数
查看答案与解析

正确答案A

解析: Return 只把数组交还池,不会修改调用方的局部引用,arr.Length 仍可读取(大于等于 100)。但归还后该数组可能被其他租用者获得,继续读写属于未定义行为——这是池使用的关键边界。

关键点: Return 不清空引用,归还后不得再使用数组。

0754 以下代码哪项正确?

难度: 实战

public class PooledBuffer : IDisposable
{
    public byte[] Data = ArrayPool<byte>.Shared.Rent(1024);

    public void Dispose() => ArrayPool<byte>.Shared.Return(Data);
}
  • A. Dispose 归还后,外部仍可安全读写 Data
  • B. 归还后该数组可能被池中其他租用者复用,租用方不得再读写 Data;这是对象池的使用边界
  • C. 归还后 Data 自动置为 null
  • D. 编译失败:字段不能由方法调用初始化
查看答案与解析

正确答案B

解析: 对象池的本质是“所有权转移”:租出方持有使用权,归还后使用权回到池中,原租用方再访问就可能踩到别人正在用的数据。调用方必须在 Dispose 后停止访问 Data,这是池化缓冲区的基本纪律。

关键点: 归还即失去使用权,再读写是数据竞争。

0755 以下代码哪项正确?

难度: 实战

for (int i = 0; i < 100_000; i++)
{
    var buffer = new byte[4096];
    // 使用 buffer
}
  • A. 数组分配在栈上,无 GC 压力
  • B. 编译器会自动合并为一次分配
  • C. 每次迭代都会分配新数组,但不会产生 GC 压力
  • D. 每次迭代都在堆上分配新数组,产生 GC 压力;热点路径可改用 ArrayPool 复用
查看答案与解析

正确答案D

解析: 数组是引用类型,new byte[4096] 每次都在堆上分配。循环里反复新建会频繁触发第 0 代回收,形成分配压力;改用 ArrayPool<byte>.Shared.Rent/Return 可复用缓冲。

关键点: 循环内建数组 = 堆分配 + GC 压力,用池复用。

0756 关于对象池的适用性,哪项正确?

难度: 进阶

  • A. 对象池适合创建成本高、复用安全且归还语义清晰的对象(如缓冲区、连接);但会占用常驻内存,归还失败会造成“逻辑泄漏”
  • B. 所有对象都应该池化
  • C. 池化对象无需归还
  • D. 对象池能无条件降低所有场景的延迟
查看答案与解析

正确答案A

解析: 池化的收益来自“高创建成本 + 复用安全”;代价是对象长期驻留内存,且归还路径一旦漏掉(异常、分支遗漏),池就“泄漏”了容量,可用对象越来越少。是否池化要按对象特征与测量决定。

关键点: 对象池是权衡:省创建成本,付常驻内存与归还义务。

0757 以下代码哪项正确?

难度: 进阶

static void Demo()
{
    var obj = new object();
    GC.KeepAlive(obj);
    GC.Collect();
}
  • A. KeepAlive 永久阻止 obj 被回收
  • B. KeepAlive 把 obj 的托管存活期延长到调用点,防止编译器/JIT 提前判定对象死亡;它不固定对象地址,也不阻止调用点之后的回收
  • C. KeepAlive 等价于固定(pin)对象
  • D. 编译失败:GC.KeepAlive 不存在
查看答案与解析

正确答案B

解析: GC.KeepAlive(obj) 本质上是一个“引用仍在使用”的提示,让对象存活到该调用点为止;调用结束后对象可随时被回收。它不固定地址(pinning 要用 fixed/GCHandle),也不能把存活期延长到调用点之后。

关键点: KeepAlive 延寿到调用点,不 pin、不永久保活。

0758 以下代码哪项正确?

难度: 实战

byte[] data = new byte[100];

fixed (byte* p = data)
{
    // 通过 p 使用 data
}
  • A. 两者完全等价
  • B. fixed 需要引用类型才能编译
  • C. fixed 固定对象地址(pinning),GC 在 fixed 块期间不移动对象;KeepAlive 只延长托管存活期到调用点,不固定地址
  • D. KeepAlive 也能固定对象地址
查看答案与解析

正确答案C

解析: fixed 语句把对象钉住(pinned),GC 在块执行期间不会移动它,保证指针有效;GC.KeepAlive 不做地址固定,只是保证托管对象存活到调用点。两者用途不同:传指针用 fixed,防提前回收用 KeepAlive。

关键点: 固定地址用 fixed/GCHandle,KeepAlive 只保存活。

0759 以下代码哪项正确?

难度: 实战

static void UseNative(byte[] buffer)
{
    var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
    try
    {
        // 原生代码使用 buffer
    }
    finally
    {
        handle.Free();
        GC.KeepAlive(buffer);
    }
}
  • A. 这是官方文档推荐的互操作模式:释放固定句柄后调用 KeepAlive,防止 buffer 在原生调用仍在使用时被回收
  • B. 编译失败:GCHandle 不能固定数组
  • C. finally 中不能调用 GC 方法
  • D. KeepAlive 必须在 handle.Free 之前调用
查看答案与解析

正确答案A

解析: 这是 .NET 互操作文档展示的标准模式:GCHandleType.Pinned 保证固定期间地址不变,handle.Free() 解除固定后,GC.KeepAlive(buffer) 防止 buffer 在原生代码仍引用其内存时被提前回收。两步各司其职。

关键点: Free 之后 KeepAlive 收尾,是原生互操作的防御性惯例。

0760 以下代码输出什么?

难度: 进阶

static void Demo()
{
    var big = new byte[100_000_000];
    var wr = new WeakReference(big);

    GC.KeepAlive(big);
    big = null;

    GC.Collect();
    Console.WriteLine(wr.IsAlive);
}
  • A. True:KeepAlive 让 big 永久存活
  • B. False:KeepAlive 只保证存活到调用点,之后 big 失去引用并被完整回收,WeakReference 观察不到存活
  • C. 抛 OutOfMemoryException
  • D. 编译失败:WeakReference 不能引用数组
查看答案与解析

正确答案B

解析: GC.KeepAlive(big) 只把存活期延长到该行;随后 big = null,数组失去强引用。GC.Collect() 做完整回收,100 MB 数组(LOH)被回收,WeakReference 的 IsAlive 为 false。KeepAlive 不是“永久保活”。

关键点: KeepAlive 的作用范围止于调用点,之后照常回收。

官方资料

当前分类

C# 选择题

查看全部分类 →
  1. 36C# 试题 36:线程同步20 题
  2. 37C# 试题 37:并行、PLINQ 与 Channel20 题
  3. 38C# 试题 38:GC 与分配20 题
  4. 39C# 试题 39:反射与特性20 题
  5. 40C# 试题 40:dynamic 与表达式树20 题
ESC

输入关键词开始搜索