概述

  1. 在讨论代码性能的时候,内存的使用效率是一个绕不开的话题 - Flink/Netty
  2. 为了避免 JVM GC 不可预测的行为以及额外的性能开销,一般倾向于使用 JVM 之外的内存来存储和管理数据 - 堆外数据 - off-heap data
  3. 使用堆外存储最常用的办法,是使用 ByteBuffer 来分配直接存储空间 - direct buffer
    • JVM 会尽最大努力直接在 direct buffer 上执行 IO 操作,避免数据在本地JVM 之间的拷贝
  4. 频繁的内存拷贝性能主要障碍之一
    • 为了极致的性能,应用程序通常会尽量避免内存的拷贝
    • 理想的情况下,一份数据只需要一份内存空间 - 即零拷贝

ByteBuffer

使用 ByteBuffer 来分配直接存储空间

1
public static ByteBuffer allocateDirect(int capacity);
  1. ByteBuffer 所在的 Java 包是 java.nio,ByteBuffer 的设计初衷是用于非阻塞编程
  2. ByteBuffer 是异步编程非阻塞编程的核心类,几乎所有的 Java 异步模式或者非阻塞模式的代码,都要直接或者间接地使用 ByteBuffer 来管理数据
  3. 非阻塞异步编程模式的出现,起始于对阻塞式文件描述符(包括网络套接字读取性能的不满
    • 诞生于 2002 年的 ByteBuffer,其最初的设想也主要是用来解决当时文件描述符的读写性能

站在现在的视角重新审视该类的设计,会发现两个主要缺陷

  1. 缺陷 1 - 没有资源释放的接口
    • 一旦一个 ByteBuffer 实例化,它占用内存的释放,会完全依赖 JVM GC
    • 使用 direct buffer 的应用,往往需要把所有潜在的性能都挤压出来
    • 而依赖于 JVM GC 的资源回收方式,并不能满足像 Netty 这样的类库的理想需求
  2. 缺陷 2 - 存储空间尺寸的限制
    • ByteBuffer 的存储空间的大小,是使用 Java 的 int 来表示的,最多只有 2G - 一个无意带来的缺陷
    • 网络编程的环境下,这并不是一个问题,可是超过 2G 的文件,一定会越来越多
    • 2G 以上的文件,映射ByteBuffer 上的时候,就会出现文件过大的问题

合理的改进 - 重造轮子 - 外部内存接口

外部内存接口

  1. 外部内存接口沿袭了 ByteBuffer 的设计思路,但使用了全新的接口布局
  2. 分配一段外部内存,并且存放 4 个字母 A
1
2
3
4
5
6
try (ResourceScope scope = ResourceScope.newConfinedScope()) {
MemorySegment segment = MemorySegment.allocateNative(4, scope);
for (int i = 0; i < 4; i++) {
MemoryAccess.setByteAtOffset(segment, i, (byte)'A');
}
}
  1. ResourceScope 定义了内存资源生命周期管理机制,实现了 AutoCloseable 接口,可以使用 try-with-resource及时释放掉它管理的内存 - 缺陷 1
  2. MemorySegment 用于定义和模拟一段连续的内存区域,而 MemoryAccess 用于定义对 MemorySegment 执行读写操作
    • 在外部内存接口的设计里,把对象表达对象操作,拆分成两个类
    • 这两类的寻址数据类型,使用的是 long - 缺陷 2