文件io和网络io

文件Io

一、输入及输出的概念

1.1 什么是 I/O
是指程序与外部设备或其他计算机进行交互的操作。
几乎所有的程序都具有输入与输出操作。
如从键盘上读取数据,从本地或网络_上的文件读取数据或写入数据等。
通过输入和输出操作可以从外界接收信息,或者是把信息传递给外界。
Java把这些输入与输出操作用流来实现,通过统一的接口来表示,从而使程序设计更为简单。

1.2 输入输出(I/0)
入还是出事相对于内存
把数据读到内存中,称为输入,即 input,进行数据的 read 操作
往内存外部设备写数据,称为输出,即 output,进行数据的 write 操作

二、File类

2.1 File 类的相关概念
File类是java. io包中很重要的一个类;
File类的对象可以表示文件,还可以表示目录,在程序中一个File类对象可以代表一个文件或目录;
File对象可以对文件或目录的属性进行操作,如:文件名、最后修改日期、文件大小等;
File对象无法操作文件的具体数据,即不能直接对文件进行读/写操作。

2.2 File 类的构造方法
File 类的构造方法有四种重载方式,不过通常使用如下方式

File(String pathname) //指定文件(或目录)名和路径创建文件对象
File file = new File("1.txt"); // 在当前目录创建文件对象
File file = new File("Java"); // 在当前目录创建一个目录对象
File f3 = new File("D:\\Java");//指明详细的路径以及目录名,请注意双斜线

三、输入流与输出流

输入流和输出流可以操作文件的内容
3.1 基本概念
流按照数据的传输方向分为:
输入流:往内存中读叫输入流
输出流:从内存中往外些叫输出流
所有输入流都是 InputStream 类或者 Reader 类的子类
类名以 inputStream 结尾的类都是 InputStream 的子类
类名以 Reader 结尾的类都是 Reader类的子类
所有输出流都是 OutputStream 类 或者 Writer类的子类
类名以 OutputStrean结尾的类都是 OutputStream的子类
类名以 Writer结尾的类都是 Writer 类的子类
四、字节流和字符流
4.1 特点
从数据流的编码格式上划分
字节流
字符流
InputStream 和 OutputStream的子类都是字节流
所以字节流可以读写二进制文件,主要处理音频、图片、歌曲等,处理单元为1个字节
Reader 和 Writer 的子类都是字符流
主要处理字符或字符串,字符流处理单元为 2个字节
字节流将读取到的字节数据,去指定的编码表中获取对应的文字

4.2 io流中分类
字节流中常用类
字节输入流:FileInputStream
字节输出流:FileOutputStream
字符流中常用类
字符输入流 FileReader
字符输出流 FileWriter

读字节流(InputStream)

不管字符流还是字节流。本质上都是通过按字节从磁盘读取数据,只不过字符流根据字符长度,一次读取多个字节而已。

FileInputStream与BufferedInputStream

FileInputStream read方法

public int read(byte b[], int off, int len) throws IOException {
        return readBytes(b, off, len);
    }

可以看出,FileInputStream读取字节的方式很简单直接调用native方法,利用操作系统做io

BufferedInputStream read方法

public synchronized int read(byte b[], int off, int len)
        throws IOException
    {
        getBufIfOpen(); // Check for closed stream
        
        if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
            //读取字节越界
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            //读取字节流为空
            return 0;
        }

        int n = 0;
        for (;;) {
            int nread = read1(b, off + n, len - n);
            if (nread <= 0)
                return (n == 0) ? nread : n;
            n += nread;
            if (n >= len)
                return n;
            // if not closed but no bytes available, return
            InputStream input = in;
            if (input != null && input.available() <= 0)
                return n;
        }
    }

可以看到这里的read方法使用了synchronized修饰,说明同时一个InputStream只能由一个线程读取,BufferedInputStream默认buf长度为8192。

read1方法

 private int read1(byte[] b, int off, int len) throws IOException {
        //计算可读字节长度
        int avail = count - pos;
        if (avail <= 0) {
            
        //可读为0,填缓存充
            fill();
            avail = count - pos;
            if (avail <= 0) return -1;
        }
        int cnt = (avail < len) ? avail : len;
        System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
        pos += cnt;
        return cnt;
    }

fill方法

 private void fill() throws IOException {
       //获取缓存buf 默认长度819
        byte[] buffer = getBufIfOpen();
       ···· 省略代码  ····
        count = pos;
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
        if (n > 0)
            count = n + pos;
    }

一次读取8192字节到buf,这里可以看出BufferedInputStream实际上还是通过FileInputStream进行读取字节流的

public class FilterInputStream extends InputStream {
    /**
     * The input stream to be filtered.
     */
    protected volatile InputStream in;
}

可以看出来FilterInputStream继承自InputStream又引用了InputStream,利用了装饰器模式对InputStream进行扩展

fill之后 count代表buf总共的数据,pos代表当前读到的位置,再看回read1方法,计算出可读长度avail,然后从pos开始读取。然后更新pos,再看回read方法,如果FileInputStream仍然可读,并且读取的长度未达到指定的长度,就继续循环读取。

从此可以看出来BufferedInputStream 也只是在FileInputStream基础上加了一层装饰,并且加了一个buf。避免频繁的数据读取。

操作系统缓存(cache)

cache是一个非常大的概念。

一、
CPU的Cache,它中文名称是高速缓冲存储器,读写速度很快,几乎与CPU一样。由于CPU的运算速度太快,内存的数据存取速度无法跟上CPU的速度,所以在cpu与内存间设置了cache为cpu的数据快取区。当计算机执行程序时,数据与地址管理部件会预测可能要用到的数据和指令,并将这些数据和指令预先从内存中读出送到Cache。一旦需要时,先检查Cache,若有就从Cache中读取,若无再访问内存,现在的CPU还有一级cache,二级cache。简单来说,Cache就是用来解决CPU与内存之间速度不匹配的问题,避免内存与辅助内存频繁存取数据,这样就提高了系统的执行效率。

二、
磁盘也有cache,硬盘的cache作用就类似于CPU的cache,它解决了总线接口的高速需求和读写硬盘的矛盾以及对某些扇区的反复读取。

三、
操作系统从磁盘读取数据时,会把相邻的数据页缓存起来。

四、
局部性原理与磁盘预读
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:

当一个数据被用到时,其附近的数据也通常会马上被使用。

程序运行期间所需要的数据通常比较集中。

由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。

预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。

FileInputStream完全不用缓存吗

当我使用FileInputStream读取一个对象(比如几个字节)时,底层操作是否涉及:
1)读取整块磁盘,这样如果我随后再进行另一次读取操作,它就不需要真正的磁盘读取,因为在上次读取操作中已经取出了该部分文件?
这里又要提到操作系统提供的pageCache,虽然在Java看来FileInputStream是直接写到磁盘上的,实际上还有操作系统的优化。

InputStream读取

以FileReader为例,在构造时指定编码

 public static StreamDecoder forInputStreamReader(InputStream var0, Object var1, String var2) throws UnsupportedEncodingException {
        String var3 = var2;
        if (var2 == null) {
            var3 = Charset.defaultCharset().name();
        }

        try {
            if (Charset.isSupported(var3)) {
                return new StreamDecoder(var0, var1, Charset.forName(var3));
            }
        } catch (IllegalCharsetNameException var5) {
            ;
        }

        throw new UnsupportedEncodingException(var3);
    }

已utf-8为例,创建StreamDecoder

 StreamDecoder(InputStream var1, Object var2, CharsetDecoder var3) {
        super(var2);
        this.isOpen = true;
        this.haveLeftoverChar = false;
        this.cs = var3.charset();
        this.decoder = var3;
        if (this.ch == null) {
            this.in = var1;
            this.ch = null;
            this.bb = ByteBuffer.allocate(8192);
        }

        this.bb.flip();
    }

可以看出FileReader实际上也有时缓冲区的。跟字节流相同,也是先填充buf,再从buf中读取字节,不过FileReader通过StreamDecoder根据字符编码读取,一次读取几个字节后解码为对应字符

近日学习IO知识的时候对于BufferedReader和FileReader的差别不是很理解,如果说BufferedReader是对于FileReader添加了一个缓冲区,那么FileReader不是本来就存在缓冲区吗?

对比源码我们发现,BufferedReader继承了Reader的read(char[] buf)方法,而改写了read()和read(char[] buf, int off, int len)方法,使用read(char[] buf)方法的效果一样,都是读取到数组中,然后从数组中读取数据,但是read()方法,一次只读一个字符,BufferedReader覆盖了Reader的read方法,BufferedReader会将字符数据读取到一个数组中,相当于调用了bufr.read(buf)方法,然后再从这个缓存区中一个一个的读取字符数据。

这是BufferedReader的read方法的源码,我们发现,即使是对于一个字符数据的读取,BufferedReader依旧是先读取一个数组的数据到缓冲区中,然后从缓冲区中一个个的取,对于read方法而言,BufferedReader是比FileReader进行了优化,减少了io操作,但是对于read(char[] buf)的操作而言,两个类都继承与Reader,所以并没有差别。

1.FileReader不能一行行读,BufferedReader可以一行行地读

2.BufferedReader可以一行行地读效率高,因为减少了IO的次数

巧妙的设计模式

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class InputStreamTest {

	public static void main(String[] args) {
		
		try {
			FileInputStream fis = new FileInputStream("D:/README.TXT"); // FileInputStream extends InputStream
			InputStreamReader isr = new InputStreamReader(fis);         // InputStreamReader extends Reader
			BufferedReader br = new BufferedReader(isr);				// BufferedReader extends Reader
			
			String s = null;
			while ((s = br.readLine()) != null) {
				System.out.println(s);
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}

inputStram,Reader,BufferedReader的组合,既是适配器模式,BufferedReader通过InputStreamReader使用FileInputStream
也可以看成装饰器模式,三个实现不断对FileInputStream的功能进行装饰增强

OutputStream 写字节流

先看FileOutputStream,java中的实现很简单

public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }

直接系统调用,交给操作系统取写字节流

在构造时可指定是否再文件追加的形式写入,这种方式顺序写

 out = new FileOutputStream(f,true)  ;    // 此处表示在文件末尾追加内容

指定位置写

  public void write(byte b[], int off, int len) throws IOException {
  }

随机写,顺序写

由于硬盘的原因。顺序写的速度要必随机写快得多

write调用操作系统到底做了什么

FileOutputStream
每次都向内核调一次syscall和write(byte[]),将二进制流写入内核的page cache。不提供flush(),只有close。

测试循环写入123456789,追踪系统调用,每次都写入:

FileOutPutStream继承outputStream,并不提供flush()方法的重写所以无论内容多少write都会将二进制流直接传递给底层操作系统的I/O,flush无效果而

再看BufferOutputStream

 public synchronized void write(byte b[], int off, int len) throws IOException {
        if (len >= buf.length) {
            /* If the request length exceeds the size of the output buffer,
               flush the output buffer and then write the data directly.
               In this way buffered streams will cascade harmlessly. */
            flushBuffer();
            out.write(b, off, len);
            return;
        }
        if (len > buf.length - count) {
            flushBuffer();
        }
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }

buffer缓存区是jvm层面的东西,默认开辟了8kb的数组。每次write先写入缓存区,等这8kb写满了才调用一次内核syscall和write(byte[]),将二进制流写入内核的page cache。提供flush,手动将数据刷入内核。
buffer优势:
减少了应用程序和内核的IO次数,即减少系统调用;
减少了用户态和内核态切换的次数。
扩展:两者都写入了内核的page cache,由系统机制把脏页刷入硬盘。

总结:
FileOutPutStream继承outputStream,并不提供flush()方法的重写所以无论内容多少write都会将二进制流直接传递给底层操作系统的I/O,flush无效果而Buffered系列的输入输出流函数单从Buffered这个单词就可以看出他们是使用缓冲区的,应用程序每次IO都要和设备进行通信,效率很低,因此缓冲区为了提高效率,当写入设备时,先写入缓冲区,等到缓冲区有足够多的数据时,就整体写入设备

使用BufferedXXXStream。默认缓冲区大小是8K。读的时候会一直填满缓冲区(或者文件读取完毕),写的时候也是等缓冲区满了之后(或者执行flush操作)才将内容送入内核缓冲区。效率高的原因就是避免了每读一个字节都要陷入操作系统内核(这是个耗时的操作)。具体代码,题主自己查API吧。

OutPutStream和InputStream还有很多扩展类,可以写字符流,数字等,就不再研究。

网络io

网路io发送

avatar

对于操作系统来说,每个socket会有自己的send/receive buffer。调用write,只是说将用户进程的数据,拷贝到了内核的socket buffer里面,拷贝完之后,就没有write什么事了。内核自己会用自己的进程,调用TCP/IP协议栈,把用户进程的数据发出去。

java sokcet自带的io流 SocketOutputStream,SocketOutputStream继承自FileOutputStream,
write方法

 FileDescriptor fd = impl.acquireFD();
  
 socketWrite0(fd, b, off, len);

从这里可以看出来socket的write的过程就是,申请一个文件,然后向文件中写数据的过程

再看一个BufferWirter其引用了SocketOutputStream,并自带一个缓冲区
看一下他的wite方法

  public void write(String s, int off, int len) throws IOException {
        synchronized (lock) {
            ensureOpen();

            int b = off, t = off + len;
            while (b < t) {
                int d = min(nChars - nextChar, t - b);
                s.getChars(b, b + d, cb, nextChar);
                b += d;
                nextChar += d;
                if (nextChar >= nChars)
                    flushBuffer();
            }
        }
    }

先写到缓冲区,默认8k,缓冲区满或者手动flush,调用SocketOutputStream的write方法写到socket文件(也就是网络缓冲区)中。

零拷贝

这是一个从磁盘文件读取并且通过socket写出的过程,对应的系统调用如下:

read(file,tmp_buf,len)
write(socket,tmp_buf,len)

通过网络把一个文件传输给另一个程序,在OS的内部,这个copy操作要经历四次user mode和kernel mode之间的上下文切换,甚至连数据都被拷贝了四次(关于数据IO时涉及的内部过程,可参阅 IO过程的内部过程-MarchOn)。如下图:

具体步骤如下:

read() 调用导致一次从user mode到kernel mode的上下文切换。在内部调用了sys_read() 来从文件中读取data。第一次copy由DMA (direct memory access)完成,将文件内容从disk读出,存储在kernel的buffer中。
然后请求的数据被copy到user buffer中,此时read()成功返回。调用的返回触发了第二次context switch: 从kernel到user。至此,数据存储在user的buffer中。
send() Socket call 带来了第三次context switch,这次是从user mode到kernel mode。同时,也发生了第三次copy:把data放到了kernel adress space中。当然,这次的kernel buffer和第一步的buffer是不同的buffer。
最终 send() system call 返回了,同时也造成了第四次context switch。同时第四次copy发生,DMA egine将data从kernel buffer拷贝到protocol engine中。第四次copy是独立而且异步的。

avatar

零拷贝的“零”是指用户态和内核态间copy数据的次数为零。

传统的数据copy(文件到文件、client到server等)涉及到四次用户态内核态切换、四次copy。四次copy中,两次在用户态和内核态间copy需要CPU参与、两次在内核态与IO设备间copy为DMA方式不需要CPU参与。零拷贝避免了用户态和内核态间的copy(共2次)、减少了两次用户态内核态间的切换,因此数据传输效率高(4、4变2、2)。

零拷贝可以提高数据传输效率,但对于需要在用户传输过程中对数据进行加工的场景(如加密)就不适合使用零拷贝。
avatar

avatar

java 的zero copy多在网络应用程序中使用。Java的libaries在linux和unix中支持zero copy,关键的api是java.nio.channel.FileChannel的transferTo(),transferFrom()方法。我们可以用这两个方法来把bytes直接从调用它的channel传输到另一个writable byte channel,中间不会使data经过应用程序,以便提高数据转移的效率。

java零拷贝的实现

java.nio.channels.FileChannel
java.nio.channels.FileChannel#transferTo

     MappedByteBuffer var11 = this.map(MapMode.READ_ONLY, var1, var9);
     int var12 = var5.write(var11);

注意这个 MappedByteBuffer
发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
mmap系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间。因为用户空间和内核空间共享了这个缓冲区数据,所以用户空间就可以像在操作自己缓冲区中数据一般操作这个由内核空间共享的缓冲区数据。
发出write系统调用,导致用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket相关联的缓冲区(第二次拷贝: kernel buffer ——> socket buffer)。
write系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)

通过操作系统直接使用 java.nio.MappedByteBuffer 通过kernal buff发送到目标socket的缓冲区,这样就不会有user到kernel的拷贝了。
其实还是这张图
avatar

注意:当请求的data size远大于kernel buffer size的时候,这个方法本身变成了性能的瓶颈。因为data需要在disk,kernel buffer,user buffer之间拷贝很多次(每次写满整个buffer)。
当data大于8388608L,8192kb,8mb,就需要分段了,多次copy了
许多web应用都会向用户提供大量的静态内容,这意味着有很多data从硬盘读出之后,会原封不动的通过socket传输给用户。这种操作看起来可能不会怎么消耗CPU,但是实际上它是低效的:kernal把数据从disk读出来,然后把它传输给user级的application,然后application再次把同样的内容再传回给处于kernal级的socket。这种场景下,application实际上只是作为一种低效的中间介质,用来把disk file的data传给socket。这种静态内容比较适合使用transferFrom进行零拷贝
实际上我们也能看出一个问题,实际上我们并不能经常使用零拷贝技术,因为我们总不可避免的需要将数据读取到用户态,操作修改数据。

nio数据读写

static int write(FileDescriptor fd, ByteBuffer src, long position,
                     NativeDispatcher nd)
        throws IOException
    {
        if (src instanceof DirectBuffer)
            return writeFromNativeBuffer(fd, src, position, nd);

        // Substitute a native buffer
        int pos = src.position();
        int lim = src.limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);
        try {
            bb.put(src);
            bb.flip();
            // Do not update src until we see how many bytes were written
            src.position(pos);

            int n = writeFromNativeBuffer(fd, bb, position, nd);
            if (n > 0) {
                // now update src
                src.position(pos + n);
            }
            return n;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(bb);
        }
    }

数据写入

static int read(FileDescriptor fd, ByteBuffer dst, long position,
                    NativeDispatcher nd)
        throws IOException
    {
        if (dst.isReadOnly())
            throw new IllegalArgumentException("Read-only buffer");
        if (dst instanceof DirectBuffer)
            return readIntoNativeBuffer(fd, dst, position, nd);

        // Substitute a native buffer
        ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining());
        try {
            int n = readIntoNativeBuffer(fd, bb, position, nd);
            bb.flip();
            if (n > 0)
                dst.put(bb);
            return n;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(bb);
        }
    }

先从缓冲区把数据读到 堆外直接内存DirectBuff,然后再从directBuffer复制到heapBuff。读取完成。同理写也是一样

nio直接内存

  1. DirectBuffer 属于堆外存,那应该还是属于用户内存,而不是内核内存?

DirectByteBuffer 自身是(Java)堆内的,它背后真正承载数据的buffer是在(Java)堆外——native memory中的。这是 malloc() 分配出来的内存,是用户态的。

所以使用DirectByteBuffer还是免不了用户态到内核态的copy

  1. 为什么要将HeapByteBuffer的数据拷贝到DirectByteBuffer呢?不能将数据直接从HeapByteBuffer拷贝到文件中吗?

并不是说操作系统无法直接访问jvm中分配的内存区域,显然操作系统是可以访问所有的本机内存区域的,但是为什么对io的操作都需要将jvm内存区的数据拷贝到堆外内存呢?是因为jvm需要进行GC,如果io设备直接和jvm堆上的数据进行交互,这个时候jvm进行了GC,那么有可能会导致没有被回收的数据进行了压缩,位置被移动到了连续的存储区域,这样会导致正在进行的io操作相关的数据全部乱套,显然是不合理的,所以对io的操作会将jvm的数据拷贝至堆外内存,然后再进行处理,将不会被jvm上GC的操作影响。

  1. DirectByteBuffer是相当于固定的内核buffer还是JVM进程内的堆外内存?

不管是Java堆还是直接内存,都是JVM进程通过malloc申请的内存,其都是用户空间的内存,只不过是JVM进程将这两块用户空间的内存用作不同的用处罢了

  1. 将HeapByteBuffer的数据拷贝到DirectByteBuffer这一过程是操作系统执行还是JVM执行?

在问题2中已经回答,DirectByteBuffer是JVM进程申请的用户空间内存,其使用和分配都是由JVM进程管理,因此这一过程是JVM执行的.也正是因为JVM知道堆内存会经常GC,数据地址经常移动,而底层通过write,read,pwrite,pread等函数进行系统调用时,需要传入buffer的起始地址和buffer count作为参数,因此JVM在执行读写时会做判断,若是HeapByteBuffer,就将其拷贝到直接内存后再调用系统调用执行步骤2.
代码在sun.nio.ch.IOUtil.write()和sun.nio.ch.IOUtil.read()中,我们看下write()的代码:

  1. 为什么在执行网络IO或者文件IO时,一定要通过堆外内存呢?

如果是使用DirectBuffer就会少一次内存拷贝。如果是非DirectBuffer,JDK会先创建一个DirectBuffer,再去执行真正的写操作。这是因为,当我们把一个地址通过JNI传递给底层的C库的时候,有一个基本的要求,就是这个地址上的内容不能失效。然而,在GC管理下的对象是会在Java堆中移动的。也就是说,有可能我把一个地址传给底层的write,但是这段内存却因为GC整理内存而失效了。所以我必须要把待发送的数据放到一个GC管不着的地方。这就是调用native方法之前,数据一定要在堆外内存的原因。可见,DirectBuffer并没有节省什么内存拷贝,只是因为HeapBuffer必须多做一次拷贝,才显得DirectBuffer更快一点而已。

  1. 在将数据写到文件的过程中需要将数据拷贝到内核空间吗?

需要.在步骤3中,是不能直接将数据从直接内存拷贝到文件中的,需要将数据从直接内存->内核空间->文件,因此使用DirectByteBuffer代替HeapByteBuffer也只是减少了数据拷贝的一个步骤,但对性能已经有提升了.

netty零拷贝,对象池,内存池

netty 一条消息发送流程

配置netty都会写这么一串代码

 bootstrap.group(nioEventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
//                        .addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()))
                                    .addLast(new LineBasedFrameDecoder(1024))
                                    .addLast(new StringDecoder(CharsetUtil.UTF_8))
                                    .addLast(new StringEncoder(CharsetUtil.UTF_8))
                                    .addLast(new SimpleChannelInboundHandler<String>() {

                                        @Override
                                        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                                            System.out.println(msg);

                                        }
                                    });
                        }
                    });

在调用wirte方法时,会由下至上,挑选出ContextOutbound挨个执行

 AbstractChannelHandlerContext next = findContextOutbound()

其中有业务handler,也有协议handler,例如 客户端都使用StringDecoder和StringEncode进行进行编码和解码通信

来看下StringEncode的编码

static ByteBuf encodeString0(ByteBufAllocator alloc, boolean enforceHeap, CharBuffer src, Charset charset,
                                 int extraCapacity) {
        //字符编码                             
        final CharsetEncoder encoder = CharsetUtil.encoder(charset);
        //通过字符长度和字符个数确定字节长度
        int length = (int) ((double) src.remaining() * encoder.maxBytesPerChar()) + extraCapacity;
        boolean release = true;
        final ByteBuf dst;
        //分配堆外还是堆内内存,堆外内存少一次,堆内到堆外的copy
        if (enforceHeap) {
            dst = alloc.heapBuffer(length);
        } else {
            dst = alloc.buffer(length);
        }
        try {
            final ByteBuffer dstBuf = dst.internalNioBuffer(0, length);
            final int pos = dstBuf.position();
            CoderResult cr = encoder.encode(src, dstBuf, true);
            if (!cr.isUnderflow()) {
                cr.throwException();
            }
            cr = encoder.flush(dstBuf);
            if (!cr.isUnderflow()) {
                cr.throwException();
            }
            dst.writerIndex(dst.writerIndex() + dstBuf.position() - pos);
            release = false;
            return dst;
        } catch (CharacterCodingException x) {
            throw new IllegalStateException(x);
        } finally {
            if (release) {
                dst.release();
            }
        }
    }
对象池

bytebuy对象使用完后需要回收。

// Stack所属主线程,注意这里使用了WeakReference
WeakReference<Thread> threadRef;    
// 主线程回收的对象
DefaultHandle<?>[] elements;
// elements最大长度
int maxCapacity;
// elements索引
int size;
// 非主线程回收的对象
volatile WeakOrderQueue head;   

Recycler将一个Stack划分给某个主线程,主线程直接从Stack#elements中存取对象,而非主线程回收对象则存入WeakOrderQueue中。
threadRef字段使用了WeakReference,当主线程消亡后,该字段指向对象就可以被垃圾回收。

DefaultHandle,对象的包装类,在Recycler中缓存的对象都会包装成DefaultHandle类。

head指向的WeakOrderQueue,用于存放其他线程的对象

WeakOrderQueue主要属性

 } finally {
     ···· 省略代码  ·····
      ReferenceCountUtil.release(buf);
 }

在回收前,需要把对象清空

 private boolean release0(int decrement) {
        for (;;) {
            int refCnt = this.refCnt;
            if (refCnt < decrement) {
                throw new IllegalReferenceCountException(refCnt, -decrement);
            }

            if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
                //如果已经没有引用了,释放空间
                if (refCnt == decrement) {
                    deallocate();
                    return true;
                }
                return false;
            }
        }
    }
 void push(DefaultHandle<?> item) {
            Thread currentThread = Thread.currentThread();
            if (thread == currentThread) {
                // The current Thread is the thread that belongs to the Stack, we can try to push the object now.
                pushNow(item);
            } else {
                // The current Thread is not the one that belongs to the Stack, we need to signal that the push
                // happens later.
                pushLater(item, currentThread);
            }
        }
   private void pushNow(DefaultHandle<?> item) {
            if ((item.recycleId | item.lastRecycledId) != 0) {
                throw new IllegalStateException("recycled already");
            }
            item.recycleId = item.lastRecycledId = OWN_THREAD_ID;

            int size = this.size;
            if (size >= maxCapacity || dropHandle(item)) {
                // Hit the maximum capacity or should drop - drop the possibly youngest object.
                return;
            }
            if (size == elements.length) {
                elements = Arrays.copyOf(elements, min(size << 1, maxCapacity));
            }

            elements[size] = item;
            this.size = size + 1;
        }

如果当前线程就是创建byteBUf的线程,pushNow就是直接把对象放入elements数组里。

 static PooledUnsafeHeapByteBuf newUnsafeInstance(int maxCapacity) {
        PooledUnsafeHeapByteBuf buf = RECYCLER.get();
        buf.reuse(maxCapacity);
        return buf;
    }
    public final T get() {
        if (maxCapacityPerThread == 0) {
            return newObject((Handle<T>) NOOP_HANDLE);
        }
        Stack<T> stack = threadLocal.get();
        DefaultHandle<T> handle = stack.pop();
        if (handle == null) {
            handle = stack.newHandle();
            handle.value = newObject(handle);
        }
        return (T) handle.value;
    }

在生成bytebuf时,先从RECYCLER对象池中获取,如没有则重新生成

   @SuppressWarnings({ "unchecked", "rawtypes" })
        DefaultHandle<T> pop() {
            int size = this.size;
            if (size == 0) {
                if (!scavenge()) {
                    return null;
                }
                size = this.size;
            }
            size --;
            DefaultHandle ret = elements[size];
            elements[size] = null;
            if (ret.lastRecycledId != ret.recycleId) {
                throw new IllegalStateException("recycled multiple times");
            }
            ret.recycleId = 0;
            ret.lastRecycledId = 0;
            this.size = size;
            return ret;
        }

注意scavenge,将其他线程的buf对象移到自身线程上.

看一张图

在这里插入图片描述

WeakOrderQueue中存储了各个线程,使用WeakRefrence,当线程消亡后,不影响线程对象的gc

对象在被创建的对象中可自由存取,如果创建后被传到其他线程,那么DefaultHandle放到当前线程的WeakQueue上

      private void pushLater(DefaultHandle<?> item, Thread thread) {
            // we don't want to have a ref to the queue as the value in our weak map
            // so we null it out; to ensure there are no races with restoring it later
            // we impose a memory ordering here (no-op on x86)
            Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();
            WeakOrderQueue queue = delayedRecycled.get(this);
            if (queue == null) {
                if (delayedRecycled.size() >= maxDelayedQueues) {
                    // Add a dummy queue so we know we should drop the object
                    delayedRecycled.put(this, WeakOrderQueue.DUMMY);
                    return;
                }
                // Check if we already reached the maximum number of delayed queues and if we can allocate at all.
                if ((queue = WeakOrderQueue.allocate(this, thread)) == null) {
                    // drop object
                    return;
                }
                delayedRecycled.put(this, queue);
            } else if (queue == WeakOrderQueue.DUMMY) {
                // drop object
                return;
            }

            queue.add(item);
        }

看下WeakOrderQueue的构造,会设置stack的head为当前queue节点,后续可以通过线程关联的stack找到WeakOrderQueue找到其他线程回收的对象

   private WeakOrderQueue(Stack<?> stack, Thread thread) {
            head = tail = new Link();
            owner = new WeakReference<Thread>(thread);
            synchronized (stack) {
                next = stack.head;
                stack.head = this;
            }

            // Its important that we not store the Stack itself in the WeakOrderQueue as the Stack also is used in
            // the WeakHashMap as key. So just store the enclosed AtomicInteger which should allow to have the
            // Stack itself GCed.
            availableSharedCapacity = stack.availableSharedCapacity;
        }

会根据stack 的head上会维护WeakOrderQueue的一个队头 然后每创建一个Queue,都会把新的Queue作为对头,并将next设置成为之前的队头
来看stack和handler的关系
Recycle为每个对象池的中的对象都包装了一个DefaultHandler

static final class DefaultHandle<T> implements Handle<T> {
        private int lastRecycledId;
        private int recycleId;

        boolean hasBeenRecycled;

        private Stack<?> stack;
        private Object value;

        DefaultHandle(Stack<?> stack) {
            this.stack = stack;
        }

        @Override
        public void recycle(Object object) {
            if (object != value) {
                throw new IllegalArgumentException("object does not belong to handle");
            }
            stack.push(this);
        }
    }

再看stack的来源

   static final class Stack<T> {

        // we keep a queue of per-thread queues, which is appended to once only, each time a new thread other
        // than the stack owner recycles: when we run out of items in our stack we iterate this collection
        // to scavenge those that can be reused. this permits us to incur minimal thread synchronisation whilst
        // still recycling all items.
        final Recycler<T> parent;
        final Thread thread;
        final AtomicInteger availableSharedCapacity;
        final int maxDelayedQueues;

        private final int maxCapacity;
        private final int ratioMask;
        private DefaultHandle<?>[] elements;
        private int size;
        private int handleRecycleCount = -1; // Start with -1 so the first one will be recycled.
        private WeakOrderQueue cursor, prev;
        private volatile WeakOrderQueue head;

        Stack(Recycler<T> parent, Thread thread, int maxCapacity, int maxSharedCapacityFactor,
              int ratioMask, int maxDelayedQueues) {
            this.parent = parent;
            this.thread = thread;
            this.maxCapacity = maxCapacity;
            availableSharedCapacity = new AtomicInteger(max(maxCapacity / maxSharedCapacityFactor, LINK_CAPACITY));
            elements = new DefaultHandle[min(INITIAL_CAPACITY, maxCapacity)];
            this.ratioMask = ratioMask;
            this.maxDelayedQueues = maxDelayedQueues;
        }
   private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {
        @Override
        protected Stack<T> initialValue() {
            return new Stack<T>(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor,
                    ratioMask, maxDelayedQueuesPerThread);
        }
    };

可以看出来stack是和线程绑定的,而handler中存储的是对应创建对象的线程的stack。如果handler被传递到别的线程释放,那么会创建对应线程的weakQueue,

 private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED =
            new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() {
        @Override
        protected Map<Stack<?>, WeakOrderQueue> initialValue() {
            return new WeakHashMap<Stack<?>, WeakOrderQueue>();
        }
    };

一个线程的weakQueue存放在threadLocal中,Key为各个从别的线程传过来的stack。、
并且在创建weakQueue时形成链,也就是说 同一个stack在不同的线程中创建的weakQueue会形成链,在获取对象池对象时,先从本线程直接取到的emelemts数组中取,如果取不到,通过scavenge,查询所有的weakqueue,找到属于本线程的DefaultHandle,并放回本线程的emelemts数组中.

对象池为什么要这么设计?

保证了对象池中的对象只能被创建该对象实例的线程再次使用,其他线程无法使用,避免了被多线程共享,保证了线程安全

内存池

netty对象池比较复杂,不再深入研究,大概理解下就是,对象池中有一块大的buf数组,创建bytebuf的时候不会为其分配新的byte数组,而是引用对象池中的byte数组,并且给一个偏移量标示其占用的内存空间,只是大概理解一下,对象池的实现十分复杂

netty零拷贝

Netty 中的 Zero-copy与上面我们所提到到 OS 层面上的 Zero-copy不太一样, Netty的 Zero-coyp完全是在用户态(Java 层面)的, 它的 Zero-copy的更多的是偏向于 优化数据操作这样的概念.

Netty 的 Zero-copy体现在如下几个个方面:

Netty 提供了 CompositeByteBuf类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝.
通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作.
ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝.

看一个例子


  ByteBuf A  = ByteBufAllocator.DEFAULT.heapBuffer(521);
        A.writeBytes("Hello".getBytes());
        ByteBuf B  = ByteBufAllocator.DEFAULT.heapBuffer(521);
        B.writeBytes(" World".getBytes());
        A.writeBytes(B);
        byte b[] = new byte[20];
        A.getBytes(0,b);
        System.out.println(new String(b));
  @Override
    public final ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
        //index就为writerIndex
        checkSrcIndex(index, length, srcIndex, src.length);
        System.arraycopy(src, srcIndex, memory, idx(index), length);
        return this;
    }
protected final int idx(int index) {
        return offset + index;
    }

可以看出来,写位置为ByteBuf中writerIndex,因为使用了对象池的buf,再拷贝的时候并不会从0开始拷贝,而是经过idx方法根据偏移量计算下标,作为起始位置进行拷贝。

看到这里的 System.arraycopy(src, srcIndex, memory, idx(index), length);就知道这里明显发生了拷贝操作

再看 A.getBytes(0,b);
由于内存池的原因,A和B各自都要计算在内存之偏移量和长度,最终进行byte数组的拷贝

        //参数分别为A writeIndex,b的byte数组,b的偏移writeIndex,要写入b的长度  
        setBytes(index, src.array(), src.arrayOffset() + srcIndex, length);
        //参数分别为    b的byte数组,b的偏移writeIndex, A的 byte数组 A的偏移writeIndex,要写入b的长度 
            System.arraycopy(src, srcIndex, memory, idx(index), length);

明显这里有发生了拷贝,不是零拷贝

那么再看看零拷贝是怎么做的compositeByteBuf

再看代码

 public static void main(String[] args) {
        ByteBuf A  = ByteBufAllocator.DEFAULT.heapBuffer(521);
        A.writeBytes("Hello".getBytes());
        ByteBuf B  = ByteBufAllocator.DEFAULT.heapBuffer(521);
        B.writeBytes(" World".getBytes());
        ByteBuf allByteBuf = Unpooled.wrappedBuffer(A, B);
        byte b[] = new byte[11];
        allByteBuf.getBytes(0,b);
        System.out.println(new String(b));

    }

首先了解slice方法,bytebuf创建AbstractUnpooledSlicedByteBuf,并返回

AbstractUnpooledSlicedByteBuf(ByteBuf buffer, int index, int length) {
        super(length);
        sliceByteBuf引用slice执行slice的buf,设置长度 writeIndex等
        this.buffer = buffer;
        adjustment = index;
        

        initLength(length);
        writerIndex(length);
    }

slice方法,返回原始ByteBuf可读字节的一部分, 修改返回的缓冲区或此缓冲区的内容会影响彼此的内容,他们维护单独的index和makers,此方法不会修改原始缓冲区的readerIndex或writerIndex。
可见,slice方法并没用去复制新的bytebuf而是,引用了其中对方,并且单独维护 index

再看io.netty.buffer.CompositeByteBuf这个类
这个类里面又一个io.netty.buffer.CompositeByteBuf#components集合
第一个component,offset为0,endoffset为真正的长度,第二个component,offset为前一个的endoffset,endOffset为前一个endoffset+第二个component的真实可读长度

其中Componen中存的是buf的sliceBuf

  Component c = new Component(buffer.order(ByteOrder.BIG_ENDIAN).slice());
            if (cIndex == components.size()) {
                wasAdded = components.add(c);
                if (cIndex == 0) {
                    c.endOffset = readableBytes;
                } else {
                    Component prev = components.get(cIndex - 1);
                    c.offset = prev.endOffset;
                    c.endOffset = c.offset + readableBytes;
                }
            }

这样 byteBuf-> sliceBuf->.CompositeByteBuf都是逻辑上对数据的划分,而没有发生真正的copy,因此是零拷贝

引用计数

retain和release方法

retain方法返回自身,引用计数+1

private ByteBuf retain0(int increment) {
        for (;;) {
            int refCnt = this.refCnt;
            final int nextCnt = refCnt + increment;

            // Ensure we not resurrect (which means the refCnt was 0) and also that we encountered an overflow.
            if (nextCnt <= increment) {
                throw new IllegalReferenceCountException(refCnt, increment);
            }
            if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {
                break;
            }
        }
        return this;
    }

release引用计数-1,减到0释放内存

 private boolean release0(int decrement) {
        for (;;) {
            int refCnt = this.refCnt;
            if (refCnt < decrement) {
                throw new IllegalReferenceCountException(refCnt, -decrement);
            }

            if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
                if (refCnt == decrement) {
                    deallocate();
                    return true;
                }
                return false;
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值