Java映射/ NIO / NFS问题导致VM故障:“最近编译的Java代码中发生了不安全内存访问操作的错误”。

9
我已经编写了一个解析器类,用于解析特定的二进制格式(如果有人感兴趣,可以访问nfdump)。该解析器使用java.nio的MappedByteBuffer读取每个几GB的文件。该二进制格式只是一系列头和大部分固定大小的二进制记录,通过调用nextRecord()来提供给调用者,该方法会推动状态机,并在完成时返回null。它表现良好,在开发机上运行正常。
但是在生产主机上,它可能运行几分钟或几小时,但总是似乎会抛出“java.lang.InternalError:在编译的Java代码中发生最近的不安全内存访问操作错误”,指向Map.getInt、getShort方法之一,即映射中的读取操作。
设置地图的无争议(?)代码如下:
    /** Set up the map from the given filename and position */
    protected void open() throws IOException {
            // Set up buffer, is this all the flexibility we'll need?
            channel = new FileInputStream(file).getChannel();    
            MappedByteBuffer map1 = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
            map1.load(); // we want the whole thing, plus seems to reduce frequency of crashes?
            map = map1;
            // assumes the host  writing the files is little-endian (x86), ought to be configurable
            map.order(java.nio.ByteOrder.LITTLE_ENDIAN);
            map.position(position);
    }

然后我使用各种map.get*方法读取shorts、ints、longs和其他字节序列,直到文件结尾并关闭该映射。在我的开发主机上从未看到过异常抛出。但我的生产主机和开发之间的重大差异是,在前者上,我通过NFS读取这些文件的序列(最终可能为6-8TB,仍在增长)。在我的开发机器上,我有一小部分这些文件的本地副本(60GB),但当它在生产主机上崩溃时,通常在达到60GB数据之前就已经崩溃了。两台机器都运行java 1.6.0_20-b02,虽然生产主机运行Debian / lenny,而开发主机运行Ubuntu / karmic。我不确定这是否会有任何区别。两台机器都有16GB RAM,并且使用相同的java堆设置运行。我认为如果我的代码有错误,JVM中肯定也有足够的错误没有抛出适当的异常!但我认为这只是一个特定的JVM实现bug,由于NFS和mmap之间的交互引起,可能是6244515的复发,这是正式修复的。
我已经尝试添加“load”调用来强制MappedByteBuffer加载其内容到RAM中-这似乎延迟了我进行的一个测试运行中的错误,但并没有防止它。或者这可能是巧合,那是它在崩溃之前持续的最长时间!
如果您已经读到这里并且曾经在java.nio中进行过此类操作,您的直觉会是什么?现在我的想法是重写它而不使用nio :)

我猜你已经看过(http://nfs.sourceforge.net/)的D8了。 - Justin
谢谢,我也没有在写这些文件。 - Matthew Bloch
我在使用Java 7u1时发现在本地ext4和tmpfs文件系统上,内存映射文件出现了这种情况。 - Peter Lawrey
尽管我有10 GB 的可用交换空间,但当我物理内存耗尽(在 top 中查看)时,这种情况仍会发生。 - Peter Lawrey
1个回答

4
我会重新编写它,而不使用映射的NIO。如果你处理的文件数量超过一个,那么就会存在一个问题,即映射内存永远不会被释放,因此你将会用完虚拟内存:请注意,这不一定只是与垃圾回收器交互的OutOfMemoryError,而是无法分配新的映射缓冲区。我会使用FileChannel。
话虽如此,对NFS文件进行大规模操作总是极其棘手的。你最好重新设计系统,使每个文件由本地CPU读取。这样做还可以获得巨大的速度提升,比不使用映射缓冲区损失的20%要多得多。

我确实考虑过缺少虚拟地址空间的问题,但正如你所说,这应该会在映射失败时表现出来(而且我一次只读取一个文件,在64位系统上)。我可能会重新安排服务器,使文件与Java进程位于同一台服务器上,并避免任何NFS问题。短期内,我将把所有内容都读入ByteBuffer中,但由于多个线程同时读取相同的文件,因此它正在重新实现mmap 应该是一个优雅解决方案的东西! - Matthew Bloch
是的,我希望得到一个让我保留mmap的答案,只是需要别人推一下说“这行不通” :)现在的open()代码只是将整个文件读入分配的ByteBuffer中。虽然我的直觉是担心内存浪费(因为几个读取器=堆上的几个副本),但与以前的运行相比,我没有看到性能下降,所以不能抱怨。我已经将旧代码注释了,希望我可以恢复“优雅”的mmap,但假设我的nfdump文件保持相同的大小,我可能不再需要它。 - Matthew Bloch
“几个读取器=堆上的几个副本”:只有在您制作这些几个副本时才会出现。难道您不能组织某种单例访问吗? - user207421

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