程序猿阵线联盟-汇总各类技术干货技术文

Java内存映射-MappedByteBuffer的释放

2018-05-26  本文已影响5人  酱君挺怎样

背景

随着时代的发展,在当今互联网时代,大家对时间的敏感度越来越高。因而对程序的处理能力要求也越来越高,各种程序优化层出不穷。其中在文件IO处理上,大部分人都采用了内存映射的概念(毕竟程序操作内存的速度比操作磁盘的速度快不是一丁半点)。

既然使用到了内存映射,在Java中,就不得不认识一下MappedByteBuffer了。

MappedByteBuffer

我们先用最简单的例子来看看 MappedByteBuffer 的使用:

  // 通过 RandomAccessFile 创建对应的文件操作类,第二个参数 rw 代表该操作类可对其做读写操作
  RandomAccessFile raf = new RandomAccessFile("fileName", "rw");

  // 获取操作文件的通道
  FileChannel fc = raf.getChannel();

  // 也可以通过FileChannel的open来打开对应的fc
  // FileChannel fc = FileChannel.open(Paths.get("/usr/local/test.txt"),StandardOpenOption.WRITE);


  // 把文件映射到内存
  MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, (int)    fc.size());

  // 读写文件
  mbb.putInt(4);
  mbb.put("test".getBytes());
  mbb.force();

  mbb.position(0);
  mbb.getInt();
  mbb.get(new byte[test.getBytes().size()]);
  ........

至此一个简单的Java通过内存操作文件的例子就完成了。但是可以看到上面的程序并没有关闭资源。这简单,加上关闭代码就好了

  // 将内存中内容刷盘
  mbb.force();
  // 关闭资源
  fc.close();
  raf.close();

很简单的代码。但是仅仅认为就这样就真的too young too simple了。

你会发现在你关闭了所有资源以后,程序还是占用着文件没有释放。即使你将mbb,fc,raf都设置为空,一样无济于事。。。这就有点尴尬了。如果程序中要定时删除对应过期的文件,由于这一直持有其文件句柄,我还无法删除了。

找了很多资料,发现这个Java关于mmap的一个bug。由于FileChannel调用了map方法做内存映射,但是没提供对应的unmap方法释放内存,导致内存一直占用该文件。实际unmap方法在FileChannelImpl中私有方法中,在finalize时,unmap无法调用导致内存没释放。

同样找了很多的解决方案,最终这2个比较靠谱的:

// 在关闭资源时执行以下代码释放内存
Method m = FileChannelImpl.class.getDeclaredMethod("unmap", MappedByteBuffer.class);
m.setAccessible(true);
m.invoke(FileChannelImpl.class, buffer);
AccessController.doPrivileged(new PrivilegedAction() {
    public Object run() {
      try {
        Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]);
        getCleanerMethod.setAccessible(true);
        sun.misc.Cleaner cleaner = (sun.misc.Cleaner)
        getCleanerMethod.invoke(byteBuffer, new Object[0]);
        cleaner.clean();
      } catch (Exception e) {
        e.printStackTrace();
      }
      return null;
    }
});

实际上面两个方法都调用了Cleaner类的clean方法释放,参考unmap代码

private static void unmap(MappedByteBuffer bb) {
    Cleaner cl = ((DirectBuffer)bb).cleaner();
    if (cl != null)
        cl.clean();
}

其实讲到这里该问题的解决办法已然清晰明了了。就是在删除索引文件的同时还取消对应的内存映射,删除mapped对象。 不过令人遗憾的是,Java并没有特别好的解决方案——令人有些惊讶的是,Java没有为MappedByteBuffer提供unmap的方法, 该方法甚至要等到Java 10才会被引入 ,DirectByteBufferR类是不是一个公有类 class DirectByteBufferR extends DirectByteBuffer implements DirectBuffer 使用默认访问修饰符 不过Java倒是提供了内部的“临时”解决方案——DirectByteBufferR.cleaner().clean() 切记这只是临时方法,毕竟该类在Java9中就正式被隐藏了,而且也不是所有JVM厂商都有这个类。 还有一个解决办法就是显式调用System.gc(),让gc赶在cache失效前就进行回收。 不过坦率地说,这个方法弊端更多:首先显式调用GC是强烈不被推荐使用的, 其次很多生产环境甚至禁用了显式GC调用,所以这个办法最终没有被当做这个bug的解决方案。

问题是解决了。但是可以看到第二个方法中,使用了AccessController,这个又是一个新的东西,我们下一期来再来研究看看这个到底是什么。

可关注我的公众号:酱君挺怎样


微信公众号.jpg
上一篇下一篇

猜你喜欢

热点阅读