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

2022-09-02 10:29:01

下面的代码

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上有效工作?

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

为了进行比较,在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.

视窗 10, Java 10.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.

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

在 Java 8 中,以下调用在内部循环中重复

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

在 Java 10 中,重复以下调用

[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

一个解决方法是;

  • 使用对文件描述符的反射fd
  • 使用 JNA 或 FFI 调用 ftruncate。

这似乎是一个黑客解决方案。Java 10中有更好的选择吗?


答案 1

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

您可以使用内核感知 Java 探查器,如异步探查器

以下是它在JDK 8中显示的内容:

JDK 8 profile for RandomAccessFile.setLength

对于 JDK 10:

JDK 10 profile for RandomAccessFile.setLength

配置文件证实了您的结论,即在JDK 8上使用syscall,但在JDK 10上使用系统调用要重得多。RandomAccessFile.setLengthftruncatefallocate

ftruncate非常快,因为它只更新文件元数据,而确实分配磁盘空间(或在发生的情况下分配物理内存)。fallocatetmpfs

进行此更改是为了在扩展文件大小以映射它时修复 JDK-8168628:SIGBUS。但后来意识到这是一个坏主意,并且在JDK 11中恢复了修复:JDK-8202261

是否有任何解决方案或替代方案可以在Java 10上有效工作?

有一个具有静态方法的内部类。它在引擎盖下使用系统调用。您可以通过Reflectlection调用它,请记住这是一个私有的不受支持的API。sun.nio.ch.FileDispatcherImpltruncate0ftruncate

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);

答案 2

推荐