直接 java.nio.ByteBuffer 与 Java 数组性能测试

Posted

技术标签:

【中文标题】直接 java.nio.ByteBuffer 与 Java 数组性能测试【英文标题】:Direct java.nio.ByteBuffer vs Java Array Performance Test 【发布时间】:2019-11-23 14:38:19 【问题描述】:

我想比较读写的直接字节缓冲区(java.nio.ByteBuffer,堆外)和堆缓冲区(通过数组实现)的性能。我的理解是,与堆缓冲区相比,堆外的 ByteBuffer 至少有两个好处。首先,它不会被考虑用于 GC,其次(我希望我做对了)JVM 在读取和写入时不会使用中间/临时缓冲区。这些优点可能使堆外缓冲区比堆缓冲区更快。如果这是正确的,我不应该期望我的基准测试显示相同吗?它总是比非堆缓冲区更快地显示堆缓冲区。

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@Fork(value = 2, jvmArgs = "-Xms2G", "-Xmx4G")
@Warmup(iterations = 3)
@Measurement(iterations = 10)
public class BasicTest 

    @Param("100000")
    private int N;

    final int bufferSize = 10000;

    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8 * bufferSize);
    long buffer[] = new long[bufferSize];


    public static void main(String arep[]) throws  Exception 

        Options opt = new OptionsBuilder()
                .include(BasicTest.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();

    


    @Benchmark
    public void offHeapBuffer(Blackhole blackhole) 

        IntStream.range(0, bufferSize).forEach(index -> 
            byteBuffer.putLong(index, 500 * index);
            blackhole.consume(byteBuffer.get(index));
        );

    

    @Benchmark
    public void heapBuffer(Blackhole blackhole) 

        IntStream.range(0, bufferSize).forEach(index -> 
            buffer[index] = 500 * index;
            blackhole.consume(buffer[index]);
        );

    

运行完成。总时间:00:00:37

基准 (N) 模式 Cnt 得分误差单位

BasicTest.heapBuffer 100000 平均 10 0.039 ± 0.003 毫秒/操作

BasicTest.offHeapBuffer 100000 平均 10 0.050 ± 0.007 毫秒/操作

【问题讨论】:

嗯,很可能是缺少中间/临时缓冲区会给您带来性能损失。我猜他们并没有把它放在那里让一切变慢。只是我个人的 2 美分... 当一切都停留在“原生世界”时,直接缓冲区的效果最好。例如,在两个通道之间传输字节。如果您将数据拉入“Java 世界”,您将失去很多好处。可能有帮助:When to use Array, Buffer or direct Buffer; ByteBuffer.allocate() vs. ByteBuffer.allocateDirect(). 为什么您的基准测试会显示 direct 缓冲区更快,而当您不执行 更快的操作时,例如读取/写入文件或套接字? @curiosa javadoc 说:“给定一个 直接 字节缓冲区,Java 虚拟机将尽最大努力执行 native I/ O 直接对其进行操作。也就是说,它会尝试在每次调用 >底层操作系统的本机 I/O 操作。” --- 它正在谈论读/写文件或套接字。它不需要调用操作系统来进行普通内存访问。 @Abidi “GC 不会考虑它” 不正确。你为什么相信?如果它是真的,那么记忆将如何被释放?你没有办法控制它。仅仅因为内存在堆外并不意味着内存的实际释放不是由垃圾收集器执行的。 【参考方案1】:

GC不会考虑

当然会考虑GC。

是垃圾收集器确定缓冲区不再使用,然后释放内存。

我是否应该期望我的基准测试显示 [that] 堆外缓冲区 [is] 比堆缓冲区快?

堆外不会使缓冲区更快地访问内存。

当 Java 与操作系统交换缓冲区中的字节时,direct 缓冲区会更快。由于您的代码不进行 I/O,因此使用直接缓冲区没有性能优势。

正如javadoc 所说:

给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接在其上执行本机 I/O 操作。也就是说,在每次调用底层操作系统的本机 I/O 之一之前(或之后),它将尝试避免将缓冲区的内容复制到(或从)中间缓冲区(或从中间缓冲区复制)操作

【讨论】:

您提供的 javadoc 链接还说“直接缓冲区的内容可能位于正常的垃圾收集堆之外,因此它们对应用程序内存占用的影响可能并不明显”。你不会推断我上面创建的缓冲区的内容不会被垃圾收集吗? @Abidi 不,我不会。它说“驻留在正常垃圾收集堆之外”,即正常的内存池。这意味着它被认为是 special (选择的替代词) 垃圾收集堆的一部分。 --- 该声明的重点是“对内存占用的影响”部分,例如内存超出-Xmx设置的限制,它可能不会改变runtime.maxMemory()runtime.totalMemory()返回的值。 @Abidi 忘记Unsafe。不要使用它。它是无证的! --- 但是如果非要使用的话,为什么会觉得它叫“不安全”呢?为什么你认为它有一个freeMemory() 方法?因为allocateMemory()返回的内存不受GC控制。 --- Quote: "[使用allocateMemory()]分配的内存不在堆中并且不在GC管理下,所以使用Unsafe.freeMemory()来处理它。它也不执行任何边界检查,因此任何非法访问都可能导致 JVM 崩溃。” @Abidi 仅仅因为它在网络上并不意味着它是真的。 @abidi DirectByteBuffer 后面分配的内存不是堆的一部分。这意味着 GC 不会扫描它或将它移动到年轻/年老代。这是使它更高效的部分原因。 (请注意,DirectByteBuffer 对象本身将像任何其他对象一样被扫描和移动。)作为其“终结”的一部分,DirectByteBuffer 调用 freeMemory 以释放该内存。【参考方案2】:

在 JDK9 中,HEAP 和 DIRECT 缓冲区都使用 sun.misc.Unsafe 进行原始内存访问。除了 HEAP 缓冲区分配更快之外,两者之间的性能差异为零。过去,将多字节原语写入 HEAP 缓冲区会带来很大的损失,但现在已经不存在了。

从 IO 读取/写入时,HEAP 缓冲区较慢,因为必须先将所有数据复制到 ThreadLocal DIRECT 缓冲区,然后再复制到 HEAP 缓冲区。

这两个对象都可以进行垃圾回收,不同之处在于 DirectByteBuffer 使用较少的 JVM HEAP 内存,而 HeapByteBuffer 将所有内存存储在 JVM HEAP 上。 DirectByteBuffer 的垃圾回收过程比 HeapByteBuffer 更复杂。

【讨论】:

以上是关于直接 java.nio.ByteBuffer 与 Java 数组性能测试的主要内容,如果未能解决你的问题,请参考以下文章

是否存在允许动态增长和扩展 java.nio.ByteBuffer 的 ByteBuffer 实现?

java.nio.ByteBuffer.array(ByteBuffer.java:959) 处的 java.lang.UnsupportedOperationException

java.nio.ByteBuffer.slice() 线程行为?

Java NIO 之 ByteBuffer 测试用例

java.nio.ByteBuffer 以及flip,clear及rewind区别

直接字节缓冲区