问题描述
使用FUZZ测试的方法,启动N线程,不断执行如下步骤:创建连接->发送几个消息->释放连接。持续N小时后停止,观察到内存存在增长不回收现象,执行heap dump寻找内存泄露的地方,反复比较查找后可定位在SelectedSelectionKeySet:持有大量select key。
排除以下几种情况:
- GC时机未到:手工触发GC之后,内存仍然持有不释放;
- 抓取heapdump的时机是刚好处理select keys的过程中,所以导致仍然有很多待处理的情况:排除这种情况,因为select key中间穿插了一些null, 而处理代码中,遇到null会认为结束,所以这意味着null之后的N多select key以后也永远无法处理。
final SelectionKey k = selectedKeys[i];
if (k == null) {
break;
}
问题原因
NIO的基本处理步骤包括两步:
- selector寻出所有就绪的通道事件(例如读、写等)
- 按顺序处理所有就绪的通道事件
@Override
protected void run() {
for (;;) {
……
select(oldWakenUp); //select出所有就绪事件
……
cancelledKeys = 0;
needsToSelectAgain = false;
……
processSelectedKeys(); //处理
}
在处理所有就绪事件时,有一个优化:select again:
在“选出”和处理“完”所有selectedKeys之间的时间段内,如果cancel的keys超过了256(测试中不断释放连接触发),那么直接放弃之后仍需要处理的select keys, 直接重新做select和具体的处理。
void cancel(SelectionKey key) {
key.cancel();
cancelledKeys ++;
if (cancelledKeys >= CLEANUP_INTERVAL) { //cancel keys > 256
cancelledKeys = 0;
needsToSelectAgain = true; //enable select again.
}
}
private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
for (int i = 0;; i ++) {
final SelectionKey k = selectedKeys[i];
if (k == null) {
break;
}
selectedKeys[i] = null;
final Object a = k.attachment();
……
processSelectedKey(k, (AbstractNioChannel) a);
if (needsToSelectAgain) {
for (;;) {
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
i++;
}
selectAgain();
selectedKeys = this.selectedKeys.flip();
i = -1;
}
}
}
此时面临的问题是:
Select Again之前那些尚未处理的select key如何处理,因为后面有select again过程,所以这部分未处理的select key无需保留,假设保留,存在的后果是:
当select again时,SelectedSelectionKeySet(包含keysA和keysB)此时已经使用另外一个keys,假设之前使用的是keysA,则使用keysB.使用keysB添加所有的select key并且处理完之后,下次添加则使用keysA. 此时如果添加的新的key的size<上轮keysA之前已消费的容量,则keysA中存在3段内容:新的key, null(或许N个),老的key。此时再重新消费keysA时,老的key仍然存在,因为遇到null就跳出了。
@Override
public boolean add(SelectionKey o) {
if (o == null) {
return false;
}
if (isA) {
int size = keysASize;
keysA[size ++] = o;
keysASize = size;
if (size == keysA.length) {
doubleCapacityA();
}
} else {
int size = keysBSize;
keysB[size ++] = o;
keysBSize = size;
if (size == keysB.length) {
doubleCapacityB();
}
}
return true;
}
因此必须要将select again之前未处理的selected key全部移除。而原有的代码因为一个简单的错误,并没有办法删除所有的keys.
if (k == null) {
break;
}
selectedKeys[i] = null; //消费一个,置Null一个
……
if (needsToSelectAgain) {
for (;;) {
if (selectedKeys[i] == null) { //此时肯定是null,所以直接跳出,未删除后面的keys.
break;
}
selectedKeys[i] = null;
i++;
}
解决方案
略微修改即可:
if (needsToSelectAgain) {
for (;;) {
i++; //把i++移动到此
if (selectedKeys[i] == null)
break;
}
selectedKeys[i] = null;
}
造成的影响是: 内存有一部分持有不释放,但是在正常的应用使用中,一般不会出现,因为要求断连的速度特别大,且超过了业务本身的处理速度。超过的越多,不释放的越多,但是正常情况下不会超过,即使超过也不会超过太多。
