Skip to content

PCL2内存优化原理

分析了 MemSwapWorks 类的实现,找到了内存优化的原理。

内存优化原理

一、内存优化生效的核心原因

具体代码
csharp
    public static bool MemorySwap(bool showHint = true)
    {
        if (!_MemSwapLock.Wait(0))
        {
            Context.Warn("检测到正在进行的内存处理,取消当前处理");
            if (showHint) HintWrapper.Show("内存优化尚未结束,请稍等!");
            return false;
        }
        Context.Info("收到内存交换请求,开始处理");
        if (showHint) HintWrapper.Show("正在进行内存优化");

        try
        {
            var before = KernelInterop.GetAvailablePhysicalMemoryBytes();
            Context.Info($"处理前内存量 {ByteStream.GetReadableLength((long)before)}");

            // 添加内存处理提权操作
            PromoteService.Append("mem-swap", result =>
            {
                try
                {
                    if (result is null)
                    {
                        var after = KernelInterop.GetAvailablePhysicalMemoryBytes();
                        Context.Info($"处理后内存量 {ByteStream.GetReadableLength((long)after)}");
                        var diff = Math.Max(0, after - before);
                        var afterStr = ByteStream.GetReadableLength((long)after);
                        var diffStr = ByteStream.GetReadableLength((long)diff);
                        Context.Info($"处理结束,总共处理 {diffStr}");
                        if (showHint) MsgBoxWrapper.Show($"内存优化结束,共优化 {diffStr},目前可用内存 {afterStr}");
                    }
                    else
                    {
                        Context.Error($"内存优化失败\n\n详细信息: {result}", actionLevel: ActionLevel.MsgBoxErr);
                    }
                }
                catch (Exception ex) { Context.Error("内存优化失败", ex); }
                finally { _MemSwapLock.Release(); }
            });

            // 执行操作
            if (PromoteService.Activate()) return true;
            _MemSwapLock.Release();
            if (showHint) MsgBoxWrapper.Show("提权进程启动失败,请允许管理员权限以使用内存优化", "内存优化失败");
            return false;
        }
        catch (Exception)
        {
            _MemSwapLock.Release();
            throw;
        }
    }

先纠正一个常见误解:这段代码并没有去"关闭"或"压缩" Minecraft / Java 真正在用的内存,它做的事情更像是**"提权后,让 Windows 内核重新整理一遍系统物理内存账本"**,把那些"看起来被占用、其实可以释放"的部分让出来,从而让任务管理器/资源监视器里的"已用内存"数字明显下降。

代码里通过 PromoteService 拿到管理员权限后,先开启两个关键特权(SeProfileSingleProcessPrivilegeSeIncreaseQuotaPrivilege),然后按 MemSwapScope 标志位依次调用 NT 内核未公开/半公开的 NtSetSystemInformation 接口,执行 7 项操作。用大白话翻译就是:

代码里的操作通俗解释对"占用 16G"的贡献
EmptyWorkingSets (值 2)所有进程当前的工作集(Working Set,可以理解为"常驻在物理内存里的活跃页面")强制踢到分页文件/备用列表里这是降数字的"主力"。MC + Java 堆原本压在物理内存里几个 G,被踢出去后,物理占用瞬间掉下来
FlushFileCache把系统文件缓存的最大/最小工作集都设成 uint.MaxValue 这种特殊值,触发文件缓存收缩MC 启动时读了大量 jar/资源包,文件缓存可能吃掉 1~3G,这一步清掉
FlushModifiedList (值 3)把"已修改但尚未写回磁盘"的内存页强制写盘并转入备用列表让"脏页"尽快变成可释放页
PurgeStandbyList (值 4)清空"备用列表"——Windows 用来加速二次访问的缓存层这是数字下降的另一大头,能瞬间空出几个 G
PurgeLowPriorityStandbyList (值 5)只清低优先级备用列表,比上面温和同上但范围小一些
RegistryReconciliation让注册表 hive 在内存中的副本做一次回收整理小头,几十 MB 级别
CombinePhysicalMemory调用 Win8.1+ 的"内存合并"功能,把内容相同的物理页合并成一份中等收益,对开多实例/多 Java 进程时效果更明显

为什么"16G → 8G"看起来这么神奇:因为这 7 步合起来,把"工作集 + 文件缓存 + 备用列表 + 修改列表 + 重复页"四五个口袋同时往外掏。其中 EmptyWorkingSetsPurgeStandbyList 是降数字最猛的两手——它们让任务管理器里"已用内存"立刻塌一截。

但要注意:被踢出去的页面并没有消失,它们去了三个地方——① 分页文件(pagefile.sys,硬盘上)、② 备用列表(仍在物理内存,标记为"可回收")、③ 真正被释放。所以"省下来的 8G"里,相当一部分其实是从物理内存挪到了硬盘,并不是无中生有。

二、长期使用适用性判断

结论:适合"按需手动触发",不适合"挂着定时跑 / 一直开着"。

适合的场景:

  • MC 关掉之后点一下,把残留在备用列表里的几个 G 资源包缓存清出来,给下一个程序腾地方 —— 这是最划算的用法。
  • 物理内存吃紧、即将爆掉时救急一次,比直接卡死或触发系统级换页风暴要温和。
  • 偶尔在开 MC 前清一次,让 JVM 拿到一块更"干净"的连续物理内存。

不适合长期/高频使用的原因:

  1. 它对抗的是 Windows 内存管理器的设计哲学。Windows 的理念是"空闲内存就是浪费的内存",备用列表和文件缓存本来就是为了加速;你频繁清空,等于一直在拆系统给你搭好的缓存。
  2. MC 运行中调用,会立刻引发性能抖动。工作集被强制清空后,JVM/MC 下一次访问那些页面时要从 pagefile 重新读回,直接表现为卡顿、掉帧、甚至几秒的假死
  3. "优化效果"会随着程序继续运行迅速回血。你刚清完看到掉了 6G,过几分钟 MC 再加载区块、贴图,又涨回去了,治标不治本。
  4. 真正的内存瓶颈是 JVM 堆大小 -Xmx,那部分根本不会被这段代码动到(堆是 JVM 主动 commit 的活跃内存,踢出去也会被立刻读回)。所以靠它"解决 MC 吃内存"是错位的——根因得改启动参数、装 Sodium/Lithium 等 mod、或者加内存条。

三、潜在使用风险

按真实使用场景从高到低列:

  1. MC/Java 卡顿、掉帧、瞬时无响应(最常见)

    • 在游戏运行中点"内存优化",EmptyWorkingSets 会把 MC 的活跃页面也一起踢走,紧接着区块更新、玩家移动会触发大量缺页中断,从硬盘把页面读回来,体感就是"突然一卡几秒"。机械硬盘上尤其明显。
  2. SSD 写入量增加,寿命有损耗

    • FlushModifiedList 会把脏页强制写回磁盘,EmptyWorkingSets 间接增加 pagefile 读写。每天点几十次这种用法对消费级 SSD 不友好,长期会贡献额外 TBW。
  3. 整机短时间内"假性卡顿"

    • 因为是对全系统所有进程生效,不只是 MC。点一下后浏览器、IDE、QQ 等都可能跟着卡 1~2 秒,因为它们的工作集也被清了。
  4. CombinePhysicalMemory 在极少数情况下兼容性差

    • 这是 Win8.1 之后的功能,老旧驱动、虚拟化环境、部分服务器系统下调用可能返回失败甚至触发蓝屏(历史上有过相关案例)。代码里只是 try/catch 包了一层,不能完全规避。
  5. 依赖未公开 NT API,未来 Windows 大版本升级可能失效

    • NtSetSystemInformation 的这几个 InformationClass(如 SystemMemoryListInformationSystemCombinePhysicalMemoryInformation)是半公开的,微软可以随时改行为甚至禁用。出问题时也无官方文档可查。
  6. 提权风险

    • 每次都要 UAC 提权拿 SeProfileSingleProcessPrivilege / SeIncreaseQuotaPrivilege。这本身不是恶意,但意味着启动器进程在那一刻有较高权限,万一被恶意 mod/插件利用提权链是个攻击面(PCL-CE 本身是开源可信的,主要风险来自第三方扩展场景)。
  7. 掩盖真实问题

    • 数字一掉,用户会以为"内存够用",继续把 -Xmx 设得过大、装更多重型 mod,最后还是要面对真·物理内存不够的现实,可能更晚才意识到该加内存条。
  8. 多次连续点击会被锁挡住

    • 代码里用 SemaphoreSlim(1,1) + Wait(0) 做了串行保护,重复点击会提示"内存优化尚未结束",这点本身是好的——不会真的并发触发,但用户可能误以为"按了没反应"而反复点。

一句话总结:这段代码本质是个**"提权后调用 Windows 未公开内核接口清缓存 + 踢工作集 + 合并重复页"的合集**,降数字非常猛但相当一部分是"挪到硬盘"而非"凭空省出",适合 MC 关闭后偶尔救急清一次,不建议在游戏运行中频繁触发,更不建议挂定时任务长期跑。真要根治"MC 吃 16G",还是得回到 -Xmx、mod 选型和加内存条这三件事上。