RandomAccessFile.setLength 在 Java 10 (Centos) 上慢得多

RandomAccessFile.setLength much slower on Java 10 (Centos)

如下代码

public class Main {
    public static void main(String[] args) throws IOException {
        File tmp = File.createTempFile("deleteme", "dat");
        tmp.deleteOnExit();
        RandomAccessFile raf = new RandomAccessFile(tmp, "rw");
        for (int t = 0; t < 10; t++) {
            long start = System.nanoTime();
            int count = 5000;
            for (int i = 1; i < count; i++)
                raf.setLength((i + t * count) * 4096);
            long time = System.nanoTime() - start;
            System.out.println("Average call time " + time / count / 1000 + " us.");
        }
    }
}

在 Java 8 上,这运行良好(文件在 tmpfs 上,所以你会认为它是微不足道的)

Average call time 1 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.
Average call time 0 us.

在 Java 10 上,随着文件变大,速度变慢

Average call time 311 us.
Average call time 856 us.
Average call time 1423 us.
Average call time 1975 us.
Average call time 2530 us.
Average call time 3045 us.
Average call time 3599 us.
Average call time 4034 us.
Average call time 4523 us.
Average call time 5129 us.

有没有办法诊断这类问题?

有没有在 Java 10 上有效工作的解决方案或替代方案?

注意:我们可以写到文件的末尾,但是这需要锁定它,我们希望避免这样做。

为了比较,On Windows 10,Java 8(不是 tmpfs)

Average call time 542 us.
Average call time 487 us.
Average call time 480 us.
Average call time 490 us.
Average call time 507 us.
Average call time 559 us.
Average call time 498 us.
Average call time 526 us.
Average call time 489 us.
Average call time 504 us.

Windows10,Java10.0.1

Average call time 586 us.
Average call time 508 us.
Average call time 615 us.
Average call time 599 us.
Average call time 580 us.
Average call time 577 us.
Average call time 557 us.
Average call time 572 us.
Average call time 578 us.
Average call time 554 us.

UPDATE 系统调用的选择似乎在 Java 8 和 10 之间发生了变化。这可以通过在命令行开头添加 strace -f 看到

在Java8中,在内循环中重复以下调用

[pid 49027] ftruncate(23, 53248)        = 0
[pid 49027] lseek(23, 0, SEEK_SET)      = 0
[pid 49027] lseek(23, 0, SEEK_CUR)      = 0

在Java10中,重复以下调用

[pid   444] fstat(8, {st_mode=S_IFREG|0664, st_size=126976, ...}) = 0
[pid   444] fallocate(8, 0, 0, 131072)  = 0
[pid   444] lseek(8, 0, SEEK_SET)       = 0
[pid   444] lseek(8, 0, SEEK_CUR)       = 0

特别是,fallocateftruncate 做的工作多得多,而且花费的时间似乎与文件的长度成正比,而不是添加到文件中的长度。

一种解决方法是;

这似乎是一个 hacky 解决方案。 Java10有更好的选择吗?

Is there a way to diagnose this kind of problem?

您可以使用内核感知 Java 分析器,例如 async-profiler

这是 JDK 8 的显示内容:

和 JDK 10:

配置文件证实了您的结论,即 RandomAccessFile.setLength 在 JDK 8 上使用 ftruncate 系统调用,但在 JDK 10 上使用更重的 fallocate

ftruncate 真的很快,因为它只更新文件元数据,而 fallocate 确实分配磁盘 space(或物理内存 tmpfs)。

此更改是为了修复 JDK-8168628: SIGBUS when extending file size to map it. But later it was realized that this is a bad idea, and the fix was reverted in JDK 11: JDK-8202261

Is there any solution or alternative which works efficiently on Java 10?

有一个内部 class sun.nio.ch.FileDispatcherImpl 有静态 truncate0 方法。它在后台使用 ftruncate 系统调用。您可以通过反射调用它,请记住这是一个不受支持的私有 API.

Class<?> c = Class.forName("sun.nio.ch.FileDispatcherImpl");
Method m = c.getDeclaredMethod("truncate0", FileDescriptor.class, long.class);
m.setAccessible(true);
m.invoke(null, raf.getFD(), length);