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

9

我想比较直接字节缓冲区(java.nio.ByteBuffer,非堆)和堆缓冲区(通过数组实现)在读写方面的性能。我的理解是,ByteBuffer 是非堆的,因此至少有两个优点。首先,它不会被视为垃圾回收的对象;其次(我希望我理解得正确),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) 模式 数量 得分 误差 单位

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

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


嗯,很可能缺少中间/临时缓冲区会给您带来性能损失。我猜他们没有把它放在那里让所有东西都变慢。这只是我的个人意见... - Curiosa Globunznik
1
直接缓冲区在一切都保持在“本地世界”时效果最佳。例如,在两个通道之间传输字节。如果将数据拉入“Java世界”,则会失去许多好处。可能有帮助的链接:何时使用Array、Buffer或Direct BufferByteBuffer.allocate() vs. ByteBuffer.allocateDirect() - Slaw
为什么你的基准测试会显示直接缓冲区更快,当你没有进行它更快的操作,例如从/写入文件或套接字中读取? - Andreas
1
@curiosa javadoc中提到:“对于给定的直接字节缓冲区,Java虚拟机将尽最大努力直接执行本地I/O操作。也就是说,在每次调用其中一个底层操作系统的本地I/O操作之前(或之后),它将尝试避免将缓冲区的内容复制到(或从)中间缓冲区。”--- 它是在谈论读写文件或套接字。对于纯内存访问,它不需要调用操作系统。 - Andreas
@Abidi "它不会被考虑为GC" 是错误的。您为什么这样认为?如果是真的,那么内存怎么会被释放呢?您无法控制这个过程。即使内存在堆之外,也并不意味着垃圾收集器不会实际释放内存。 - Andreas
我推荐 https://codereview.stackexchange.com/。 - FailingCoder
2个回答

9

它不会被视为GC

当然会被GC考虑。

它是垃圾收集器(Garbage Collector)决定缓冲区不再使用并释放内存的。

我不应该期望我的基准测试显示离堆缓冲区比堆缓冲区更快吗?

离堆并不会使缓冲区在内存访问方面更快。

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

javadoc所述:

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


1
@Abidi 不要使用 Unsafe,它是未记录的!--- 但是如果你必须使用它,为什么你认为它被称为“不安全”?为什么它有一个 freeMemory() 方法?因为由 allocateMemory() 返回的内存不受 GC 控制。--- 引用“使用 allocateMemory() 分配的内存不位于堆中,不受 GC 管理,因此请使用 Unsafe.freeMemory() 对其进行管理。它也不执行任何边界检查,因此任何非法访问可能会导致 JVM 崩溃。” - Andreas
3
仅仅因为某件事情在互联网上出现,并不意味着它是真实的。 - Andreas
4
DirectByteBuffer所分配的内存不属于堆。这意味着GC不会扫描或将其移动到年轻/老一代中,这正是使它更有效率的部分(需要注意的是,像其他对象一样,DirectByteBuffer对象本身将被扫描和移动)。在其"finalization"的一部分中,DirectByteBuffer调用freeMemory来释放那块内存。 - Sotirios Delimanolis
@SotiriosDelimanolis Andreas提到,通过allocateDirect()分配的缓冲区内容将被垃圾回收,您说它们不会被GC移动。您的意思是它们将被垃圾回收,但不像通过Xmx分配的堆中的对象那样被收集?我理解DirectByteBuffer对象,因为它是通过new关键字创建的。 - Abidi
以上评论中有一点小错误需要纠正:直接创建的ByteBuffer对象是在垃圾收集堆以外分配存储空间的(不是在其他特殊的垃圾收集堆里),因此它本身并不会被垃圾回收。相反,一个清理任务会将幻象引用与ByteBuffer相关联,并将其与一个可运行的函数一起释放非堆内存数据,当ByteBuffer变得不可达时,它将释放该数据。这里没有任何魔法,也没有第二个垃圾收集堆,只是普通的 Java 结合 sun.misc.Unsafe 实现了非堆内存管理。ByteBuffer实例本身位于普通堆上。 - Score_Under
显示剩余10条评论

4
在JDK9中,HEAP和DIRECT缓冲区都使用sun.misc.Unsafe进行原始内存访问。除了HEAP缓冲区分配更快之外,两者之间没有任何性能差异。以前向HEAP缓冲区写入多字节基元存在很大代价,但现在已经没有了。
从IO读取/写入时,HEAP缓冲区较慢,因为所有数据必须先复制到ThreadLocal DIRECT缓冲区,然后再复制到您的HEAP缓冲区。
这两个对象都可以进行垃圾回收,不同之处在于DirectByteBuffer占用JVM HEAP内存较少,而HeapByteBuffer将所有内存存储在JVM HEAP上。DirectByteBuffer的垃圾回收过程比HeapByteBuffer更加复杂。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接