Wrayの知识库 Wrayの知识库
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
  • Java章节编写规范
  • Java基础

  • Java集合

  • Java并发

  • Java IO

    • Java IO概述
    • IO基础

    • 常用IO流

    • 文件与目录操作

    • Java NIO

    • 高级主题

    • 实践与性能优化

    • 面试与设计题

    • IO实践速查

      • Java IO知识面总复盘
        • 1. IO 选型总图
        • 2. 字节流与字符流
        • 3. 缓冲的作用
        • 4. File、Path、Files 怎么选
        • 5. NIO 三件套
        • 6. BIO、NIO、AIO 怎么理解
        • 7. 大文件处理
        • 8. 可靠写文件
        • 9. 序列化安全
        • 10. IO 性能排查
        • 11. 开发检查清单
        • 12. 面试表达
        • Tips 快问快答
  • JVM

目录

Java IO知识面总复盘

Java IO 的知识点很多,容易学成一堆类名。真正用于开发和面试时,需要把“数据来源、数据单位、访问方式、资源生命周期、性能瓶颈、异常边界”串成一张图。

这一节用于复盘整个 Java IO 章节:新手可以用它建立全局地图,高级开发者可以用它做选型清单,资深开发者可以用它审查线上 IO 代码是否可靠。

# 1. IO 选型总图

先问六个问题:

数据是什么
  ├─ 二进制:InputStream / OutputStream
  └─ 文本:Reader / Writer + Charset

数据在哪里
  ├─ 文件:FileInputStream / Files / FileChannel
  ├─ 内存:ByteArrayInputStream / ByteBuffer
  ├─ 网络:Socket / SocketChannel
  └─ 进程间:PipedInputStream / PipedOutputStream

数据多大
  ├─ 小文件:Files.readString/readAllBytes
  ├─ 中等文件:缓冲流分块读写
  └─ 大文件:流式处理、FileChannel、MappedByteBuffer

访问方式
  ├─ 顺序读写:普通流
  ├─ 随机访问:RandomAccessFile / FileChannel position
  └─ 多路复用:Selector

是否要高性能
  ├─ 减少系统调用:BufferedInputStream
  ├─ 减少拷贝:transferTo / transferFrom
  └─ 减少线程阻塞:NIO / Reactor

失败后怎么办
  ├─ 资源关闭
  ├─ 临时文件清理
  ├─ 重试与幂等
  └─ 半写入恢复

IO 选型不是记 API,而是先识别场景,再选择最小够用且边界清晰的工具。

# 2. 字节流与字符流

字节流直接处理原始字节,字符流负责把字节按字符集解码为字符。

场景 推荐 原因
图片、音频、视频、压缩包 字节流 数据不是文本,不能按字符解码
网络协议原始报文 字节流 协议通常以字节定义字段
配置文件、CSV、日志文本 字符流 需要按字符集处理文本
文件编码转换 转换流 明确指定源编码和目标编码
混合二进制和文本 字节流 + 协议解析 避免字符流破坏字节结构

典型错误:

try (Reader reader = new FileReader("image.png")) {
    // 错误:图片不是文本,字符解码会破坏内容
}

正确做法:

try (InputStream input = Files.newInputStream(Path.of("image.png"))) {
    byte[] buffer = new byte[8192];
    while (input.read(buffer) != -1) {
        process(buffer);
    }
}

文本处理必须显式指定字符集。

try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        handle(line);
    }
}

不要依赖平台默认编码,否则同一份代码在开发机、Linux 服务器、容器镜像中可能表现不同。

# 3. 缓冲的作用

缓冲流的价值是减少频繁系统调用和磁盘/网络访问。

无缓冲:
程序每读 1 个字节 -> 调用底层 IO -> 成本高

有缓冲:
一次读取一批数据到内存 -> 程序从缓冲区消费 -> 成本低
API 适合场景 注意点
BufferedInputStream 二进制顺序读取 缓冲区大小结合数据量调整
BufferedOutputStream 二进制顺序写入 写完要 flush 或关闭
BufferedReader 文本按行读取 适合日志、CSV、配置
BufferedWriter 文本批量写入 关闭前会刷新

缓冲不是越大越好。过大的缓冲区会占用更多内存,过小则系统调用频繁。常见起点是 8KB、16KB、64KB,再通过压测调整。

# 4. File、Path、Files 怎么选

File 是早期 API,既表示路径,也提供文件操作。NIO.2 引入 Path 和 Files 后,路径表达和文件操作被拆得更清晰。

能力 File Path / Files
路径表达 支持 更现代,和文件系统更贴合
符号链接 支持有限 支持更完整
文件属性 较弱 支持 POSIX、owner、ACL 等
遍历目录 listFiles walkFileTree、Files.walk
异常信息 很多方法返回 boolean 抛出具体异常
推荐程度 维护旧代码会遇到 新代码优先使用

新代码推荐:

Path path = Path.of("/data/app/config.yml");

if (Files.exists(path) && Files.isRegularFile(path)) {
    String content = Files.readString(path, StandardCharsets.UTF_8);
    parseConfig(content);
}

处理文件操作时,不要只看 exists。文件系统是共享资源,检查和使用之间可能被其他进程修改,所以关键操作仍要处理异常。

# 5. NIO 三件套

NIO 的核心是 Buffer、Channel、Selector。

Channel 负责连接数据源
Buffer 负责暂存数据
Selector 负责监听多个 Channel 的就绪事件
组件 作用 类比
Buffer 一块可读写的内存区域 货物暂存区
Channel 数据传输通道,可读可写 运输通道
Selector 监听多个通道事件 调度中心

Buffer 状态转换:

写入数据
  │ put/read from channel
  ▼
position 增加,limit 为 capacity
  │ flip
  ▼
读取数据
  │ get/write to channel
  ▼
position 增加,limit 为原写入位置
  │ clear/compact
  ▼
继续写入

NIO 常见坑:

  • 忘记 flip,导致读不到数据。
  • 忘记处理半包、粘包。
  • 以为 write 一次一定写完。
  • OP_WRITE 长期开启导致 CPU 空转。
  • Selector 空轮询没有退避和重建策略。

# 6. BIO、NIO、AIO 怎么理解

模型 线程模型 适合场景 风险
BIO 一个连接通常占用一个线程 连接少、逻辑简单 高连接数下线程成本高
NIO 少量线程监听多个连接 高并发网络服务 编码复杂,需要处理事件和缓冲
AIO 操作系统完成后回调 异步文件/网络场景 平台差异和生态使用较少

BIO 不等于低性能,NIO 也不等于一定高性能。真正的性能取决于连接数、请求耗时、数据量、线程模型、协议设计和业务处理能力。

Netty 等框架的价值在于把 NIO 的复杂细节封装成稳定的事件模型,让业务代码关注协议和处理逻辑。

# 7. 大文件处理

大文件最重要的原则是不要一次性加载到内存。

错误示例:

byte[] bytes = Files.readAllBytes(path);

如果文件很大,这会直接带来内存压力甚至 OOM。

流式处理:

try (InputStream input = Files.newInputStream(path);
     OutputStream output = Files.newOutputStream(target)) {
    byte[] buffer = new byte[1024 * 64];
    int len;
    while ((len = input.read(buffer)) != -1) {
        output.write(buffer, 0, len);
    }
}

按行处理大文本:

try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
    lines
            .filter(line -> !line.isBlank())
            .forEach(this::handleLine);
}

注意 Files.lines 返回的是惰性流,必须关闭。用 try-with-resources 可以确保底层文件句柄释放。

# 8. 可靠写文件

可靠写文件要考虑“写到一半失败”的情况。

推荐流程:

写入临时文件
  │
  ▼
flush / close
  │
  ▼
必要时 fsync
  │
  ▼
原子移动到目标路径
  │
  ▼
清理旧文件或临时文件

示例:

Path target = Path.of("/data/config.json");
Path temp = Files.createTempFile(target.getParent(), "config-", ".tmp");

try {
    Files.writeString(temp, content, StandardCharsets.UTF_8);
    Files.move(temp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
    Files.deleteIfExists(temp);
    throw e;
}

如果文件系统不支持 ATOMIC_MOVE,需要捕获异常并设计降级策略。

# 9. 序列化安全

Java 原生序列化可以把对象图写成字节,但生产环境要谨慎。

风险包括:

  • 反序列化安全漏洞。
  • serialVersionUID 兼容性问题。
  • 对象图过大导致内存压力。
  • 跨语言能力弱。
  • 数据格式不透明,不利于长期演进。

常见替代方案:

场景 推荐
Web API JSON
高性能内部 RPC Protobuf、Thrift、FlatBuffers
配置文件 YAML、JSON、Properties
本地缓存 JSON、二进制协议、数据库
Java 内部短期传输 谨慎使用原生序列化,并设置过滤器

如果必须使用 Java 序列化,应限制反序列化类型,设置 JEP 290 对象输入过滤器,并避免反序列化不可信数据。

# 10. IO 性能排查

IO 慢时不要只盯 Java 代码,还要看系统和下游。

排查路径:

确认慢在哪里
  ├─ 磁盘读写慢
  ├─ 网络传输慢
  ├─ 下游服务慢
  ├─ 文件句柄耗尽
  ├─ 线程池阻塞
  └─ GC / 内存压力

常见指标:

指标 含义
文件大小 决定是否能一次性读取
读写耗时 判断是否需要缓冲或分块
吞吐量 MB/s 或 requests/s
P95/P99 判断尾延迟
打开文件数 是否接近系统限制
线程状态 是否大量阻塞在 IO
GC 日志 大量 byte[] 分配会触发 GC 压力

优化顺序:

  1. 先确认瓶颈是磁盘、网络、CPU、锁还是下游。
  2. 再减少不必要的数据复制和对象分配。
  3. 然后调整缓冲区、线程池、批量大小。
  4. 最后考虑 NIO、零拷贝、内存映射等高级方案。

# 11. 开发检查清单

写 IO 代码前后检查:

  1. 文件是文本还是二进制。
  2. 文本是否显式指定字符集。
  3. 文件大小是否可能超过内存承受范围。
  4. 是否使用 try-with-resources 关闭资源。
  5. 写文件失败时是否会留下半文件。
  6. 是否处理路径不存在、权限不足、磁盘满。
  7. 上传文件是否校验大小、类型和路径穿越。
  8. 网络 IO 是否设置超时。
  9. NIO 写出是否处理未写完。
  10. 序列化数据是否可信。
  11. 是否需要缓冲、分块、限流或背压。
  12. 是否有日志、指标和异常上下文。

# 12. 面试表达

如何系统回答 Java IO?

可以这样回答:

我会先从方向和数据单位讲起:输入/输出、字节流/字符流。字节流适合二进制,字符流适合文本但必须关注编码。然后说明 Java IO 通过装饰器模式组合缓冲、转换、数据、对象等能力。文件操作上,新代码优先使用 Path 和 Files。进阶部分要讲 NIO 的 Buffer、Channel、Selector,以及 BIO/NIO/AIO 的线程模型差异。最后补充生产实践:try-with-resources、显式字符集、大文件流式处理、可靠写文件、序列化安全和性能排查。

# Tips 快问快答

Q:字节流和字符流怎么选? A:二进制用字节流,文本用字符流,并且文本必须明确字符集。

Q:为什么不建议使用 FileReader? A:它依赖平台默认编码,跨环境容易乱码;推荐 Files.newBufferedReader(path, charset)。

Q:缓冲流解决什么问题? A:减少频繁底层 IO 调用,提高顺序读写效率。

Q:read 返回值为什么必须判断? A:返回值表示实际读取字节数,不能假设缓冲区每次都会被填满。

Q:大文件为什么不能 readAllBytes? A:它会把整个文件加载到内存,文件过大时容易造成 OOM。

Q:NIO 的 Buffer 为什么要 flip? A:写入后要切换到读取模式,flip 会把 limit 设为写入位置并把 position 归零。

Q:Selector 适合解决什么问题? A:用少量线程管理大量连接的就绪事件,适合高并发网络 IO。

Q:零拷贝是不是完全没有拷贝? A:不是。它通常指减少用户态和内核态之间的数据复制,并不表示硬件层面零次复制。

Q:Java 原生序列化为什么谨慎使用? A:安全风险高、格式不透明、兼容性复杂,反序列化不可信数据尤其危险。

Q:可靠写文件的关键是什么? A:先写临时文件,成功后再原子移动到目标路径,避免半写入污染正式文件。

Q:IO 慢一定要换 NIO 吗? A:不一定。先定位瓶颈,很多问题通过缓冲、分块、线程池隔离、超时和下游优化就能解决。

Q:IO 代码最容易漏掉什么? A:资源关闭、字符集、异常上下文、大文件内存风险和失败后的清理逻辑。

上次更新: 2026/06/25, 14:19:18
IO场景设计题
JVM概述

← IO场景设计题 JVM概述→

Copyright © 2023-2026 Wray | 鄂ICP备2024050235号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式