File.ReadAllLines()会爆内存,因其将GB级文件全载入内存并转为string[],UTF-8→UTF-16膨胀及对象头开销致托管堆占用近2GB,叠加LOH碎片,在32位或容器内存限制下必然OOM。

直接用 StreamReader 逐行读取,别碰 File.ReadAllLines() 和 File.ReadAllText() —— 这俩在 GB 级文件上基本等于主动触发 OutOfMemoryException。
为什么 File.ReadAllLines() 会爆内存
它不是“读得慢”,是根本没给内存留活路:把整个文件按行切开,每行转成一个 string,全部塞进一个 string[]。1GB UTF-8 日志实际在 .NET 里占约 1.9GB 托管堆(UTF-8 → UTF-16 膨胀 + 每个 string 对象头开销),再加上 LOH(大对象堆)碎片,32 位进程或容器内存限制下基本秒崩。
常见错误现象:
- 本地跑通,上 Kubernetes 就被
OOMKilled - 日志解析服务 GC 时间飙升,CPU 占满但吞吐不涨
- 用
string += line拼全文,Gen2回收迟迟不触发,内存曲线一路冲顶
StreamReader.ReadLine() 怎么写才安全
核心就一条:保持单行生命周期极短,不缓存、不累积、不跨循环引用。
实操建议:
- 必须用
using包裹,确保Dispose()及时释放底层FileStream - 别在循环里把
line全塞进List<string>或拼接成大字符串 - 如果要过滤或转换,直接在循环体内做,处理完立刻丢弃引用
- 编码明确时传入
Encoding.UTF8,避免自动探测开销;若不确定,用StreamReader构造函数的detectEncodingFromByteOrderMarks: true
示例:
using (var reader = new StreamReader("access.log", Encoding.UTF8))
{
string line;
while ((line = reader.ReadLine()) != null)
{
// ✅ 直接解析 IP、时间戳、状态码,写入 DB 或流式输出
// ❌ 不要:lines.Add(line); 或 fullContent += line;
}
}
File.ReadLines() 和手写 StreamReader 的区别在哪
File.ReadLines() 表面看更简洁,但它底层就是封装了一个惰性 StreamReader + yield return,行为几乎一致。关键差异在可控性:
-
File.ReadLines():适合简单遍历,无法控制缓冲区大小、无法显式设超时、无法接入取消令牌(CancellationToken) - 手写
StreamReader:可配bufferSize(如new StreamReader(fs, Encoding.UTF8, true, 64 * 1024)),可接ReadLineAsync(cancellationToken),便于中断长任务 - 性能上无本质差距,但手写方式在 ETL、日志采集等生产场景中调试和容错能力更强
遇到超长行或二进制混入怎么办
StreamReader.ReadLine() 默认按 \n 或 \r\n 切分,但如果某一行长达几百 MB(比如未分行的 Base64 块、单行 JSON),它仍会试图一次性分配那么大一个 string,照样 OOM。
这时必须放弃“按行”假设,改用分块读取 + 手动查找换行符:
- 用
FileStream+Span<byte>读固定大小字节块(如 64KB) - 在
Span<byte>中用IndexOfAny找\n/\r - 对每个完整行片段调用
Encoding.UTF8.GetString(),剩余未闭合部分暂存并拼到下一块开头 - 这种模式绕过了
StreamReader内部的行缓冲逻辑,彻底规避单行内存失控风险
真正的大文件处理,往往卡在“以为自己在流式读,其实某一行已经吃掉全部内存”这个盲点上。


















