Java中的IO

Java中的IO

不同的IO的方式是以不同的抽象模型和交互方式区分的。

BIO Block IO 同步阻塞

NIO NonBlock IO 多路复用,同步非阻塞

AIO Async IO 异步非阻塞

BIO

最传统的IO模型,基于流模型实现,如文件流、网络流等。
交互方式是同步、阻塞的方式。在读取或写入完成之前,线程会一直阻塞。代码会可靠的顺序执行。

下面是使用BIO的Socket Api实现的网络Server端,accept()方法会一直阻塞,直到有客户端连接上来。为了更多的客户端提供服务,一般会单独开启一个线程来处理与客户端的交互。

private void openServerSocket(int port, int maxConnCount) {
        try {
            ServerSocket serverSocket = new ServerSocket(port, maxConnCount);
            while (true) {
                Socket clientSocket = serverSocket.accept();//阻塞
                new ClientRequestHandler(clientSocket).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    class ClientRequestHandler extends Thread {
        private Socket clientSocket;

        ClientRequestHandler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }

        @Override
        public void run() {
            super.run();
            try {
                BufferedInputStream bufferedInputStream = new BufferedInputStream(clientSocket.getInputStream());
                byte[] buffer = new byte[1024];
                int readSize;
                while ((readSize = bufferedInputStream.read(buffer)) != -1) {//同步
                    String readContent = new String(buffer, 0, readSize);
                }

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

这是典型的构建并发服务的方式,弊端就是需要频繁创建销毁线程,当然可以通过线程池来实现线程的复用。但当并发请求数急剧上升时,频繁的线程上下文切换就会成为性能瓶颈。

NIO

在java.nio包下,提供了同步非阻塞方式,基于通道和缓冲区实现。

Buffer 高效的数据容器

Channel 通道,一个通道对应一个输入/输出流,支持批量IO操作的抽象

Selector NIO多路复用的基础,可以检测注册到Selector上的多个Channel是否处于就绪状态,从而实现单线程对多个Channel的管理。

我们再通过NIO方式实现一个服务端。

public class SocketTest {

    private class NIOServer extends Thread {

        private int port;

        Selector selector = null;

        public NIOServer(int port) {
            this.port = port;
            try {
                selector = Selector.open();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @RequiresApi(api = Build.VERSION_CODES.N)
        @Override
        public void run() {
            super.run();
            try {
                ServerSocketChannel socketChannel = ServerSocketChannel.open();
                socketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
                socketChannel.configureBlocking(false);//配置为非阻塞,在阻塞模式下不允许注册操作
                socketChannel.register(selector, SelectionKey.OP_ACCEPT);//注册到Selector上,并说明触发事件类型

                while (true) {
                    int readyCount = selector.select();//阻塞等待就绪的Channel,也就是等待客户端连接。返回值是就绪Channel的数量

                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey selectionKey = iterator.next();

                        if (selectionKey.isAcceptable()) {
                            //新连接事件
                            handleClientConnection((ServerSocketChannel) selectionKey.channel());
                        } else if (selectionKey.isReadable()) {
                            //有数据可以读取事件
                        }
                        iterator.remove();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }


        private void handleClientConnection(ServerSocketChannel serverSocketChannel) throws IOException {
            //取出客户端连接,并发送一个字符串。
            //如果连接不可读或不可写不会阻塞,直接返回null。
            SocketChannel client = serverSocketChannel.accept();
            if (client != null) {
                client.write(Charset.defaultCharset().encode("Hello world!"));
                //这里通过注册一个数据可读事件,而不是阻塞线程直到有数据可读取,这就是NIO单线程实现并发服务的的原因
                client.register(selector, SelectionKey.OP_READ);//注册有数据可读事件
            }
        }
    }
}

NIO模式中只使用了一个线程,通过事件轮询事件高效定位就绪的Channel,也就是可读写的连接。仅仅在select阶段是阻塞的,避免多线程频繁切换带来的开销。

可能有同学会产生这样一个疑问:向SocketChannel中写入或读取数据时不会阻塞线程么?如果写入数据比较大,会不会影响其他客户端的连接。

答案是会阻塞线程,如果有请求发生在read/write阶段,并且数据资源较大,会导致线程长期阻塞,这就是多路复用的性能瓶颈。解决方法还是开启新线程来处理IO操作。

NIO2 (AIO)

在NIO中,服务端在等待客户端连接时和数据读写时还是会阻塞当前线程,所以在java7又引入了NIO2,也叫AIO异步IO。它将数据读写和Socket监听做到了异步实现。下面仍以Socket为例,展示AIO的用法。

public class Server {

    @RequiresApi(api = Build.VERSION_CODES.O)
    void create(int port) {
        SocketAddress socketAddress;
        socketAddress = new InetSocketAddress(port);

        try {
            final AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open().bind(socketAddress);
            //1、通过指定回调方法,不阻塞当前线程执行
            serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
                @Override
                public void completed(AsynchronousSocketChannel result, Void attachment) {
                    serverSocketChannel.accept(null, this);//等待下一个连接

                    System.out.println("客户端连接成功");

                    handleConnection(result);

                }

                @Override
                public void failed(Throwable exc, Void attachment) {

                }
            });

            System.out.println("监听客户端连接,不阻塞");

            //2、如果要阻塞线程,直到客户端连接,可使用如下方式
//                Future<AsynchronousSocketChannel> acceptFuture = serverSocketChannel.accept();
//                AsynchronousSocketChannel socketChannel = acceptFuture.get();


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    private void handleConnection(AsynchronousSocketChannel socketChannel) {
        ByteBuffer writeContent = Charset.defaultCharset().encode("Hello world!");

        socketChannel.write(writeContent, null, new CompletionHandler<Integer, Object>() {
            @Override
            public void completed(Integer result, Object attachment) {
                System.out.println("Send to client success: " + result);
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                System.out.println("Send to client failed: " + exc.getMessage());
            }
        });

        final ByteBuffer readContent = ByteBuffer.allocate(100);
        socketChannel.read(readContent, null, new CompletionHandler<Integer, Object>() {
            @Override
            public void completed(Integer result, Object attachment) {
                System.out.println("Read from client success: " + new String(readContent.array(), 0, readContent.position()));
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                System.out.println("Read from client failed: " + exc.getMessage());
            }
        });

    }


    @RequiresApi(api = Build.VERSION_CODES.O)
    public static void main(String[] args) {
        Server server = new Server();
        server.create(8888);

        //因为AIO不阻塞线程,所以需要手动阻塞。
        try {
            Thread.currentThread().join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

可以看到,AsynchronousServerSocketChannel通过回调机制,告诉调用者事件的发生。

底层实现原理

BIO

系统读写Api read、write就是阻塞的,并且有用户空间和内核空间环境的切换。

需要在用户空间和内核空间之间发生两次数据拷贝。磁盘文件拷贝到内核缓存区,内核缓存区再拷贝到用户空间内存。

下面以文件读写为例,追踪BIO的底层实现:

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

这是FileInputStream提供的读操作Api,根据注释可以看出它是从当前流中读取指定len长度的数据到字节数组b[]中。这个方法会一直阻塞,直到数据准备就绪。如果len为0则直接返回。返回值代表本次读取到字节数组的实际长度,如果返回-1,则代表已经到达文件末尾。
最终会转到native层实现。

jint
readBytes(JNIEnv *env, jobject this, jbyteArray bytes,
          jint off, jint len, jfieldID fid)
{
    jint nread;
    char stackBuf[BUF_SIZE];
    char *buf = NULL;
    FD fd;
    
    if (len == 0) {
        return 0;
    } else if (len > BUF_SIZE) {
        //申请一块内存,用来临时存储读取的数据
        buf = malloc(len);
        if (buf == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            return 0;
        }
    } else {
        buf = stackBuf;
    }
    
    fd = GET_FD(this, fid);
    if (fd == -1) {
        JNU_ThrowIOException(env, "Stream Closed");
        nread = -1;
    } else {
        //根据文件描述符,读取数据到缓存区buf中,nread是读取数据长度
        nread = IO_Read(fd, buf, len);
        if (nread > 0) {
            //如果读取成功,将缓冲区buf数据拷贝到Java层提供的bytes字节数组中
            (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);
        } else if (nread == -1) {
            JNU_ThrowIOExceptionWithLastError(env, "Read error");
        } else { /* EOF */
            nread = -1;
        }
    }
    
    if (buf != stackBuf) {
        free(buf);
    }
    return nread;

}

这里的IO_Read要区分不同的操作系统,在Solaris系统使用read函数,在Windows系统上使用ReadFile函数。

//solaris/native/java/io/io_util_md.c

ssize_t
handleRead(FD fd, void *buf, jint len)
{
    ssize_t result;
    RESTARTABLE(read(fd, buf, len), result);
    return result;
}
//windows/native/java/io/io_util_md.c

JNIEXPORT
jint
handleRead(FD fd, void *buf, jint len)
{
    DWORD read = 0;
    BOOL result = 0;
    HANDLE h = (HANDLE)fd;
    if (h == INVALID_HANDLE_VALUE) {
        return -1;
    }
    result = ReadFile(h,          /* File handle to read */
                      buf,        /* address to put data */
                      len,        /* number of bytes to read */
                      &read,      /* number of bytes read */
                      NULL);      /* no overlapped struct */
    if (result == 0) {
        int error = GetLastError();
        if (error == ERROR_BROKEN_PIPE) {
            return 0; /* EOF */
        }
        return -1;
    }
    return (jint)read;
}

read和ReadFile函数都属于系统调用,会将运行环境从用户态切换到内核态,由系统内核先从文件读取数据到内核缓冲区,然后将数据拷贝到用户空间指定的内存区域内,这个过程用户进程是阻塞的,知道读取完成,函数返回继续执行。

另外,我们可以看到read函数读取完成之后,又通过SetByteArrayRegion函数做了一次数据拷贝。

NIO

Linux系统上,使用Epoll机制注册事件监听。
使用DMA(直接内存访问)DirectBuffer使用户空间和内核空间共享一块内存,从而少一次的数据复制。

ByteBuffer分为HeapByteBuffer和DirectByteBuffer,前者就是Java堆上分配的字节数组,后者则是分配的堆外内存。

使用FileChannelImpl读取数据时,不是用字节数组来承载,而是通过ByteBuffer.

//sun/nio/ch/FileChannelImpl.java

    public int read(ByteBuffer dst) throws IOException {
        ensureOpen();
        if (!readable)
            throw new NonReadableChannelException();
        synchronized (positionLock) {
            int n = 0;
            int ti = -1;
            try {
                begin();
                ti = threads.add();
                if (!isOpen())
                    return 0;
                do {
                    //调用IOUtil的read方法,其中nd是FileDispatcherImpl对象,用来区分不同平台实现
                    n = IOUtil.read(fd, dst, -1, nd);
                } while ((n == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(n);
            } finally {
                threads.remove(ti);
                end(n > 0);
                assert IOStatus.check(n);
            }
        }
    }
// sun/nio/ch/IOUtil.java

    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);
        }
    }
    
    
    private static int readIntoNativeBuffer(FileDescriptor fd, ByteBuffer bb,
                                            long position, NativeDispatcher nd)
        throws IOException
    {
        int pos = bb.position();
        int lim = bb.limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);

        if (rem == 0)
            return 0;
        int n = 0;
        //申请的直接内存访问区ByteBuffer可以获取到内存地址address
        //Native层可以直接使用该地址,进行数据写入
        
        //这里区分是否指定文件的开始读取位置
        if (position != -1) {
            n = nd.pread(fd, ((DirectBuffer)bb).address() + pos,
                         rem, position);
        } else {
            n = nd.read(fd, ((DirectBuffer)bb).address() + pos, rem);
        }
        if (n > 0)
            bb.position(pos + n);
        return n;
    }
//solaris/native/sun/ch/FileDispatcherImpl.c

//从头开始读
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_read0(JNIEnv *env, jclass clazz,
                             jobject fdo, jlong address, jint len)
{
    jint fd = fdval(env, fdo);
    void *buf = (void *)jlong_to_ptr(address);

    return convertReturnVal(env, read(fd, buf, len), JNI_TRUE);
}

//指定文件偏移量读
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_pread0(JNIEnv *env, jclass clazz, jobject fdo,
                            jlong address, jint len, jlong offset)
{
    jint fd = fdval(env, fdo);
    void *buf = (void *)jlong_to_ptr(address);

    return convertReturnVal(env, pread64(fd, buf, len, offset), JNI_TRUE);
}

我们看到NIO的文件读取与BIO的文件操作是一致的,都是使用各个平台的读取函数read/ReadFile。
区分在于,NIO使用DirectBuffer开辟堆外内存,并将内存地址交给Native层来缓存文件数据,相对于BIO少了一次内存拷贝。

那NIO的非阻塞特性是如何体现的呢?

前面说的都是针对本地文件IO流,当需要读取数据时不需要判断文件是否存在可读的数据,直接去读就好了,有数据就读到了,没有数据不会等着。
而对于网络IO流,接受者是不知道什么时候数据会发送过来,所以read时会一直阻塞,直到有数据可以读。
而NIO解决了等待数据时的阻塞,而不是读数据的阻塞。

那NIO是怎么知道网络IO流可读呢?前面已经说过了,就是epoll机制。

接受者只需在IO文件上,注册数据可读/可写的事件,当数据来了之后,系统内核会发送数据可读的信号,接受者只需要不停的观察有没有可读信号就可以了。

下面我们依据前面NIOde使用流程,追踪其源码实现。

//java/nio/channels/Selector.java

//创建一个选择器
public static Selector open() throws IOException {

    //SelectorProvider根据系统配置或系统平台选择合适的SelectorProvider来创建一个Selector
    return SelectorProvider.provider().openSelector();
}
//sun/nio/ch/DefaultSelectorProvider.java
    
    //假如没有特殊配置,则使用默认Selector提供器
    public static SelectorProvider create() {
        String osname = AccessController
            .doPrivileged(new GetPropertyAction("os.name"));
            //根据不同的平台,创建不同机制的SelectorProvider
        if (osname.equals("SunOS"))
            return createProvider("sun.nio.ch.DevPollSelectorProvider");
        if (osname.equals("Linux"))
            return createProvider("sun.nio.ch.EPollSelectorProvider");
        return new sun.nio.ch.PollSelectorProvider();
    }
//solaries/sun/nio/ch/EpollSelectorProvider.java

//Linux系统上的实现
public class EPollSelectorProvider
    extends SelectorProviderImpl
{
    public AbstractSelector openSelector() throws IOException {
        return new EPollSelectorImpl(this);
    }

    public Channel inheritedChannel() throws IOException {
        return InheritedChannel.getChannel();
    }
}
//solaries/sun/nio/ch/EPollSelectorImpl

//构造函数
EPollSelectorImpl(SelectorProvider sp) throws IOException {
        super(sp);
        long pipeFds = IOUtil.makePipe(false);
        fd0 = (int) (pipeFds >>> 32);
        fd1 = (int) pipeFds;
        pollWrapper = new EPollArrayWrapper();
        pollWrapper.initInterrupt(fd0, fd1);
        fdToKey = new HashMap<>();
}

可以看到Linux上是通过epoll+管道实现的,IOUtil.makePipe(false)会创建一个管道,并返回管道读写的两个文件描述符,高32位是读端,低32位是写端。

创建Selector成功后,还需要创建需要监听的对象。
ServerSocketChannel socketChannel = ServerSocketChannel.open();

ServerSocketChannelImpl(SelectorProvider sp) throws IOException {
        super(sp);
        this.fd =  Net.serverSocket(true);
        this.fdVal = IOUtil.fdVal(fd);
        this.state = ST_INUSE;
}

然后将ServerSocketChannel注册到Selector上。
socketChannel.register(selector, SelectionKey.OP_ACCEPT);

//solaries/sun/nio/ch/EPollSelectorImpl

protected void implRegister(SelectionKeyImpl ski) {
   if (closed)
       throw new ClosedSelectorException();
   SelChImpl ch = ski.channel;
   //这里的fd,就是ServerSocketChannelImpl创建时生成的socket文件描述
   int fd = Integer.valueOf(ch.getFDVal());
   fdToKey.put(fd, ski);
   //将文件描述符添加epoll的监听中
   //pollWrapper是对Epoll操作的封装
   pollWrapper.add(fd);
   keys.add(ski);
}

注册完事件后,就可以等待事件发生了。

int readyCount = selector.select();
Set selectionKeys = selector.selectedKeys();

select()方法会阻塞线程,直到事件发生,返回值就是就绪事件的数量。之后可以通过selectedKeys()方法查询到发生的事件类型。

//solaries/sun/nio/ch/EPollSelectorImpl

protected int doSelect(long timeout) throws IOException {
   if (closed)
       throw new ClosedSelectorException();
   processDeregisterQueue();
   try {
       begin();
       //poll方法内部就是Native层的epoll_wait函数,该函数会阻塞当前线程
       pollWrapper.poll(timeout);
   } finally {
       end();
   }
   processDeregisterQueue();
   //发生事件后,线程被唤醒,这里通过updateSelectedKeys()方法查找发生IO事件的文件描述符以及事件类型
   //避免由调用者遍历所有文件描述符
   int numKeysUpdated = updateSelectedKeys();
   if (pollWrapper.interrupted()) {
       // Clear the wakeup pipe
       pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
       synchronized (interruptLock) {
           pollWrapper.clearInterrupted();
           IOUtil.drain(fd0);
           interruptTriggered = false;
       }
   }
   return numKeysUpdated;
}

//更新被epoll选择的事件集合,将准备好的事件添加到准备队列中
private int updateSelectedKeys() {
   int entries = pollWrapper.updated;
   int numKeysUpdated = 0;
   for (int i=0; i<entries; i++) {
       int nextFD = pollWrapper.getDescriptor(i);
       SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
       // ski is null in the case of an interrupt
       if (ski != null) {
           int rOps = pollWrapper.getEventOps(i);
           if (selectedKeys.contains(ski)) {
               if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
                   numKeysUpdated++;
               }
           } else {
               ski.channel.translateAndSetReadyOps(rOps, ski);
               if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
                   selectedKeys.add(ski);
                   numKeysUpdated++;
               }
           }
       }
   }
   return numKeysUpdated;
}
//share/sun/nio/ch/SelectorImpl

public Set<SelectionKey> selectedKeys() {
   if (!isOpen() && !Util.atBugLevel("1.4"))
       throw new ClosedSelectorException();
       //publicSelectedKeys就是准备就绪的事件集合
   return publicSelectedKeys;
}

AIO

前面BIO和NIO在读写数据时都是使用Native层的读写函数read/ReadFile,它们都是同步的,也就是在数据被拷贝到用户空间前,当前线程要一直阻塞。那么有没有办法不阻塞呢,答案是AIO异步IO。AIO机制中,数据读写时不再阻塞,当数据读写完成后通过回调的方式告诉调用者,并且去除了NIO中的事件轮询。
类似下面代码所示:

AsynchronousSocketChannel socketChannel;

final ByteBuffer readContent = ByteBuffer.allocate(100);
//每当接收到客户端的数据时,通过CompletionHandler回调通知调用者,而不是一直阻塞在read函数上。
socketChannel.read(readContent, null, new CompletionHandler<Integer, Object>() {
       @Override
       public void completed(Integer result, Object attachment) {
           System.out.println("Read from client success: " + new    
            String(readContent.array(), 0, readContent.position()) + "-" + 
            Thread.currentThread().getName());
       }

       @Override
       public void failed(Throwable exc, Object attachment) {
           System.out.println("Read from client failed: " + exc.getMessage());
       }
});

异步IO的实现分为两种,一种是利用系统特性;另外一种是固定线程池。后者比较简单,就是新启动一个线程来读写数据,如下代码所示:

//share/sun/nio/ch/SimpleAsynchronousFileChannelImpl.java

    @Override
    <A> Future<Integer> implRead(final ByteBuffer dst,
                                 final long position,
                                 final A attachment,
                                 final CompletionHandler<Integer,? super A> handler)
    {
        if (position < 0)
            throw new IllegalArgumentException("Negative position");
        if (!reading)
            throw new NonReadableChannelException();
        if (dst.isReadOnly())
            throw new IllegalArgumentException("Read-only buffer");

        // complete immediately if channel closed or no space remaining
        if (!isOpen() || (dst.remaining() == 0)) {
            Throwable exc = (isOpen()) ? null : new ClosedChannelException();
            if (handler == null)
                return CompletedFuture.withResult(0, exc);
            Invoker.invokeIndirectly(handler, attachment, 0, exc, executor);
            return null;
        }

        final PendingFuture<Integer,A> result = (handler == null) ?
            new PendingFuture<Integer,A>(this) : null;
            
        //新建一个异步任务
        Runnable task = new Runnable() {
            public void run() {
                int n = 0;
                Throwable exc = null;

                int ti = threads.add();
                try {
                    begin();
                    do {
                        //线程内部还是同步读
                        n = IOUtil.read(fdObj, dst, position, nd);
                    } while ((n == IOStatus.INTERRUPTED) && isOpen());
                    if (n < 0 && !isOpen())
                        throw new AsynchronousCloseException();
                } catch (IOException x) {
                    if (!isOpen())
                        x = new AsynchronousCloseException();
                    exc = x;
                } finally {
                    end();
                    threads.remove(ti);
                }
                if (handler == null) {
                    result.setResult(n, exc);
                } else {
                    //读完之后,回调给调用者
                    Invoker.invokeUnchecked(handler, attachment, n, exc);
                }
            }
        };
        //交给线程池执行
        executor.execute(task);
        return result;
    }

利用系统特性实现异步IO:

//solaris/sun/nio/ch/UnixAsynchronousSocketChannelImpl.java

<V extends Number,A> Future<V> implRead(boolean isScatteringRead,
                                            ByteBuffer dst,
                                            ByteBuffer[] dsts,
                                            long timeout,
                                            TimeUnit unit,
                                            A attachment,
                                            CompletionHandler<V,? super A> handler)
    {
        // A synchronous read is not attempted if disallowed by system property
        // or, we are using a fixed thread pool and the completion handler may
        // not be invoked directly (because the thread is not a pooled thread or
        // there are too many handlers on the stack).
        Invoker.GroupAndInvokeCount myGroupAndInvokeCount = null;
        boolean invokeDirect = false;
        boolean attemptRead = false;
        //判断系统是否支持异步读
        //如果支持,判断是否传入回调函数handler,如果没有传则认为是临时尝试读,采用同步IO方式
        if (!disableSynchronousRead) {
            if (handler == null) {
                attemptRead = true;
            } else {
                myGroupAndInvokeCount = Invoker.getGroupAndInvokeCount();
                invokeDirect = Invoker.mayInvokeDirect(myGroupAndInvokeCount, port);
                // okay to attempt read with user thread pool
                attemptRead = invokeDirect || !port.isFixedThreadPool();
            }
        }

        int n = IOStatus.UNAVAILABLE;
        Throwable exc = null;
        boolean pending = false;

        try {
            begin();

            if (attemptRead) {
                if (isScatteringRead) {
                    n = (int)IOUtil.read(fd, dsts, nd);
                } else {
                    n = IOUtil.read(fd, dst, -1, nd);
                }
            }

            //如果IO流处于未就绪状态,则需要异步等待
            if (n == IOStatus.UNAVAILABLE) {
                PendingFuture<V,A> result = null;
                synchronized (updateLock) {
                    this.isScatteringRead = isScatteringRead;
                    this.readBuffer = dst;
                    this.readBuffers = dsts;
                    if (handler == null) {
                        this.readHandler = null;
                        result = new PendingFuture<V,A>(this, OpType.READ);
                        this.readFuture = (PendingFuture<Number,Object>)result;
                        this.readAttachment = null;
                    } else {
                        this.readHandler = (CompletionHandler<Number,Object>)handler;
                        this.readAttachment = attachment;
                        this.readFuture = null;
                    }
                    //设置超时时间后,需要安排一个读超时任务readTimeoutTask
                    if (timeout > 0L) {
                        this.readTimer = port.schedule(readTimeoutTask, timeout, unit);
                    }
                    this.readPending = true;
                    //注册IO事件,并等待事件发生
                    updateEvents();
                }
                pending = true;
                //返回值,如果handler为空则返回一个PendingFuture,否则返回null
                return result;
            }
        } catch (Throwable x) {
            if (x instanceof ClosedChannelException)
                x = new AsynchronousCloseException();
            exc = x;
        } finally {
            if (!pending)
                enableReading();
            end();
        }

        Number result = (exc != null) ? null : (isScatteringRead) ?
            (Number)Long.valueOf(n) : (Number)Integer.valueOf(n);

        // read completed immediately
        if (handler != null) {
            if (invokeDirect) {
                Invoker.invokeDirect(myGroupAndInvokeCount, handler, attachment, (V)result, exc);
            } else {
                Invoker.invokeIndirectly(this, handler, attachment, (V)result, exc);
            }
            return null;
        } else {
            return CompletedFuture.withResult((V)result, exc);
        }
    }
    

//监听IO事件
private void updateEvents() {
   assert Thread.holdsLock(updateLock);
   int events = 0;
   if (readPending)
       events |= Net.POLLIN;
   if (connectPending || writePending)
       events |= Net.POLLOUT;
   if (events != 0)
       port.startPoll(fdVal, events);
}

这里的port是EPollPort类型的实例,利用Linux系统的epoll机制,并使用线程池封装了IO事件的处理,然后将结果分发给CompletionHandler。

//solaris/sun/nio/ch/EpollPort.java

//将需要监听的文件描述符添加到epoll中
@Override
void startPoll(int fd, int events) {
   // update events (or add to epoll on first usage)
   int err = epollCtl(epfd, EPOLL_CTL_MOD, fd, (events | EPOLLONESHOT));
   if (err == ENOENT)
       err = epollCtl(epfd, EPOLL_CTL_ADD, fd, (events | EPOLLONESHOT));
   if (err != 0)
       throw new AssertionError();     // should not happen
}

到这里有点懵逼,startPoll只是调用epoll_ctl添加了监听事件,但并没有epoll_wait。
实际上epoll_wait是在构造EpollPort实例的时候就开始了。

//solaris/sun/nio/ch/LinuxAsynchronousChannelProvider.java

//创建EpollPort实例
private EPollPort defaultEventPort() throws IOException {
   if (defaultPort == null) {
       synchronized (LinuxAsynchronousChannelProvider.class) {
           if (defaultPort == null) {
            //指定了线程池,并调用start()方法
            //这个线程池是带缓存的线程池newCachedThreadPool,初始化线程数可以由系统属性指定,如果没有指定则是CPU核心数
               defaultPort = new EPollPort(this, ThreadPool.getDefault()).start();
           }
       }
   }
   return defaultPort;
}
//solaris/sun/nio/ch/EpollPort.java

//start()方法就是启动一个新线程死循环执行epoll_wait函数
EPollPort start() {
   startThreads(new EventHandlerTask());
   return this;
}


private class EventHandlerTask implements Runnable {
    public void run() {
        ...
      
        for (;;) {
            ev = queue.take();//取出事件队列的头部,判断是否需要开始poll
            if (ev == NEED_TO_POLL) {
                try {
                    ev = poll();
                } catch (IOException x) {
                    x.printStackTrace();
                    return;
                }
            }
            
            //poll函数返回时,说明有事件发生,当前线程被唤醒,需要处理任务或关闭
            if (ev == EXECUTE_TASK_OR_SHUTDOWN) {
              Runnable task = pollTask();
              if (task == null) {
                  // shutdown request
                  return;
              }
              // run task (may throw error/exception)
              replaceMe = true;
              task.run();
              continue;
            }
            
            //真正处理事件,通过回调发送给调用者
            try {
                  ev.channel().onEvent(ev.events(), isPooledThread);
              } catch (Error x) {
                  replaceMe = true; throw x;
              } catch (RuntimeException x) {
                  replaceMe = true; throw x;
              }
        }
    }
    
    
    //poll()方法的实现就是调用epoll_wait函数
    private Event poll() throws IOException {
        try {
                for (;;) {
                    int n = epollWait(epfd, address, MAX_EPOLL_EVENTS);
                    /*
                     * 'n' events have been read. Here we map them to their
                     * corresponding channel in batch and queue n-1 so that
                     * they can be handled by other handler threads. The last
                     * event is handled by this thread (and so is not queued).
                     */
                    fdToChannelLock.readLock().lock();
                    try {
                        while (n-- > 0) {
                            long eventAddress = getEvent(address, n);
                            int fd = getDescriptor(eventAddress);

                            // wakeup
                            if (fd == sp[0]) {
                                if (wakeupCount.decrementAndGet() == 0) {
                                    // no more wakeups so drain pipe
                                    drain1(sp[0]);
                                }

                                // queue special event if there are more events
                                // to handle.
                                if (n > 0) {
                                    queue.offer(EXECUTE_TASK_OR_SHUTDOWN);
                                    continue;
                                }
                                return EXECUTE_TASK_OR_SHUTDOWN;
                            }

                            PollableChannel channel = fdToChannel.get(fd);
                            if (channel != null) {
                                int events = getEvents(eventAddress);
                                Event ev = new Event(channel, events);

                                // n-1 events are queued; This thread handles
                                // the last one except for the wakeup
                                //遍历所有事件,并添加到队列中,最后返回最后一个事件
                                if (n > 0) {
                                    queue.offer(ev);
                                } else {
                                    return ev;
                                }
                            }
                        }
                    } finally {
                        fdToChannelLock.readLock().unlock();
                    }
                }
            } finally {
                // to ensure that some thread will poll when all events have
                // been consumed
                //添加完所有事件到队列中,还需要再添加一个NEED_TO_POLL事件,告诉外层循环:处理完事件后,还要继续执行epoll_wait
                queue.offer(NEED_TO_POLL);
            }
    }
}

实现了onEvent回调的类将会实际处理对应事件:

// solaris/sun/nio/ch/UnixAsynchronousSocketChannelImpl.java

    /**
     * 当文件描述被轮询到事件时,由事件处理程序线程调用该方法
     */
    @Override
    public void onEvent(int events, boolean mayInvokeDirect) {
        boolean readable = (events & Net.POLLIN) > 0;
        boolean writable = (events & Net.POLLOUT) > 0;
        if ((events & (Net.POLLERR | Net.POLLHUP)) > 0) {
            readable = true;
            writable = true;
        }
        finish(mayInvokeDirect, readable, writable);
    }
    
    //根据事件类型判断是读、写、或新连接,实际上新连接也是一个写操作
    private void finish(boolean mayInvokeDirect,
                        boolean readable,
                        boolean writable)
    {
        boolean finishRead = false;
        boolean finishWrite = false;
        boolean finishConnect = false;

        // map event to pending result
        synchronized (updateLock) {
            if (readable && this.readPending) {
                this.readPending = false;
                finishRead = true;
            }
            if (writable) {
                if (this.writePending) {
                    this.writePending = false;
                    finishWrite = true;
                } else if (this.connectPending) {
                    this.connectPending = false;
                    finishConnect = true;
                }
            }
        }

        // complete the I/O operation. Special case for when channel is
        // ready for both reading and writing. In that case, submit task to
        // complete write if write operation has a completion handler.
        if (finishRead) {
            if (finishWrite)
                finishWrite(false);
            finishRead(mayInvokeDirect);
            return;
        }
        if (finishWrite) {
            finishWrite(mayInvokeDirect);
        }
        if (finishConnect) {
            finishConnect(mayInvokeDirect);
        }
    }
    
    //如果是可读事件,则调用系统提供的read/ReadFile函数去读取文件
    private void finishRead(boolean mayInvokeDirect) {
        ...
        //读数据,将数据写入readBuffer中,就是调用者传入的读缓冲区
        if (scattering) {
           n = (int)IOUtil.read(fd, readBuffers, nd);
        } else {
           n = IOUtil.read(fd, readBuffer, -1, nd);
        }
        
        ...
        
        //告知调用者,读操作完成了
        // invoke handler or set result
        if (handler == null) {
            future.setResult(result, exc);
        } else {
            if (mayInvokeDirect) {
                Invoker.invokeUnchecked(handler, att, result, exc);
            } else {
                Invoker.invokeIndirectly(this, handler, att, result, exc);
            }
        }
    }

堆外内存

我们都知道,在Java语言中通过new关键字分配的内存,都是在Java堆上分配的,并且这块内存会受到GC的管理。那么堆内存和堆外内存有什么区别呢?它们的优缺点是什么?

堆外内存是通过Unsafe类的allocateMemory(int size)方法分配内存的,allocateMemory则是通过Native层的malloc函数分配的。这块内存不是在Java堆上分配的。而引用这块内存的指针是属于堆内存的。
堆外内存不会随着垃圾回收而引起内存地址的变更。
堆外内存也会被自动回收,回收时机就是引用堆外内存的DirectBuffer对象被回收时,会触发堆外内存的回收。

分配

// java/nio/DirectByteBuffer.java

    DirectByteBuffer(int var1) {
        super(-1, 0, var1, var1);
        boolean var2 = VM.isDirectMemoryPageAligned();
        int var3 = Bits.pageSize();
        long var4 = Math.max(1L, (long)var1 + (long)(var2 ? var3 : 0));
        Bits.reserveMemory(var4, var1);
        long var6 = 0L;

        try {
        
            //调用Unsafe类的allocateMemory函数
            var6 = unsafe.allocateMemory(var4);
        } catch (OutOfMemoryError var9) {
            Bits.unreserveMemory(var4, var1);
            throw var9;
        }

        unsafe.setMemory(var6, var4, (byte)0);
        if (var2 && var6 % (long)var3 != 0L) {
            this.address = var6 + (long)var3 - (var6 & (long)(var3 - 1));
        } else {
            this.address = var6;
        }

        this.cleaner = Cleaner.create(this, new DirectByteBuffer.Deallocator(var6, var4, var1));
        this.att = null;
    }
// share/vm/prims/unsafe.cpp

UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
  UnsafeWrapper("Unsafe_AllocateMemory");
  size_t sz = (size_t)size;
  if (sz != (julong)size || size < 0) {
    THROW_0(vmSymbols::java_lang_IllegalArgumentException());
  }
  if (sz == 0) {
    return 0;
  }
  sz = round_to(sz, HeapWordSize);
  //系统分配内存的Api
  void* x = os::malloc(sz, mtInternal);
  if (x == NULL) {
    THROW_0(vmSymbols::java_lang_OutOfMemoryError());
  }
  //Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
  return addr_to_java(x);
UNSAFE_END

销毁

//java/nio/DirectByteBuffer.java

 DirectByteBuffer(int var1) {
    
    ...
    
    //在创建DirectByteBuffer实例时,生成一个Cleaner管理类,其中DirectByteBuffer.Deallocator负责回收堆外内存
    // var6是堆外内存地址
    // var1是申请内存大小
    // var4是经过内存对齐后的大小
    this.cleaner = Cleaner.create(this, new DirectByteBuffer.Deallocator(var6, var4, var1));
 }

上面Cleaner是利用了虚引用来判断DirectByteBuffer对象是否需要被回收,当DirectByteBuffer引用被回收时,其分配的堆外内存会通过DirectByteBuffer.Deallocato回收掉。

//java/nio/DirectByteBuffer.java

//Deallocator是一个线程类,被执行时会调用Unsafe类的freeMemory释放堆外内存
private static class Deallocator implements Runnable {
   private static Unsafe unsafe = Unsafe.getUnsafe();
   private long address;
   private long size;
   private int capacity;

   private Deallocator(long var1, long var3, int var5) {
       assert var1 != 0L;

       this.address = var1;
       this.size = var3;
       this.capacity = var5;
   }

   public void run() {
       if (this.address != 0L) {
           unsafe.freeMemory(this.address);
           this.address = 0L;
           Bits.unreserveMemory(this.size, this.capacity);
       }
   }
}
// sun/misc/Cleaner.java

//Cleaner类继承了虚引用对象,Java 虚引用允许对象被回收之前做一些清理工作。
//
public class Cleaner extends PhantomReference<Object> {
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
    private static Cleaner first = null;
    private Cleaner next = null;
    private Cleaner prev = null;
    private final Runnable thunk;

    public static Cleaner create(Object var0, Runnable var1) {
        return var1 == null ? null : add(new Cleaner(var0, var1));
    }
    
    private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
    }
    
    //添加到双向链表中
    private static synchronized Cleaner add(Cleaner var0) {
        if (first != null) {
            var0.next = first;
            first.prev = var0;
        }

        first = var0;
        return var0;
    }
    
    //clean()方法中会启动Deallocator线程,销毁内存
    public void clean() {
        //从双向链表中删除
        if (remove(this)) {
            try {
                //执行Deallocator类的run()方法
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }
}

当 DirectByteBuffer 被GC之前cleaner对象会被放入一个引用队列(ReferenceQueue),JVM会启动一个低优先级线程扫描这个队列,并且执行Cleaner的clean方法来做清理工作。

//java/lang/ref/Reference.java

public abstract class Reference<T> {
    
    
    static boolean tryHandlePending(boolean var0) {
        Reference var1;
        Cleaner var2;
        try {
            synchronized(lock) {
                if (pending == null) {
                    if (var0) {
                        lock.wait();
                    }

                    return var0;
                }

                var1 = pending;
                var2 = var1 instanceof Cleaner ? (Cleaner)var1 : null;
                pending = var1.discovered;
                var1.discovered = null;
            }
        } catch (OutOfMemoryError var6) {
            Thread.yield();
            return true;
        } catch (InterruptedException var7) {
            return true;
        }

        if (var2 != null) {
            //调用Cleaner的clean()方法
            var2.clean();
            return true;
        } else {
            ReferenceQueue var3 = var1.queue;
            if (var3 != ReferenceQueue.NULL) {
                var3.enqueue(var1);
            }

            return true;
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值