深入分析java I/O 的工作机制
1.Java的I/O类库的基本架构
基于字符的i/o操作接口:注意不管是磁盘还是网络传输,其最小存储都是字节,但是我们程序中用到的通常是字节,所以要经过编码转换,类库中的reader类要通过StreanmDecoder通过制定InputStream和Charset产生,而直接使用InputStreamReader或者FileReader实际上使用一个以默认字符集编码的StreamDecoder
2.磁盘I/O工作机制:
(1)因为磁盘设备室由操作系统管理的,应用程序访问物理设备只能通过系统调用的方式来工作,读和写分别对应read和write两个系统调用。而只要是系统调用就会存在内核空间地址和用户空间地址的切换问题(这时由于操作系统吧内核使用的内存空间和用户程序运行的内存空间隔离造成的)
(2)几种访问文件的方式:
<1>标准访问文件方式:当程序调用read的系统调用时,操作系统检查内核高速缓存中有没有需要的数据,如果已经缓存,则直接返回,否则,从磁盘中读取,然后缓存在操作系统的缓存中。当用户调用write的系统调用的时候将数据从用户地址空间复制到内核地址空间,这时对于程序来说,写操作已经完成,至于什么时候写到磁盘由操作系统决定,除非显式调用了sync命令
<2>直接I/O方式:就是应用程序直接访问磁盘数据,而不经过内核数据缓冲区。(通常在数据库管理系统中用到,因为这些数据库管理系统需要明确知道缓存哪一些数据,而操作系统并不知道哪一些是热点数据,只简单缓存上一次从磁盘读取的数据)
<3>同步访问文件方式:读取和写入都是同步操作,它与标准访问文件方式的不同点在于,只有在数据被成功写入磁盘是才返回应用程序成功的标志(同让存在内核地址与用户地址的切换)(使用于对文件的安全性要求较高的操作)
<4>异步访问文件方式:就是访问数据的小县城发出请求后,线程会处理其他事情,而不是阻塞等待,当请求的数据返回后继续处理下面的操作(可以提高程序效率,订单不会改变文件访问效率)
<5>内存映射方式:是指操作系统将内存中的某一块区域与磁盘中的文件关联起来(把用户空间地址映射到内核空间地址?),当要访问内存中的某一段数据时,转为访问文件的某一段数据(目的是减少从内核空间缓存到用户空间缓存的数据复制操作,因为这两个空间是共享的)
(3)java访问磁盘文件
<1>java中的File并不代表一个真实存在的文件对象,当你指定一个路径描述符时它就返回一个代表这个路径的一个虚拟对象。这可能是一个真实文件,有肯能是一个文件夹
<2>何时会真正检查一个文件存不存在?在创建一个FileInpurStream的时候就会创建一个FileDescriptor对象,这个对象就是代表一个存在的文件的描述,FileInpurStream.getFD可以获得这个描述对象(当File对象真正读取文件的操作对象,这时也会创建一个文件描述符File Descriptor),由可以通过FileDesriptor.sync()方法将操作系统缓存的数据刷新到物理磁盘
(4)Java序列化技术
<1>Java序列化就是将一个对象转化成一个二进制标书的字节数组,通过保存或转移这些字节数据来达到持久化的目的。
<2>序列化对象必须要原始类作为模板才能把对象还原,序列化数据保留的是什么信息?包括:序列化文件头,序列化的类的描述(类名,标志号(是否支持序列化),包含的域个数(域是包括属性项的??),SerialVersionUID),各个属性项的描述,父类信息的描述,属性项的实际值
<3>java的序列化能够保证对象状态的持久保存,但是遇到一些负载情况还是比较难处理:
《1》 当父类集成了Serializable接口买所有自雷都可以被序列化
《2》 子类实现了Serialuzable接口,父类没有,父类中的属性不能被序列化(不会报错)
《3》 如果序列化的属性时对象,那么这个对象必须实现Serializable接口,否则会报错
《4》 反序列化时,如果对象的属性有修改或删减,修改的部分属性会丢失,但不会报错(是指作为模板的类的属性出现修改或删除?)
《5》 在反序列化时,如果serialVersionUID被修改,那么反序列化时会失败(如果没有指定serialVersionUID会根据类生成,即使该类也些微修改也会导致该serialVersionUID不同(为了确保安全),所以有些情况下我们显式指定serialVersionUID。)
3.网络I/O工作机制
<1>TCP状态转化:
《1》议中的一个标志位。如果该位被置为1,则表示这个报文是一个请求建立连接的报文。 ack也是该协议的一个标志位。如果该位被置为1,则表示这个报文是一个用于确认的报文。(Fin是请求关闭?)
《2》三次握手后建立连接(客户机发送syn,服务器回应syn+ack,客户端发送ack),四次握手关闭(主动一方发送fin,进入FIN-WAIT-1状态,被动一方接受到fin后,将进入CLOSE-WAIT状态,并分别发送fin和对主动方的fin的ack,此时,主动方将发生3中情况:1.先接受到被动方的fin,发送ACK,进入FIN-WAIT-2再接受到自己的fin的ack进入TIME-WAIT状态。2.先接收到自己的fin的ack,进入CLOSING状态,在接受到对方的FIN,发送ACK,进入TIME-WAIT状态,3.同时接受到被动方的fin和被动方对主动方的FIN的ACK直接由FIN-WAIT-1进入到TIME-WAIT状态),最后进入TIME-WAIT状态后当定时器过期时进入CLOSED状态,而被动方则接受到自己FIN的ACK后进入CLOSED状态
<2>影响网络传输的因素
《1》 网络带宽:就是一秒钟一条物理链路最大能够传输的比特数(b/s,比特每秒)
《2》 传输距离:也就是数据要在光纤中走的距离
《3》 TCP拥塞控制:Tcp传输时会设定一个窗口,这个窗口的大小就是 带宽(b/s)*RTT(s),RTT是数据在两端来回的时间,也就是响应时间,这个乘积同时也是理论上最优的TCP缓冲区大小
<3>Java Socket的工作机制
《1》 socket没有具体的实体,它描述计算机之间完成相互通信的一种抽象功能,,大部分情况我们使用的都是基于TCP/IP的流套接字(TCP/IP是协议簇,包括UDP。Socket同样可以使用UDP协议)
《2》 建立通信链路
socket对象在构造完成前必须进行TCP的三次握手协议,完成后实例才创建完成,否则抛出IOException异常。
ServerSocket对象调用accrpt()方法,每一个请求到来时将为这个连接创建一个新的套接字数据结构(包含请求原地址和信息),这个新创建的数据结构将关联至ServerSocket实例的一个未完成的连接数据结构列表中,每个数据建构代表与一个客户端建立的TCP连接(服务器端的socket实例并未完成创建,仍需等待三次握手后)
《3》数据传输
<1>每个Socket对象都有一个InputStream和OutputStream,并通过这两个对象来交换数据
<2>操作系统将会为InputStream和OutputStream分别分配一定大小的缓存区,数据的写入和读取都是通过这个缓存区完成的
<3>写入端将数据写到SendQ队列中,当队列满时,将数据转移到另一端的RecvQ队列中。如果RecvQ已满,那么outputStream的写入方法将被阻塞(缓存区的大小,写入端的速度,读取段的速度影响传输效率,所以网络传输io比磁盘io还多了一个写入和读取的协调过程)
4.NIO的工作方式
(1)不论是磁盘IO还是网络IO,数据再写入或者读取的时候都可能发生阻塞(如为每对socket产生一个线程,可能为产生大量线程开销,平且在如网站本身维护大量长连接的情况下不可行)
(2)NIO中使用Selector和Channel两个关键类,Selector为每一个注册了它的Chanel监听其注册的事件,当事件发生后用户根据发生的处理。由Selector管理Chanel(Chanel可以设置为非阻塞),由Buffer充当缓存区,Buffer就是用户内存区,与socket不同的是,我们可以控制这个缓冲区的大小,而已扩容。
(3)由于用户地址和内核地址之间的复制比较耗性能,ByteBuffer.AllocateDirector(size)方法可以直接关联到底层存储空间的缓存区。(但是可能会造成内存泄露,书后面的章节会介绍)
(4)NIO的数据访问方式(NIO提供了两个方法对数据的访问优化)
<1>FileChannel.transferFrom/to:数据直接在内核地址中移动(linux使用sendfile系统调用)(适合文件的复制?)
<2>FileChannel.map:将文件按照一定大小映射为内存区域,当访问这个内存区域时将直接操作这个文件数据(省去了从内核空间向用户空间的复制损耗),但是这种方式是通过操作系统底层I/O实现的(适合于大文件只读性操作)
5.IO调优 (这一块需要加深了解)
(1)性能检测:要判断io是否一个瓶颈,有一些参数指标可以参考
<1>系统的i/o wait指标,linux操作系统下可以通过iostat命令查看
<2>IOPS,每个磁盘的IOPS通常在一个范围内,这和储存在磁盘上的数据块的大小和访问方式有关,但是主要由磁盘的转速决定。需要确定应用程序所需要的IOPS是否满足应用程序要求
<3>现在为了提高磁盘的io性能,通常采用一种叫RAID的技术,就是将不同的磁盘组合起来以提高IO性能。每种RAID技术对IO性能的提升会有不同磁盘的吞吐量,可以用一个raid因子来代表,可以通过iostat命令(linux的命令?)来获取,于是可以计算出一个理论的IOPS值,计算公式如下:
(磁盘数*每块磁盘的IOPS)/(磁盘读的吞吐量+RAID因子*磁盘写的吞吐量)=IOPS
(2)提升IO性能的方法有:
<1>增加缓存,减少磁盘访问次数。(用户内存中的缓存还是系统内存中的?如何修改?)
<2>优化磁盘的管理系统,设计最优的磁盘方式策略,以及磁盘的寻址策略,这时在底层操作系统层面考虑的
<3>设计合理的磁盘存储数据块,以及访问这些数据块的策略,这时从应用层面考虑。例如给存放的数据设计索引,通过寻址索引来加快和减少磁盘的访问量,还可以采用异步和非阻塞的方式加快磁盘的访问速度
<4>应用合理的RAID策略来提升磁盘IO。(RAUD策略及说明表见书本的P50)
(3)TCP网络参数调优
<1>如32为操作系统的端口号通常用两个字节表示,也就是只有2的16次方也就是65535个,又操作系统还有一些端口0~1024是受保护的,如22,80等(Linux可以通过查看/proc/sys/net/ipv4/ip_local_port_range文件来知道当前这个主机可以使用的端口范围),所以一台主机能够建立的连接是有限的,遇到大量的并发请求时就会成为瓶颈(可以加大端口范围,详细网络参数见书本P51(应该是linux环境下的))
<2>另外如果发现大量的TIME_WAIT放入话,linux下可以通过设置/proc/sys/net/ipv4/tcp_fin_timeout修改TCP协议中的定时器等待的时间来快速释放请求(可设置TCP连接复用,降低建立连接的损耗,详细网络参数见书本P51(应该是linux环境下的))
(3)以上设置都是临时的,启动系统后就会丢失
(4)网络IO优化
<1>网络IO优化哦那个厂由如下一些基本处理原则:
[1]减少网络交互次数:可以在网络两端设置缓存、合并请求(如静态文件中js文件的下载使用一个http链接)
[2]减少网络传输数据量大小:如将数据压缩后传输(如web服务器通常将web页面压缩后传给浏览器)、通过设计简单的协议,尽量通过读取协议头来获取有用的价值信息(如规定传输的前多少个字节是协议头,其中的第几个字节代表什么信息??)
[3]尽量减少编码:再通过网络传输时,尽量使用字节形式发送,也就是说提前将字符转为字节
<2>根据应用场景设计适合的交互方式(注意这里的同步和阻塞特指IO处理中的某些概念)
[1]同步和异步:通常IO处理都会遇到选择同步还是异步处理方式的问题,同步可以保证可靠性,异步可以提升程序的性能(同步和异步的选择:1.是否依赖于另一个任务,另一个任务完成后,依赖任务才可以完成)(也就是指不同任务之间是否需要同步?)
[2]阻塞和非阻塞:阻塞就是cpu停下来来等待一个慢操作完成后才继续执行后续的操作,表面上看非阻塞能够带来更好的cpu利用率,但是有可能带来大量的线程切换开销,要注意该开销是否可以值得。
[3]两种方式的组合:即同步阻塞(IO性能很差,CPU长时间空闲),异步阻塞(这种方式在分布式数据库中经常用到,能够提高网络IO效率,尤其是写多个不同数据时(异步地写多个不同的数据,也就是同时向多处通过网络IO进行写操作,网络IO的使用率更高?)),同步非阻塞 (常用手段,IO性能好,会增加CPU消耗,要清楚性能瓶颈是在IO上还是CPU上),异步非阻塞(适合于一些复杂的分布式情况,当需要传输多分数据,并且那些数据量虽然不大,但很频繁)。(性能分析详见书本P54)
[4]注意异步和阻塞能够提高IO的性能,但是会导致,线程数量的增加,程序设计复杂度上升
6.设计模式解析之适配器模式
(1)设计模式的功能就是把一个类的接口换成客户端所能接受的接口
(2)以InpytStream作为adaptee,以InputStreamReader作为Adapter,以Reader作为Target就是Java的IO类库中的一个适配器例子:作为adapter的InputStreamReader使用StreamDecoder把作为Adaptee的InputStream读取的字节编码为字符,以提供给Reader
7.设计模式解析之装饰器模式
(1)装饰模式要求不破快原有类的结构而将某个类重新“装扮”,赋予被装饰类更多的功能
(2)以InputStream作为抽象组件角色,以FileInputStream作为具体组件实现类,以FilterInputStream作为做时期,以BufferedInputStream作为装饰器实现者是IO类库中的装饰器模式的一个实现,这个装饰器的作用是使得InputStream读取的数据被保存在内存中,提高读取数据的性能。
(3)装饰器模式和适配器模式的区别:装饰器与适配器模式都有一个别名就是包装模式,他们作用看似都是包装一个类或对象的作用,但是他们的目的不同,适配器模式是吧一个借口转变成另一个借口,通过改变接口使得重用,而装饰器模式不改变接口,只是增强装有对象的功能,或者改变原有对象的处理方法以提高性能。