零拷贝与高性能 I/O

共 20 题
📑 题目列表 20 题
#
★★★

1. ByteBuffer 在 Netty Unpooled.wrappedBuffer 的零拷贝应用

ByteBuffer 在 Netty Unpooled.wrappedBuffer 的零拷贝应用是什么?

  • Unpooled.wrappedBuffer
  • ByteBuf 零拷贝包装
  • 堆外/堆内缓冲

Netty 的 Unpooled.wrappedBuffer(byte[]) 将已有字节数组包装为 ByteBuf,不拷贝数据,直接复用底层数组,实现零拷贝(避免额外的数据复制)。Unpooled.wrappedBuffer 还可包装 ByteBuffer、多个 buffer(CompositeByteBuf)。应用场景:将已从网络/文件读取的字节数组包装为 ByteBuf 进行处理,避免复制;将 JDK ByteBuffer 包装为 Netty ByteBuf 以便使用 Netty API。零拷贝体现在包装不复制数据,只建立视图。注意:wrappedBuffer 不管理底层数组的所有权(不在池中),释放需注意。理解 wrappedBuffer 零拷贝包装(不复制、复用数组)是本题关键。

核心是 wrappedBuffer 将字节数组/ByteBuffer 包装为 ByteBuf 不复制数据,实现零拷贝。理解其应用与语义是本题关键。

byte[] data = readFromSocket();
ByteBuf buf = Unpooled.wrappedBuffer(data);   // 零拷贝包装,不复制
#
★★★

2. 零拷贝在 Kafka Producer(FileChannel.transferTo)的应用

零拷贝在 Kafka Producer(FileChannel.transferTo)的应用是什么?

  • Kafka 的零拷贝
  • FileChannel.transferTo
  • 发送路径优化

Kafka 在发送日志数据时使用零拷贝:Kafka 的存储层将日志分段(Segment)存储在磁盘文件,生产者/消费者发送数据时,通过 FileChannel.transferTo 将文件内容直接传输到 socket,避免数据经用户态拷贝(FileChannel.transferTo 在 Linux 上基于 sendfile 实现零拷贝)。这样 Kafka 的读路径(磁盘文件 -> socket)只需一次内核态拷贝,减少了 CPU 拷贝与系统调用,提升吞吐。Kafka 的零拷贝主要应用于服务端发送日志给消费者(fetch 请求),用 FileChannel.transferTo 直接将 segment 数据发送到 socket。理解 Kafka 通过 FileChannel.transferTo(sendfile)实现磁盘到 socket 的零拷贝发送是本题关键。

核心是 Kafka 用 FileChannel.transferTo 将文件直接发送到 socket,零拷贝提升吞吐。理解应用场景是本题关键。

#
★★★

3. 零拷贝(Zero-Copy)的概念与 sendfile 系统调用

零拷贝(Zero-Copy)的概念是什么?sendfile 系统调用如何实现零拷贝?

  • 零拷贝概念
  • sendfile 系统调用
  • 减少拷贝次数

零拷贝(Zero-Copy)是指数据传输过程中减少或避免 CPU 参与的数据拷贝,尤其是消除用户态与内核态之间的拷贝。传统文件传输(read + write)需要 4 次拷贝(文件到内核、内核到用户、用户到内核、内核到网卡),且多次系统调用。sendfile 系统调用直接在文件描述符(文件与 socket)之间传输数据,由内核完成,数据从文件页缓存直接到 socket 发送缓冲,无需经过用户态,减少拷贝(通常 2 次:DMA + 内核拷贝,或借助网卡 DMA 单次拷贝),并减少系统调用。Java 的 FileChannel.transferTo 在 Linux 上调用 sendfile。理解零拷贝概念(减少用户态拷贝)与 sendfile 实现(内核直接搬运)是本题关键。

核心是零拷贝消除用户态拷贝,sendfile 由内核直接传输文件到 socket,减少拷贝与系统调用。理解概念与实现是本题关键。

#
★★★

4. 零拷贝的三种实现,mmap、sendfile、splice 在内核中的拷贝路径差异如何?

零拷贝的三种实现 mmap、sendfile、splice 在内核中的拷贝路径差异是什么?

  • mmap 的拷贝路径
  • sendfile 的拷贝路径
  • splice 的拷贝路径

三种零拷贝实现的拷贝路径差异:mmap(内存映射)将文件映射到进程地址空间,CPU 通过映射访问文件,文件到内核页缓存无需拷贝(建立映射),但随后从页缓存到 socket 仍需一次内核拷贝(或网卡 DMA);sendfile 是文件到 socket 的专用传输,内核从文件页缓存直接搬运到 socket 缓冲,无需用户态,通常 2 次拷贝(DMA + 内核)或 1 次(支持 DMA 的网卡);splice 在两个文件描述符之间移动数据,通过管道(pipe)在内核中搬运,无需用户态,可用于任意两个 fd(如 socket 到 socket)。差异:mmap 适合用户态反复访问(随机访问),sendfile 适合文件到 socket 的流式传输,splice 适合任意 fd 间传输(如 socket 到 socket)。三者都减少用户态拷贝,但适用场景与拷贝次数不同。理解三者的拷贝路径与适用场景差异是本题关键。

核心是 mmap 映射随机访问、sendfile 文件到 socket、splice 任意 fd 间,都减少用户态拷贝但路径与场景不同。理解差异是本题关键。

#
★★

5. Direct Buffer 与零拷贝的内存对齐

Direct Buffer 与零拷贝的内存对齐有何关系?

  • Direct Buffer 的内存对齐
  • 零拷贝的对齐要求
  • 性能影响

Direct Buffer 分配在堆外,且通常按页对齐分配,这为零拷贝提供了条件:对齐的内存地址允许内核(如 mmap、sendfile、DMA)直接引用,避免因地址不对齐而退化为逐字节拷贝或额外对齐处理。零拷贝(如 FileChannel.transferTo、内存映射)依赖对齐的内存/页缓存,Direct Buffer 的页对齐满足该要求。若使用堆内缓冲,I/O 时需先拷贝到直接内存(对齐),多一次拷贝。因此 Direct Buffer 的内存对齐是高效零拷贝的重要基础。理解 Direct Buffer 页对齐与零拷贝(sendfile/mmap/DMA)对齐要求的关联是本题关键。

核心是 Direct Buffer 页对齐为内核零拷贝(sendfile/mmap/DMA)提供条件,避免退化为普通拷贝。理解对齐与零拷贝关系是本题关键。

#
★★

6. Java 25 虚拟线程下零拷贝的协作

Java 25 虚拟线程下零拷贝如何协作?

  • 虚拟线程与零拷贝
  • FileChannel.transferTo 与虚拟线程
  • 协作方式

在 Java 25 虚拟线程下,零拷贝(如 FileChannel.transferTo)与虚拟线程可以协作:虚拟线程执行 transferTo 等阻塞式 I/O 时,会释放 carrier 线程(不占用平台线程),从而在零拷贝传输期间保持高并发。transferTo 是阻塞调用,但虚拟线程使得阻塞式零拷贝传输不消耗平台线程,适合大量并发文件传输。协作方式:在虚拟线程中调用 transferTo 进行文件到 socket 的零拷贝传输,同时利用虚拟线程承载大量并发连接;零拷贝的收益(减少拷贝)与虚拟线程的收益(减少线程开销)可叠加。注意:transferTo 的非阻塞/部分传输需处理,虚拟线程下阻塞式语义更简单。理解虚拟线程下零拷贝(transferTo)可并发执行且不占用平台线程是本题关键。

核心是虚拟线程执行 transferTo 零拷贝时释放 carrier 线程,并发传输与线程减少收益叠加。理解协作是本题关键。

#
★★

7. Java NIO FileChannel.transferTo 的真实收益

Java NIO FileChannel.transferTo 的真实收益是什么?

  • transferTo 的收益
  • 减少拷贝与系统调用
  • 适用场景与限制

FileChannel.transferTo 的真实收益:减少数据拷贝(在 Linux 上基于 sendfile,数据从文件页缓存直接到 socket,不经用户态,减少用户态拷贝)与减少系统调用(一次调用完成传输,而非多次 read/write),提升吞吐、降低 CPU 占用。收益在服务端发送文件(如文件下载、日志发送)场景显著。但收益有边界:平台依赖(Linux 上 sendfile 支持,Windows 实现不同);transferTo 单次有 2GB 上限需循环;若目标通道不支持或需要处理数据(加密、加工),收益有限;小文件收益不明显。真实收益是"大文件/高吞吐的纯搬运场景"减少拷贝与系统调用。理解 transferTo 的收益(减少拷贝与系统调用)与适用边界(大文件、纯搬运、Linux)是本题关键。

核心是 transferTo 减少拷贝与系统调用,在大文件纯搬运/服务端发送场景收益显著,但受平台与场景限制。理解收益与边界是本题关键。

#
★★

8. Kafka 的零拷贝设计与 JVM 边界

Kafka 的零拷贝设计与 JVM 边界是什么?

  • Kafka 零拷贝设计
  • JVM 边界
  • 零拷贝的应用边界

Kafka 的零拷贝设计:服务端通过 FileChannel.transferTo 将日志 segment 文件直接发送到 socket(sendfile 零拷贝),读路径不经用户态,减少拷贝。Kafka 在 JVM 之上运行,但零拷贝通过 JVM 的 NIO(FileChannel.transferTo)委托给内核 sendfile,JVM 边界在于:JVM 无法直接执行系统调用,零拷贝通过 JVM 的 NIO API 调用内核;transferTo 的字节数限制、平台行为由 JVM/Kernel 决定。Kafka 的零拷贝收益来自内核 sendfile,JVM 只是传递请求。JVM 边界还包括:Java 内无法对发送数据做加工(加密、压缩)时用零拷贝,若需加工则回归用户态。理解 Kafka 通过 JVM 的 FileChannel.transferTo 调用内核 sendfile 实现零拷贝、JVM 是边界传递层是本题关键。

核心是 Kafka 用 transferTo 调用内核 sendfile,JVM 是传递层,零拷贝限于纯搬运场景。理解设计与 JVM 边界是本题关键。

#
★★

9. Netty 的 CompositeByteBuf 与零拷贝设计

Netty 的 CompositeByteBuf 与零拷贝设计是什么?

  • CompositeByteBuf
  • 零拷贝组合
  • 避免数据复制

Netty 的 CompositeByteBuf 是组合多个 ByteBuf 的缓冲,逻辑上作为一个整体读写,但内部引用多个组件,不复制数据,实现零拷贝(避免把多个数据段拼接成一个大缓冲)。应用场景:协议编解码时,多个数据块(协议头 + 载荷)组合为一个 CompositeByteBuf 发送,避免复制合并;接收多个数据分段时组合处理。Netty 的零拷贝还体现在:wrappedBuffer(包装不复制)、slice/duplicate(视图共享)、FileRegion(文件零拷贝)、transferTo。CompositeByteBuf 通过引用计数管理,释放时释放组件。理解 CompositeByteBuf 组合多个缓冲不复制数据(零拷贝)是本题关键。

核心是 CompositeByteBuf 组合多个缓冲逻辑一体但不复制数据,零拷贝用于协议拼接。理解设计是本题关键。

#
★★

10. 零拷贝在小文件和大文件下的差异

零拷贝在小文件和大文件下的差异是什么?

  • 小文件与大文件的零拷贝
  • 开销与收益
  • 场景差异

零拷贝(如 transferTo)在大文件下的收益显著:大文件传输减少的拷贝与系统调用量巨大,吞吐提升明显;小文件下收益有限,因为零拷贝的系统调用与建立传输的开销(如 sendfile 的调用、页缓存加载)可能超过小文件本身的拷贝成本,且小文件常被页缓存命中,普通 read/write 与零拷贝差异不大。此外,小文件数量多时,零拷贝的每次系统调用开销累积,收益不如大文件。因此零拷贝适合大文件/高吞吐场景;小文件场景需权衡,可能普通缓冲读写更合适。理解零拷贝大文件收益显著、小文件收益有限(开销摊销)是本题关键。

核心是零拷贝大文件收益显著、小文件收益有限(系统调用开销摊销不明显)。理解差异是本题关键。

#
★★

11. 零拷贝的诊断(strace/perf)

如何用 strace/perf 诊断零拷贝?

  • strace 系统调用跟踪
  • perf 性能分析
  • 零拷贝验证

strace 可用于诊断零拷贝:跟踪系统调用,观察是否使用了 sendfile(而非多次 read/write),验证零拷贝是否生效及调用次数。例如 strace -e trace=sendfile 观察 sendfile 调用。perf 用于性能分析:分析 CPU 占用、缓存命中、拷贝热点(如 perf stat、perf record 火焰图),评估零拷贝的 CPU 开销与拷贝次数。诊断步骤:用 strace 确认 sendfile 被调用(而非 read/write 循环);用 perf 对比零拷贝与普通拷贝的 CPU 占用、cache-miss、拷贝指令数;分析数据路径(DMA 是否生效)。理解 strace 确认 sendfile 调用、perf 分析 CPU/拷贝开销是本题关键。

核心是 strace 跟踪 sendfile 系统调用验证零拷贝,perf 分析 CPU 与拷贝开销。理解诊断方法是本题关键。

#
★★

12. Direct Buffer 与堆外内存,分配/回收(Cleaner/虚引用)与 GC 的关系如何?

Direct Buffer 与堆外内存的分配/回收(Cleaner/虚引用)与 GC 的关系是什么?

  • Direct Buffer 的堆外分配
  • Cleaner 与虚引用回收
  • GC 关系

Direct Buffer 分配堆外内存(不受 -Xmx 限制,受 MaxDirectMemorySize 限制)。回收通过 Cleaner(基于 PhantomReference 虚引用)实现:当 DirectByteBuffer 对象不可达时,GC 将关联的 Cleaner 加入引用队列,由 Reference Handler 线程执行清理释放堆外内存。因此堆外内存的回收依赖 GC 触发(对象不可达),具有不确定性。若 Direct Buffer 未被 GC 或长期存活,堆外内存不会及时释放,可能积压导致 OOM。关系:Direct Buffer 的 Java 对象受 GC 管理,堆外内存通过 Cleaner(虚引用)随 GC 回收;频繁分配/不释放 Direct Buffer 会因堆外内存压力触发 GC。实践:复用 Direct Buffer(池化)减少分配,及时释放(refCnt 或置 null 促 GC)。理解 Direct Buffer 堆外分配、Cleaner 虚引用回收依赖 GC、池化实践是本题关键。

核心是 Direct Buffer 堆外内存靠 Cleaner(虚引用)随 GC 回收,回收时机不确定,需池化复用。理解回收与 GC 关系是本题关键。

#
★★

13. FileChannel.transferTo 与普通 read/write 的对比,什么场景能真正减少用户态拷贝?

FileChannel.transferTo 与普通 read/write 的对比:什么场景能真正减少用户态拷贝?

  • transferTo 与 read/write 对比
  • 减少用户态拷贝的场景
  • 适用条件

FileChannel.transferTo 与普通 read/write 的对比:transferTo 在 Linux 上基于 sendfile,数据从文件页缓存直接传输到 socket,不经过用户态 ByteBuffer,真正减少用户态拷贝;普通 read/write 需要数据经用户态 ByteBuffer(read 从内核拷到用户,write 从用户拷到内核),多一次用户态拷贝。真正减少用户态拷贝的场景:文件到 socket 的纯搬运(服务端发送文件、日志下载),且不需要对数据加工(加密、压缩、解析)。若需要处理数据内容(如加密、压缩、协议转换),则必须经用户态,transferTo 不适用。因此 transferTo 减少用户态拷贝的前提是"文件到 socket 的纯搬运、无需加工"。理解 transferTo 与 read/write 的拷贝差异与适用场景(纯搬运)是本题关键。

核心是 transferTo 在文件到 socket 纯搬运时减少用户态拷贝,需加工数据时回归 read/write。理解适用场景是本题关键。

#
★★

14. 高性能 IO 的工程实践,Netty 中零拷贝(CompositeByteBuf/FileRegion)如何使用?

Netty 中零拷贝(CompositeByteBuf/FileRegion)的工程实践是什么?

  • CompositeByteBuf 零拷贝
  • FileRegion 零拷贝
  • 工程实践

Netty 的零拷贝工程实践:CompositeByteBuf 组合多个缓冲避免拼接复制(协议多个数据段直接组合发送);FileRegion 实现文件零拷贝(底层用 transferTo/sendfile,将文件直接发送到 socket,避免数据经用户态);wrappedBuffer 包装不复制;slice/duplicate 共享视图。实践场景:发送大文件用 FileRegion(如文件下载服务);协议编解码用 CompositeByteBuf 组合头部与载荷;避免不必要的数据复制。注意:CompositeByteBuf 需引用计数管理(release),FileRegion 适合文件传输。理解 Netty 中 CompositeByteBuf(组合不复制)与 FileRegion(文件零拷贝)的工程实践是本题关键。

核心是 CompositeByteBuf 组合不复制、FileRegion 文件零拷贝,工程实践用于协议拼接与文件发送。理解实践是本题关键。

#
★★

15. RocketMQ 的存储设计,CommitLog 与 ConsumeQueue 基于 mmap 顺序写,与 Kafka 的 sendfile 读路径对比

RocketMQ 的存储设计(CommitLog 与 ConsumeQueue 基于 mmap 顺序写)与 Kafka 的 sendfile 读路径有何对比?

  • RocketMQ 的 mmap 顺序写
  • Kafka 的 sendfile 读路径
  • 读写路径对比

RocketMQ 的存储设计:所有消息写入单一 CommitLog(日志文件),采用 mmap 内存映射顺序写(映射文件到内存,顺序追加,利用页缓存与顺序 I/O 提升写性能);ConsumeQueue 是消费队列(索引),记录消息在 CommitLog 的偏移,也基于 mmap 顺序写。Kafka 的读路径:消费者 fetch 时,Kafka 通过 FileChannel.transferTo(sendfile)将日志文件直接发送到 socket,实现零拷贝读。对比:RocketMQ 侧重写路径性能(mmap 顺序写,由于写操作需在用户态组装消息,用 mmap 映射写);Kafka 侧重读路径零拷贝(sendfile 直接将文件发到 socket,不经用户态)。RocketMQ 的读也会查 ConsumeQueue 定位后从 CommitLog 读。理解 RocketMQ 用 mmap 顺序写(写路径)、Kafka 用 sendfile(读路径)的设计对比是本题关键。

核心是 RocketMQ 用 mmap 顺序写 CommitLog 提升写性能,Kafka 读路径用 sendfile 零拷贝,读写侧重点不同。理解对比是本题关键。

#
★★

16. GatheringByteChannel 的 scatter/gather,一次系统调用读写多个缓冲区,与零拷贝的关系和适用场景

GatheringByteChannel 的 scatter/gather 一次系统调用读写多个缓冲区,与零拷贝的关系和适用场景是什么?

  • scatter/gather 一次系统调用
  • 与零拷贝的关系
  • 适用场景

GatheringByteChannel(write(ByteBuffer[]))与 ScatteringByteChannel(read(ByteBuffer[]))支持一次系统调用读写多个缓冲区(scatter/gather,底层 readv/writev)。这与零拷贝不同:scatter/gather 减少的是系统调用次数(一次调用处理多个缓冲区),而非消除用户态拷贝;数据仍经用户态缓冲区。关系:scatter/gather 与零拷贝是不同优化维度,可结合使用(如协议头与载荷分开放多个缓冲区,一次 gather 写出)。适用场景:协议编解码(头、体分开在不同缓冲)、一次收集多个数据块写出、一次读取填充多个缓冲。理解 scatter/gather 减少系统调用(非消除拷贝)、与零拷贝的区别与适用场景是本题关键。

核心是 scatter/gather 减少系统调用次数而非消除拷贝,用于协议多缓冲一次读写。理解其与零拷贝的区别是本题关键。

#
★★

17. FileChannel.transferTo 的部分传输,单次约 2GB 上限需循环调用,返回值小于请求长度时的处理

FileChannel.transferTo 的部分传输如何处理?单次约 2GB 上限需循环调用,返回值小于请求长度时应怎样处理?

  • transferTo 的部分传输
  • 2GB 上限
  • 循环调用与返回值处理

FileChannel.transferTo 在某些平台(如 Windows)单次传输有约 2GB 上限,且可能只传输部分数据,返回值是实际传输的字节数,可能小于请求长度。正确处理:循环调用 transferTo,累加已传输字节数,更新 position 或剩余长度,直到全部传输完成或返回 0;若返回值小于请求长度,不应视为错误,继续循环传输剩余部分。实现:long count = 0; while (count < total) { long n = ch.transferTo(pos+count, total-count, target); if (n <= 0) break; count += n; }。注意返回值 0 表示无法继续传输(如目标已满),需处理。理解 transferTo 部分传输与 2GB 上限、循环调用处理返回值是本题关键。

核心是 transferTo 可能部分传输且有 2GB 上限,需循环调用并累加返回值,直到完成。理解处理方法是本题关键。

long pos = 0, total = fileSize;
while (pos < total) {
    long n = fileChannel.transferTo(pos, total - pos, socketChannel);
    if (n <= 0) break;
    pos += n;
}
#

18. 零拷贝在 HTTP/3 与 HTTP/2 协议栈的实现

零拷贝在 HTTP/3 与 HTTP/2 协议栈的实现是什么?

  • HTTP/2 与 HTTP/3 的零拷贝
  • QUIC 与帧多路复用
  • 零拷贝实现

HTTP/2 在 TCP 之上多路复用帧,其零拷贝(如文件传输)可通过底层 sendfile 实现(文件直接到 socket 的 TCP 发送)。HTTP/3 基于 QUIC(UDP 之上的可靠传输),其多路复用与重传机制在用户态(QUIC 栈)实现,因此零拷贝面临挑战:QUIC 的加密、重传、帧管理需要用户态处理数据,sendfile 难以直接用于 QUIC 的密文传输。实现上,HTTP/3 的零拷贝需在 QUIC 栈内优化(如内核态 QUIC 支持、减少用户态拷贝),或配合 TLS 加密(内核态 TLS KTLS)。理解 HTTP/2 零拷贝可基于 sendfile,HTTP/3(QUIC)因用户态多路复用与加密,零拷贝更难实现,需 QUIC 栈优化或内核支持是本题关键。

核心是 HTTP/2 可基于 sendfile 零拷贝,HTTP/3(QUIC)因用户态多路复用/加密零拷贝难,需内核 QUIC/KTLS 支持。理解差异是本题关键。

#

19. 内存映射文件的限制,文件大小与地址空间、并发写与持久化语义如何?

内存映射文件的限制是什么?文件大小与地址空间、并发写与持久化语义如何?

  • 内存映射的限制
  • 文件大小与地址空间
  • 并发写与持久化

内存映射文件(MappedByteBuffer)的限制:文件大小受进程地址空间限制(32 位受限,64 位上限约 2^47/2^48,但仍受虚拟地址空间限制);映射的文件大小不能超过地址空间容纳范围。并发写:多个线程映射同一文件区域并发写时,需同步(MappedByteBuffer 的 put 非线程安全),可能导致数据竞争;跨进程并发写同一映射区域由操作系统/文件系统保证页级一致性。持久化语义:映射后的写操作先到页缓存,不立即落盘,需通过 MappedByteBuffer.force()(或 FileChannel.force)刷盘,否则崩溃可能丢失;映射的脏页由操作系统按策略刷盘。理解内存映射的地址空间限制、并发写需同步、持久化需 force 是本题关键。

核心是内存映射受地址空间限制、并发写需同步、持久化需 force 刷盘。理解限制与语义是本题关键。

#

20. 零拷贝与加密流量,TLS 下数据需在用户态加解密,sendfile 无法直接用于密文,以及内核态 TLS 的例外

零拷贝与加密流量:TLS 下数据需在用户态加解密,sendfile 无法直接用于密文,以及内核态 TLS 的例外是什么?

  • TLS 与零拷贝的冲突
  • sendfile 无法用于密文
  • 内核态 TLS(KTLS)

零拷贝(sendfile)与 TLS 加密存在冲突:TLS 下数据需在用户态加解密后发送,sendfile 直接传输文件到 socket 的明文或密文路径无法满足 TLS 的 TLS 记录封装与加密(明文需加密、密文需记录头)。因此 sendfile 无法直接用于 TLS 流量(文件明文不能直接 sendfile,需先加密)。例外:内核态 TLS(KTLS,Kernel TLS)允许内核处理 TLS 加解密,配合 sendfile 实现零拷贝加密传输(文件直接到内核,内核加密并发送),从而在 TLS 场景利用零拷贝。KTLS 是 Linux 内核功能,Java 通过特定 API 或框架可能利用。理解 TLS 与零拷贝的冲突(需用户态加解密)、sendfile 无法直接用于密文、KTLS 是例外(内核加密)是本题关键。

核心是 TLS 需用户态加解密,sendfile 无法直接用于 TLS 密文,KTLS 内核加密可实现零拷贝例外。理解冲突与例外是本题关键。