Java性能 -- IO模型
什么是IO
- 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资源
- 导致大量的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理论上可以轮询成千上万的客户端
AIO
- JDK 1.7中,Java发布了NIO2,即AIO
- AIO实现了真正意义上的异步IO,直接将IO操作交给操作系统进行异步处理
- 但很多通信框架依然使用NIO,这是因为异步IO模型在Linux内核中没有实现
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.