SelectedSelectionKeySet

这是一个 Netty 针对 Java NIO Selector 的一个非常巧妙的性能优化。理解它的作用需要先了解原生 NIO Selector 的一个痛点。

在标准的 Java NIO 编程中,事件循环的流程大致是:

  1. 调用 selector.select() 方法,阻塞等待 I/O 事件。
  2. 当 select() 返回时,表示有事件发生。
  3. 调用 selector.selectedKeys() 方法,获取一个 Set<SelectionKey> 集合。这个集合包含了所有就绪的 SelectionKey
  4. 遍历这个 Set,处理每一个 SelectionKey 对应的事件。
  5. 处理完一个 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 为了追求极致性能所做的努力。

更多推荐