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 没有小整数装箱缓存:每次 int 转 object 都会在堆上分配一个装箱对象。同一值重复装箱也各自独立分配,这正是热路径上应避免的隐式分配。
关键点: 装箱每次都分配,没有整数缓存。
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 的作用范围止于调用点,之后照常回收。