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;
}
}
}