- IO是机器获取和交换信息的主要渠道,而 流 是完成IO操作的主要方式
- 在计算机中,流是一种 信息的转换
- 流是 有序 的
- 把机器或者应用程序接收外界的信息称为 输入流 (InputStream)
- 从机器或者应用程序向外输出的信息称为 输出流 (OutputStream)
- 流可以被看作一种 数据的载体 ,通过它可以实现数据的 交换 和 传输
Java IO
- Java IO主要在java.io下,有四个基本类: InputStream 、 OutputStream 、 Reader 、 Writer ,分别用于处理 字节流 和 字符流
- 字符到字节必须经过 转码 ,该过程 非常耗时 ,如果不知道 编码类型 就很容易出现 乱码 问题
- 因此IO流提供了 直接操作字符的接口 ,方便对 字符 进行 流操作
字节流
- 字节流的抽象类: InputStream/OutputStream
- 文件的读写操作:FileInputStream/FileOutputStream
- 数组的读写操作:ByteArrayInputStream/ByteArrayOutputStream
- 普通字符串的读写操作:BufferedInputStream/BufferedOutputStream
字符流
字符流的抽象类: Reader/Writer
传统IO的性能问题
- IO操作分为磁盘IO操作和网络IO操作
- 磁盘IO操作 :从磁盘读取数据源输入到内存,之后将读取的信息持久化输出到物理磁盘上
- 网络IO操作 :从网络中读取信息输入到内存,最终将信息输出到网络中
多次内存复制
输入操作在操作系统中的具体流程
- JVM发出read 系统调用 ,向内核发起读请求
- 内核向硬件发出读指令,并等待 读就绪
- 内核把将要读取的数据 复制 到指定的 内核缓存 中
- 操作系统内核将数据 复制 到 用户空间缓冲区 ,然后read系统调用返回
- 数据先从 外部设备 复制到 内核空间 ,再从 内核空间 复制到 用户空间 ,发生了 两次内存复制
- 导致不必要的 数据拷贝 和 上下文切换 , 降低了IO性能
阻塞
- 在传统IO中,InputStream的read()是一个 while循环操作 ,会 一直等待数据读取 ,直到数据就绪才会返回
- 如果没有数据就绪,读取操作将会一直被 挂起 ,用户线程将处于 阻塞 状态
- 在发生 大量 连接请求时,需要创建大量监听线程,一旦这些线程发生阻塞,就会 不断地抢夺CPU资源
优化IO操作
- 面对上面两个性能问题,不仅 编程语言 进行了优化,在 操作系统 层面也进行了优化
- JDK 1.4发布了java.nio包,NIO的发布优化了 内存复制 以及 阻塞 导致的严重性能问题
- JDK 1.7发布了NIO2,从 操作系统 层面实现 异步IO
使用缓冲区 – 优化读写流操作
- 在传统IO中,提出 基于流的IO实现 ,即InputStream和OutputStream,这种基于流的实现是以 字节 为单位处理数据
- NIO与传统IO不同,它是基于 块 (Block)的,以 块 为单位处理数据
- NIO中最为重要的两个组件是 缓冲区 (Buffer)和 通道 (Channel)
- Buffer是一块 连续的内存块 ,是NIO 读写数据的中转地
- Channel表示 缓冲数据的源头或目的地 ,用于 读取 缓冲或者 写入 缓冲,是 访问缓冲的接口
- 传统IO与NIO的最大区别: 传统IO面向流,NIO面向Buffer
- Buffer可以将文件 一次性读入 内存再做后续处理,传统IO是 边度边处理 数据
- 传统IO后来也使用了 缓冲块 ,如BufferedInputStream,但仍然 不能和NIO相媲美
- 使用NIO替代传统IO,可以 立竿见影地提升系统的整体性能
使用DirectBuffer – 减少内存复制
- NIO的Buffer除了做了 缓冲区优化 之外,还提供了 直接访问物理内存 的类:DirectBuffer
- 普通的Buffer分配的是JVM堆内存,而DirectBuffer是直接分配物理内存
- 输出数据到外部设备
- 普通Buffer:从用户空间复制到内核空间,再复制到外部设备
- DirectBuffer: 简化为从内核空间复制到外部设备 ,减少了数据拷贝
- DirectBuffer申请的是非JVM堆内存, 创建和销毁的代价很高
- DirectBuffer申请的内存并 不直接由JVM负责GC
- 在DirectBuffer包装类被回收时,会通过 Java Reference机制 来释放该内存块
避免阻塞
- NIO常被称为Non-Block IO,即 非阻塞IO ,这体现了NIO的特点
- 传统IO即使使用了缓冲块,依然存在阻塞问题
- 线程池线程数有限,一旦发生 大量并发请求 ,超过最大数量的线程就只能 等待 ,直到线程池中有 空闲的线程 可以被复用
- 对Socket的输入流进行读取时,会一直 阻塞 ,直到发生其中一种情况: 有数据可读 、 连接释放 、 空指针或IO异常
- 阻塞问题是传统IO的最大弊端 ,NIO通过 通道 和 多路复用器 这两个组件实现了 非阻塞
通道(Channel)
- 传统IO的数据读写是从 用户空间 到 内核空间 来回复制,内核空间的数据是通过 操作系统层面的IO接口 从磁盘或网络读写的
- 最开始,在应用程序调用操作系统IO接口时, 由CPU完成分配 ,问题: 发生大量IO请求时,非常消耗CPU
- 后来,操作系统引入 DMA (Direct memory access)
- 内核空间与磁盘之间的存取完全由DMA负责
- 但依然需要向CPU申请权限,且需求借助DMA总线来完成数据的复制操作,如果 DMA总线过多 ,会造成 总线冲突
- Channel有自己的处理器 :可以完成 内核空间 和 磁盘 之间的IO操作
- 在NIO中,数据的读写都需要通过Channel,Channel是 双向 的,所以 读写可以同时进行
多路复用器(Selector)
- Selector是Java NIO编程的基础,用于 检查一个或多个NIO Channel的状态是否处于可读、可写
- Selector是基于 事件驱动 实现的
- 在Selector中 注册accept、read监听事件 ,Selector会不断 轮询 注册在其上的Channel
- 如果某个Channel上面发生 监听事件 ,该Channel就处于 就绪 状态,然后进行IO操作
- 一个线程使用一个Selector ,通过 轮询 的方式,可以 监听多个Channel上的事件
- 可以在 注册Channel 时设置该Channel为 非阻塞
- 当Channel上 没有IO操作 时,线程 不会一直等待 ,而是会 不断轮询所有Channel ,从而 避免发生阻塞
- 目前操作系统的IO多路复用机制都使用了 epoll
- 相比于传统的select机制,epoll没有 最大连接句柄1024 的限制
- 所以Selector理论上可以轮询成千上万的客户端
原文
http://zhongmingmao.me/2019/07/22/java-performance-io-model/