Netty 针对 Java NIO Selector 优化:SelectedSelectionKeySet
SelectedSelectionKeySet
这是一个 Netty 针对 Java NIO Selector 的一个非常巧妙的性能优化。理解它的作用需要先了解原生 NIO Selector 的一个痛点。
在标准的 Java NIO 编程中,事件循环的流程大致是:
- 调用
selector.select()方法,阻塞等待 I/O 事件。 - 当
select()返回时,表示有事件发生。 - 调用
selector.selectedKeys()方法,获取一个Set<SelectionKey>集合。这个集合包含了所有就绪的SelectionKey。 - 遍历这个
Set,处理每一个SelectionKey对应的事件。 - 处理完一个
SelectionKey后,必须手动调用iterator.remove()或set.remove(key)将其从集合中移除,否则下次调用select()时,这个 key 仍然会出现在集合中,导致重复处理。
这里的性能瓶颈主要在于两点:
selector.selectedKeys()返回的Set:在 HotSpot JVM 的sun.nio.ch.SelectorImpl实现中,这个Set通常是一个HashSet。每次遍历和remove()操作都会有相应的哈希计算和数据结构维护的开销。- GC 压力:频繁地创建迭代器(
iterator())和remove()操作会产生不少的垃圾对象,在高并发场景下会给 GC 带来压力。
Netty 的目标是榨干服务器的每一分性能。为了解决上述瓶颈,Netty 设计了 SelectedSelectionKeySet,用它来替换掉 Selector 内部默认的 HashSet。
我们来逐个分析 SelectedSelectionKeySet.java 的代码,看看它是如何做到优化的。
类定义与核心数据结构
final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {
SelectionKey[] keys;
int size;
SelectedSelectionKeySet() {
keys = new SelectionKey[1024];
}
//...
extends AbstractSet<SelectionKey>: 它继承自AbstractSet,表明自己是一个Set。这是为了符合 Java Selector API 的要求,因为 Selector 内部的selectedKeys字段就是一个Set类型。SelectionKey[] keys;和int size;: 这是整个优化的核心!它没有使用HashSet或任何复杂的集合类,而是直接使用一个简单的数组keys和一个整型size来存储就绪的SelectionKey。这种数据结构极其简单高效。
add() 方法:高效添加
// ... existing code ...
@Override
public boolean add(SelectionKey o) {
if (o == null) {
return false;
}
if (size == keys.length) {
increaseCapacity();
}
keys[size++] = o;
return true;
}
// ... existing code ...
private void increaseCapacity() {
SelectionKey[] newKeys = new SelectionKey[keys.length << 1];
System.arraycopy(keys, 0, newKeys, 0, size);
keys = newKeys;
}
// ... existing code ...
当 selector.select() 发现一个 Channel 就绪时,它会调用这个 add 方法将对应的 SelectionKey 加入集合。
- 操作: 这里的
add操作非常快,它只是简单地将SelectionKey放到数组的末尾,然后将size加一。这是一个O(1)的操作,没有任何哈希计算或复杂的树/链表操作。 - 扩容:
increaseCapacity()实现了动态扩容,当数组满了之后,会创建一个两倍大小的新数组,并将旧数据拷贝过去。这是一个常规操作,但由于初始容量较大(1024),在绝大多数情况下不会频繁触发。
remove() 和 contains() 方法
// ... existing code ...
@Override
public boolean remove(Object o) {
return false;
}
@Override
public boolean contains(Object o) {
// ... 遍历数组查找 ...
return false;
}
// ... existing code ...
remove(Object o): 这个方法直接返回false,它不支持单个元素的移除。这是 Netty 优化的关键之一。Netty 并不需要像原生 NIO 那样一个个地移除 Key,它有更高明的办法。contains(Object o): 实现了简单的线性查找,虽然是O(n),但在 Netty 的使用场景中,这个方法很少被调用。
reset() 方法:颠覆性的“清空”操作
// ... existing code ...
void reset() {
reset(0);
}
void reset(int start) {
Arrays.fill(keys, start, size, null);
size = 0;
}
// ... existing code ...
这是 SelectedSelectionKeySet 的精髓所在。
Netty 在处理完一轮就绪的 SelectionKey 之后,并不会去调用 iterator.remove()。相反,它会直接调用 reset() 方法。这个方法只是简单地将数组中从 0 到 size 的元素置为 null,然后把 size 设为 0。
这个操作代替了原生 NIO 中成百上千次的 remove() 调用,将多次操作合并为一次高效的批量操作,极大地减少了方法调用和 CPU 开销,并且完全避免了 remove() 操作产生的垃圾对象。
如何将 SelectedSelectionKeySet "注入" Selector?
既然 JDK 的 Selector 内部实现是私有的,Netty 是如何用自己的 SelectedSelectionKeySet 替换掉默认的 HashSet 的呢?
答案是Java 反射。
在 NioIoHandler的初始化过程中,有这样一段关键代码(逻辑类似):
// ... existing code ...
final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
// 通过反射获取 SelectorImpl 内部的私有字段
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
// ...
// 暴力破解访问权限
selectedKeysField.setAccessible(true);
publicSelectedKeysField.setAccessible(true);
// 将我们自己的 selectedKeySet 实例设置进去
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
// ...
} catch (Exception e) {
return e;
}
return null;
}
});
// ... existing code ...
通过 AccessController 和反射,Netty 强行打开了 SelectorImpl 的内部实现,将 selectedKeys 和 publicSelectedKeys 这两个字段(它们在不同 JDK 版本中可能指向同一个或不同的 Set 实例)都替换成了 Netty 自己创建的 SelectedSelectionKeySet 实例。
这样一来,当 selector.select() 将就绪的 key 放入 selectedKeys 集合时,实际上调用的是 SelectedSelectionKeySet.add() 方法。当 Netty 处理完事件后,就可以调用 selectedKeySet.reset() 来高效清空了。
总结
SelectedSelectionKeySet 是 Netty NIO 性能优化的一个典范。它体现了 Netty 团队对 JVM 和 JDK 内部实现的深刻理解。
- 目的: 替换 JDK NIO Selector 内部低效的
HashSet,减少迭代和移除操作的开销,降低 GC 压力。 - 核心思想: 使用简单的数组代替
HashSet,将add操作优化为O(1)的数组末尾添加。 - 颠覆性创新: 用一次性的
reset()操作代替了成百上千次的remove()调用,将清空集合的成本降到最低。 - 实现方式: 通过 Java 反射,在运行时将自定义的
Set实例 "注入" 到Selector内部,实现了对 JDK 底层行为的"偷梁换柱"。
这个小小的类,完美地展示了 Netty 为了追求极致性能所做的努力。
更多推荐



所有评论(0)