有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

随机存取文件。在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.");
        }
    }
}

在Java8上,这运行得很好(该文件位于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.

在Java10上,随着文件变大,速度会变慢

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.

Windows 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.

更新:系统调用的选择似乎在Java8和Java10之间发生了变化。这可以通过将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做的工作多得多,而且所花费的时间似乎与文件的长度成正比,而不是与添加到文件中的长度成正比

一个解决办法是:

  • fd文件描述符使用反射
  • 使用JNA或FFI呼叫ftruncate

这似乎是一个很难解决的问题。Java 10中有更好的替代方案吗


共 (1) 个答案

  1. # 1 楼答案

    Is there a way to diagnose this kind of problem?

    可以使用async-profiler之类的支持内核的Java探查器

    以下是JDK 8的演示:

    JDK 8 profile for RandomAccessFile.setLength

    对于JDK 10:

    JDK 10 profile for RandomAccessFile.setLength

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

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

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

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

    有一个内部类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);