.NET Core API使用享元模式内存占用反而更高问题咨询
享元模式占用内存更高的原因主要有4点:
1. 内存统计方式不准确
你用的Process.PrivateMemorySize64统计的是整个进程的私有内存总量,包含了JIT编译生成的机器码、非托管内存开销、托管堆预留空间、运行时临时对象等,和你创建的业务对象实际占用内存差距很大。如果要精准统计托管对象的内存占用,应该使用GC.GetTotalMemory(true),参数传true会先强制执行一次完整垃圾回收,排除未回收的垃圾对象干扰。
2. 未排除JIT编译的额外开销
第一次调用享元模式的接口时,.NET运行时会对TreeFactory类、你用到的LINQ扩展方法(Any、Where)做即时编译,编译生成的机器码会占用进程内存,这部分额外开销被你算到了享元模式的头上,自然会出现内存更高的情况。
3. 享元实现存在额外的临时对象开销
你当前的GetPartTree方法每次调用都会执行两次LINQ遍历:第一次用Any判断对象是否存在,第二次用Where查找对象,每次调用都会生成临时的委托对象、枚举器对象,循环5000次就会生成大量短生命周期的临时对象,这些对象在你统计内存的时候还没被GC回收,也会推高内存数值。
你可以直接用字典做缓存替换List遍历,既提升查询效率,也能避免临时对象开销:
public class TreeFactory { // 用元组做Key,字典做缓存,查询复杂度O(1),无额外遍历开销 private static readonly Dictionary<(string Name, string Bark, string Color), PartTree> _parts = new(); public PartTree GetPartTree(string name, string bark, string color) { var key = (name, bark, color); if (_parts.TryGetValue(key, out var existPart)) { return existPart; } var newPart = new PartTree() { Name = name, Bark = bark, Color = color }; _parts.Add(key, newPart); return newPart; } }
4. 测试逻辑存在设计缺陷
你两个方法中的List<Tree>都是方法内的局部变量,方法执行结束后这些列表、Tree对象都是无引用的垃圾对象,会被GC回收。而享元模式中的PartTree是存在静态缓存中的,有全局引用不会被回收,如果是多次调用接口后统计的结果,普通模式的对象每次调用完都会被回收,而享元模式的缓存会一直留存,也会出现享元内存看起来更高的情况。
正确测试步骤
你可以按照以下步骤调整测试逻辑,就能得到符合预期的结果:
- 测试前先分别调用一次两个接口做预热,完成所有相关代码的JIT编译
- 每次测试前先调用
GC.Collect()、GC.WaitForPendingFinalizers()强制清理所有垃圾对象 - 用
GC.GetTotalMemory(true)代替Process.PrivateMemorySize64做内存统计 - 多次测试取平均值,排除随机干扰
内容的提问来源于stack exchange,提问作者Dr oscar

