核心在于绕过堆内存瓶颈、减少jvm与内核间拷贝、压低gc压力;需协同优化数据库流式游标、nio堆外缓冲写入、jvm及系统参数,并采用分片导出+后台合并策略。

Java中利用NIO实现千万级大表导出为CSV文件,核心不在“用不用NIO”,而在于绕过传统堆内存瓶颈、减少JVM与内核间数据拷贝、压低GC压力。NIO的FileChannel和MappedByteBuffer确实能参与优化,但盲目使用反而容易引发OOM或文件锁问题。真正起效的是整条链路的协同调优:从数据库读取、内存缓冲、写入方式到系统参数配置。
数据库侧:必须启用流式游标(Streaming ResultSet)
这是整个导出链路的起点,也是最容易被忽略的一环。若JDBC驱动把全部结果集加载进JVM堆内存,后续所有NIO优化都无意义。
- MySQL需显式设置:
statement.setFetchSize(Integer.MIN_VALUE),并使用ResultSet.TYPE_FORWARD_ONLY和ResultSet.CONCUR_READ_ONLY - PostgreSQL需开启
setFetchSize(1000)+setAutoCommit(false),配合cursor命名查询 - SQL务必含确定性排序(如
ORDER BY id),避免分页/流式场景下数据错乱或重复
应用层:用NIO做“零拷贝”写入,而非全量映射
MappedByteBuffer对千万行CSV并不友好——它会尝试将整个文件地址空间映射进用户态虚拟内存,易触发OutOfMemoryError: Map failed(实际是mmap系统调用失败,受/proc/sys/vm/max_map_count限制)。更稳妥的做法是:
- 用
FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)获取通道 - 搭配
ByteBuffer.allocateDirect(64 * 1024)创建堆外缓冲区(避免GC扫描),每次填满后channel.write(buffer) - 表头和每行数据先序列化为
byte[],再buffer.put(bytes),避免String→char→byte多次编码 - 禁用
buffer.flip()后未clear()导致写入卡死——这是常见NIO陷阱
JVM与内核参数协同调优
单靠Java代码无法突破系统级限制。以下参数需同步调整:
- JVM启动加
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4M,避免大对象触发Full GC - Linux上增大
vm.max_map_count(如设为262144),预防mmap失败(即使不用MappedByteBuffer,某些NIO实现底层仍依赖) - 提升文件描述符上限:
ulimit -n 65536,防止高并发导出时Too many open files - 关闭ext4日志模式(仅限SSD+非关键环境):
tune2fs -o journal=disable /dev/sdX,降低小块写入延迟
写入策略:放弃“一个文件”执念,改用分片+合并
千万级CSV单文件极易超时、难恢复、占满磁盘I/O。生产环境推荐“分片导出+后台合并”:
- 按主键ID范围分片(如
id BETWEEN ? AND ?),每个分片生成独立CSV临时文件 - 各分片用独立线程+独立
FileChannel写入,互不阻塞 - 最后用
Files.move()或cat part_*.csv > final.csv合并(Linux下cat比Java合并快5倍以上) - 失败时只需重跑对应分片,无需全量重试
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











