ByteBuffer.allocate() 和 ByteBuffer.allocateDirect() 的区别

165
< p > allocate() 还是 allocateDirect(),这是个问题。

多年来,我一直认为由于 DirectByteBuffer 是在操作系统层面上的直接内存映射,使用 get/put 调用速度比 HeapByteBuffer 更快。直到现在我才对这种情况感兴趣并想要了解确切的细节。我想知道这两种类型的 ByteBuffer 哪种更快,以及在什么条件下。


为了给出具体的答案,你需要明确地说明你正在使用它们做什么。如果一个总是比另一个快,为什么会有两种变体。也许你可以详细说明为什么现在“非常有兴趣找出确切的细节”。顺便问一下:你读过代码了吗,特别是DirectByteBuffer的代码? - Peter Lawrey
它们将用于读取和写入配置为非阻塞的SocketChannel。因此,关于@bmargulies所说的,DirectByteBuffer对于通道的性能表现更快。 - user238033
@Gnarly 至少目前我的回答版本表明通道有望受益。 - bmargulies
4个回答

162
在Ron Hitches的优秀著作Java NIO中,似乎提供了一个我认为可能是对你问题的很好答案:

操作系统对内存区域执行I/O操作。就操作系统而言,这些内存区域是连续的字节序列。因此,只有字节缓冲区才有资格参与I/O操作。同时请注意,操作系统将直接访问进程的地址空间(在本例中为JVM进程)以传输数据。这意味着作为I/O操作目标的内存区域必须是连续的字节序列。在JVM中,字节数组可能不会被连续地存储在内存中,或者垃圾回收器可以随时移动它。在Java中,数组是对象,并且对象内部存储数据的方式可能因不同的JVM实现而异。

因此,引入了直接缓冲区的概念。直接缓冲区旨在与通道和本地I / O例程交互。它们尽最大努力将字节元素存储在通道可以用于直接或原始访问的内存区域中,通过使用本机代码告诉操作系统直接排空或填充内存区域。

直接字节缓冲区通常是进行I/O操作的最佳选择。按照设计,它们支持JVM可用的最有效的I/O机制。非直接字节缓冲区可以传递给通道,但这样做可能会导致性能损失。通常不可能将非直接缓冲区作为本地I / O操作的目标。如果您将非直接ByteBuffer对象传递给通道进行写入,则通道可能隐式执行以下操作:

  1. 创建一个临时的直接ByteBuffer对象。
  2. 将非直接缓冲区的内容复制到临时缓冲区。
  3. 使用临时缓冲区执行低级 I/O 操作。
  4. 临时缓冲区对象超出范围并最终被垃圾回收。

这可能导致在每次 I/O 操作时进行缓冲拷贝和对象变化,而这正是我们想避免的。然而,根据实现方式,情况可能不会这么糟糕。运行时很可能会缓存和重用直接缓冲区或执行其他聪明的技巧来提高吞吐量。如果您只是为一次性使用创建缓冲区,则差异并不显著。另一方面,如果您将在高性能场景中反复使用缓冲区,则最好分配直接缓冲区并重用它们。

直接缓冲区对于 I/O 是最优的,但它们可能比非直接字节缓冲区更昂贵。直接缓冲区使用的内存是通过调用本机、操作系统特定的代码来分配的,绕过了标准 JVM 堆。建立和撤销直接缓冲区可能会比常驻堆的缓冲区显著更昂贵,这取决于主机操作系统和 JVM 实现。直接缓冲区的内存存储区域不受垃圾回收的影响,因为它们在标准 JVM 堆之外。

使用直接与非直接缓冲区的性能权衡可以因 JVM、操作系统和代码设计而大不相同。通过将内存分配到堆之外,您可能会使应用程序受到 JVM 不知道的其他力量的影响。当引入其他运动部件时,请确保您正在实现所需的效果。我建议遵循旧软件规范:先让它工作,再让它快。不要过于担心优化前期;首先要专注于正确性。JVM 实现可能能够执行缓冲区高速缓存或其他优化,从而为您提供最佳的性能。

你可以轻松获得所需的性能,而无需花费过多不必要的努力。


13
我不喜欢那个引语,因为它包含太多猜测。此外,当非直接ByteBuffer进行IO时,JVM肯定不需要分配一个直接的ByteBuffer:只需在堆上malloc一系列字节,进行IO,从字节复制到ByteBuffer并释放字节即可。这些区域甚至可以被缓存。但是为此分配Java对象是完全不必要的。真正的答案只能通过测量获得。上次我进行测量时没有显着差异。我必须重新进行测试才能得出所有具体细节。 - Robert Klemme
4
描述NIO(以及本机操作)的书是否确实可靠并不确定。毕竟,不同的JVM和操作系统会以不同的方式管理事物,因此作者无法保证特定行为。 - Martin Tuskevicius
@RobertKlemme,+1,我们都讨厌猜测。然而,由于主要操作系统太多了,可能无法为所有主要操作系统测量性能。另一篇帖子尝试过,但我们可以看到很多问题,从“结果根据操作系统波动很大”开始。此外,如果有一个黑羊在每个I/O上执行可怕的缓冲区复制之类的操作,那么因为这只羊,我们可能被迫阻止编写我们本来会使用的代码,仅仅是为了避免这些最坏情况。 - Pacerier
@RobertKlemme 我同意。这里有太多的猜测了。例如,JVM极不可能稀疏地分配字节数组。 - user207421
@Edwin Dalorzo:在现实世界中,为什么我们需要这样的字节缓冲区?它们是作为一种黑客方式发明出来的,以便在进程之间共享内存吗?例如,JVM运行在一个进程上,而负责传输数据的可能是另一个运行在网络或数据链路层上的进程 - 这些字节缓冲区是否被分配用于在这些进程之间共享内存?如果我错了,请纠正我。 - Tom Taylor

29

没有理由认为直接缓冲区在JVM内部的访问速度会更快。它们的优势在于当您将它们传递给本地代码时--例如,各种通道背后的代码。


确实。比如,在Scala/Java中需要进行IO操作并调用嵌入式Python/本地库来处理大量内存数据进行算法处理,或者直接将数据提供给Tensorflow中的GPU。 - SemanticBeeng

25

由于DirectByteBuffers是直接在操作系统级别上进行的内存映射

这不是正确的说法。它们只是普通的应用程序进程内存,但不会在Java垃圾收集期间被重新定位,这在JNI层面内部简化了很多事情。您所描述的适用于MappedByteBuffer。

使用get/put调用性能更快

这个结论并不是从前提得出的;前提是错误的;而且结论也是错误的。一旦进入JNI层面,它们就会更快,如果您正在从同一个DirectByteBuffer读取和写入数据,则它们会更快,因为数据根本不需要跨越JNI边界。


9
这是一个很好且重要的观点:在IO路径中,你必须在某个时刻穿越Java-JNI边界。直接和非直接字节缓冲区只是移动了这个边界:使用直接缓冲区,从Java空间进行的所有put操作都必须穿过边界;而对于非直接缓冲区,所有IO操作都必须穿过边界。哪种更快取决于应用程序。 - Robert Klemme
1
@RobertKlemme 您的摘要不正确。对于所有缓冲区,任何进出Java的数据都必须跨越JNI边界。直接缓冲区的重点在于,如果您只是将数据从一个通道复制到另一个通道,例如上传文件,则根本不需要将其传递到Java中,这样速度会快得多。 - user207421
我的总结哪里不正确?而且,一开始什么“总结”?我明确在谈论“来自Java领域的put操作”。当然,如果您只是在通道之间复制数据(即从未涉及到Java领域中的数据),那就是另一回事了。 - Robert Klemme
@RobertKlemme 您的说法“使用直接缓冲区,所有来自Java的put操作都必须跨越”是不正确的。gets和puts都必须跨越。 - user207421
1
EJP,你似乎仍然没有理解RobertKlemme在选择在一个短语中使用“put操作”一词并在句子的对比短语中使用“IO操作”一词时所要表达的区别。在后一短语中,他的意图是指缓冲区和某种由操作系统提供的设备之间的操作。 - naki

21

谢谢。我会接受你的答案,但我正在寻找有关性能差异的更具体细节。 - user238033

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