使用FileWriter边读边写处理大文件是否会导致内存溢出?
问题解答
你当前实现的边读边写逻辑不会将全量文件加载进内存,只要代码没有逻辑问题,哪怕文件体积远大于JVM堆内存上限,也不会触发内存溢出。
内存占用逻辑说明
你代码里用长度为16的char[]作为读写缓冲区,JVM堆内和文件内容相关的内存占用就只有这16个字符(合计32字节):
- 每次循环最多从磁盘读取16个字符填入缓冲区,立刻把缓冲区里的有效内容写入目标流,下一次读取会直接覆盖缓冲区的旧内容,全程不会留存之前读过的文件数据。
FileWriter内置的流缓冲区、操作系统层面的磁盘页缓存都是固定大小/由系统自动回收的:前者默认只有几KB,不会随文件大小增长;后者属于操作系统内核管理的内存,不占用JVM堆配额,内存不足时系统会自动回收,不会导致应用OOM。
为什么有人边读边写还是会内存溢出?
只有踩了以下几个坑的时候,才会出现大文件读写OOM的问题,和边读边写的模式本身无关:
- 缓冲区设置过大:比如为了追求速度直接开几百MB甚至上GB的缓冲区,本身就超出了JVM堆的可用内存,和文件大小无关。
- 额外留存全量数据:边读边把内容追加到
StringBuilder、List<Byte>这类内存结构里,最后才统一写入,本质还是把全量文件存在内存中,文件多大就占多少内存。 - 误用全量读取API:比如调用
readAllBytes()、readAllChars()这类一次性把整个流读完的方法,自然会把全量内容加载进内存。
你当前代码的可优化点
你现在的写法思路是对的,但有几个影响功能和稳定性的问题:
- 构造
FileWriter时第二个参数传了true,代表开启追加模式,复制文件时如果目标文件已存在,新内容会拼在旧内容后面,不会覆盖原文件,正常复制场景应该去掉这个参数或者传false。 - 流关闭逻辑有缺陷:如果
fileReader.close()抛出异常,后面的fileWriter.close()就不会执行,会导致写入流资源泄漏。建议直接用Java 7+支持的try-with-resources语法自动管理流关闭,不用手写嵌套try-catch。 - 16长度的缓冲区太小,会触发大量native IO调用,复制性能很差,一般把缓冲区大小设为8KB(8192)到64KB就足够,性能和内存占用最均衡。
优化后的基础复制代码参考:
public class FileCopyDemo { public static void main(String[] args) { String sourcePath = "D:\\IdeaProjects\\IOStreamLearning\\assets\\test.txt"; String targetPath = "D:\\IdeaProjects\\IOStreamLearning\\assets\\test_copy.txt"; // try-with-resources会自动在代码块结束后关闭括号里声明的流,哪怕中途抛异常 try (FileReader reader = new FileReader(sourcePath); FileWriter writer = new FileWriter(targetPath)) { char[] buffer = new char[8192]; // 8KB缓冲区 int readLen; while ((readLen = reader.read(buffer)) != -1) { writer.write(buffer, 0, readLen); } } catch (IOException e) { e.printStackTrace(); } } }
大文件读写的可选优化方案
如果要处理GB级以上的超大文件,可以根据场景选更高效的方案:
- 纯二进制文件(图片、视频、压缩包、exe等)不要用字符流(
FileReader/FileWriter),这类流会做字符编码转码,不仅慢还可能损坏文件,改用FileInputStream/FileOutputStream字节流,逻辑和上面的字符流写法一致,把char[]缓冲区换成byte[]即可。 - 追求更高复制性能可以用NIO的
FileChannel零拷贝API,数据直接在内核态完成传输,不需要拷贝到JVM堆内存,比普通流读写性能高30%以上,示例代码:
public void fastCopy(String source, String target) throws IOException { try (FileChannel srcChannel = new FileInputStream(source).getChannel(); FileChannel destChannel = new FileOutputStream(target).getChannel()) { // 直接调用transferTo完成内核态拷贝 srcChannel.transferTo(0, srcChannel.size(), destChannel); } }
内容的提问来源于stack exchange,提问作者Support
相关产品推荐
相关产品推荐

