.NET Framework4.6.1读取超2GB TXT时抛出OutOfMemoryException异常
问题根因
你遇到的OutOfMemoryException和64位系统没有直接关系,核心是当前实现的内存占用模型完全不支持大文件处理:
ReadToEnd()会一次性将全量文件内容加载为字符串,.NET中字符串默认是UTF-16编码,如果原文件是UTF-8编码的2GB文件,仅这一步就需要至少4GB的连续内存- 后续
Split()拆分行数组、StringBuilder拼接输出内容、最终ToString()生成输出字符串,会产生多份全量数据拷贝,峰值内存会达到原文件体积的4~6倍,2GB文件峰值内存需求超过10GB - .NET Framework 4.6.1默认对大对象堆(LOH)有分配限制,且即使是64位进程,内存碎片也很容易导致找不到足够大的连续内存块触发OOM。
第一步:放开基础内存限制(临时缓解方案)
如果暂时不想重构代码,可以先调整项目配置拉高内存上限:
- 右键项目→【属性】→【生成】,取消勾选首选32位,平台目标选择
x64或Any CPU - 在项目的
App.config中添加如下运行时配置,开启大对象支持:
<configuration> <runtime> <!-- 允许总大小超过2GB的数组等大对象 --> <gcAllowVeryLargeObjects enabled="true" /> <!-- 多核环境下开启服务器GC优化内存分配效率 --> <gcServer enabled="true"/> </runtime> </configuration>
注意:这个方案只能提升可处理的文件体积上限,面对更大的文件依然会触发OOM,且内存占用极高,不是长久方案。
第二步:重构为流式逐行处理(根治方案)
你的业务逻辑本身是按行维度处理的,完全不需要加载全量文件,只需要维护一个最多存2行的缓存队列,就可以实现内存占用恒定(仅数MB,和文件大小无关)的处理,同时避免原文件损坏风险:
- 逐行读取原文件,用队列缓存最近读到的2行
- 每读到新行时,将队列中最早存入的行按普通规则判断处理写入临时文件
- 全部行读取完成后,队列中剩余2行分别对应原文件的倒数第二行、最后一行:倒数第二行按特殊规则处理写入,最后一行直接丢弃
- 所有内容写入临时文件成功后,用临时文件替换原文件
优化后的完整实现代码如下:
public static void ModifyFile(string directory, string filename) { string filePath = Path.Combine(directory, filename); string tempFilePath = Path.Combine(directory, filename + ".tmp"); int counter = 1; // 用队列缓存最多2行,用于识别最后两行 Queue<string> lineBuffer = new Queue<string>(2); // 逐行读原文件,逐行写临时文件 using (StreamReader reader = new StreamReader(filePath)) using (StreamWriter writer = new StreamWriter(tempFilePath, false, reader.CurrentEncoding)) { string currentLine; while ((currentLine = reader.ReadLine()) != null) { lineBuffer.Enqueue(currentLine); // 缓存满2行后,取出最早的一行按普通规则处理 if (lineBuffer.Count > 2) { string processLine = lineBuffer.Dequeue(); // 判断第121位开始的2个字符是否为NA,加长度校验避免越界 if (processLine.Length >= 123 && processLine.Substring(121, 2).Equals("NA")) { string processed = ManipulateStringElement(processLine, counter, 22); writer.WriteLine(processed); counter++; } } } // 文件读完后,处理队列里的倒数第二行(队列第一个元素) if (lineBuffer.Count >= 2) { string secondLastLine = lineBuffer.Dequeue(); string processed = ManipulateStringElement(secondLastLine, counter, 22); // 倒数第二行按原逻辑不追加额外换行 writer.Write(processed); } // 队列最后一个元素是原文件最后一行,直接丢弃 } // 处理完成后替换原文件 File.Delete(filePath); File.Move(tempFilePath, filePath); } private static string ManipulateStringElement(string item, int counter, int start) { if (item.Length < start + 9) return item; // 短行直接返回避免越界 // 按位置精准替换,避免原Replace逻辑误替换行内其他相同字符串 return item.Remove(start, 9).Insert(start, GenerateProgressive(counter)); } private static string GenerateProgressive(int counter) { return counter.ToString().PadLeft(9, '0'); }
额外优化点说明
- 移除了原逻辑中冗余的换行符替换:
StreamReader.ReadLine()会自动剥离行尾的\r/\n/\r\n,写入时用WriteLine统一输出系统标准换行,不会出现换行符混乱问题 - 移除了冗余的
Close()调用:using代码块会在资源离开作用域时自动释放流,不需要手动调用关闭 - 增加了行长度校验,避免原文件存在格式错误的短行时触发
ArgumentOutOfRangeException - 采用临时文件写入+最终替换的逻辑,避免处理中途程序崩溃导致原文件被清空损坏
- 去掉了原逻辑中无意义的全量字符串
Split、全量StringBuilder拼接,内存占用和文件大小完全解耦,哪怕是几十GB的文件也可以正常处理 - 修正了原逻辑中用
Replace替换指定位置内容可能误替换行内其他相同字符串的bug,改为按位置精准移除插入
内容的提问来源于stack exchange,提问作者Marduk
相关产品推荐
相关产品推荐

