1. 现象简述
在项目性能测试过程中发现,同样的代码,连接同样数量(10万)的设备(设备和代码之间通过NIO有大量的数据交互),在Linux下CPU利用率只有20%~30%,而在windows下却一直高于80%。
2. 原因初步排查
通过jconsole分别监控运行在linux和windows上的程序,在【线程】选项卡中发现,windows下启动了大量未命名线程,堆栈信息都类似于下图:
通过Java自带的Jstack将Java程序对应进程的内存信息导出,命令如下:
jstack -l 31372 > c:/31372.stack
说明: 其中31372为该进程的PID。
然后搜索有相同堆栈信息的线程,发现同样的线程启动了97个,通过windows的监控工具Process Explorer(该工具使用可参考这里)可以发现,这些线程每个大约占用0.7%~0.9%的CPU资源,那么这97个线程约占用了69.7%的CPU资源,而Linux并未启动这些线程,这也就可以从宏观上解释windows下CPU利用率比Linux高出60%多的现象了。
3. NIO深度分析
从openJDK下载WindowsSelectorImpl类的源码,可以发现:
final class WindowsSelectorImpl extends SelectorImpl {
...
}
该类继承了SelectorImpl类,于是找到SelectorImpl类的源码:
abstract class SelectorImpl extends AbstractSelector {
...
}
可以发现,该类继承了AbstractSelector抽象类。
在Eclipse中,可以看到这个类的继承关系:
即这些类最终的实现类为Selector,在代码中找到使用Selector类的地方:
int n = selector.select(25);
Selector类的select()方法在SelectorImpl类中实现,具体如下:
public int select(long timeout) throws IOException {
if (timeout < 0)
throw new IllegalArgumentException("Negative timeout");
return lockAndDoSelect((timeout == 0) ? -1 : timeout);
}
select()方法调用了lockAndDoSelect()方法,源码如下:
private int lockAndDoSelect(long timeout) throws IOException {
synchronized (this) {
if (!isOpen())
throw new ClosedSelectorException();
synchronized (publicKeys) {
synchronized (publicSelectedKeys) {
return doSelect(timeout);
}
}
}
}
lockAndDoSelect()方法调用了doSelect()方法,而doSelect()方法在SelectorImpl类中是抽象方法。
protected abstract int doSelect(long timeout) throws IOException;
其具体实现与操作系统相关,windwos系统中该方法在WindowsSelectorImpl类中实现,Linux系统中该类在EPollSelectorImpl类中实现。
3.1 Windows下NIO的实现分析
查看WindowsSelectorImpl类的源码,找到doSelect()方法,源码如下:
protected int doSelect(long timeout) throws IOException {
if (channelArray == null)
throw new ClosedSelectorException();
this.timeout = timeout; // set selector timeout
processDeregisterQueue();
if (interruptTriggered) {
resetWakeupSocket();