就在前两天,用nio做了一个文件的crud,但是在window下删除文件的时候报了一个奇怪的异常,即AccessDeniedException,搭眼一看这不就是没有授予文件的删除权限么,于是我手动删除 这个文件,提示文件被java进程占用,不能删除,于是大概就知道为什么了,第一个想到的是读取文件是不是没有关掉流,于是查阅了代码,发现并不是这个问题导致的,因为我是通过try/resource方式自动关闭了流,因此可以排除这个问题,代码见下:
try (FileChannel fileChannel = FileChannel.open(Paths.get("D:/aaa.txt"),
StandardOpenOption.READ)){
//xxx
}catch (Exception e){
}
其次在网上搜阅了大量资料,发现这特喵是jdk的一个bug,从2002年就被开发者提到了官网,到jdk8还没有close掉这个问题,可见这个问题很伤……因此在开发者的回答列表里发现了一个解决方案,即通过反射调用其 sun公司提供的 FileChannelImpl,unmap方法,为什么是FileChannelImpl这个class呢?
看源码,因为我们在打开通道的时候会调用FileChannel.map()方法来进行内存数据传输,因为FileChannel是个抽象类,map()是个抽象方法,其实际调用map的class就是FileChannelImpl,用来从 开启一个文件大小的堆外内存,这个buffer可设置为只读,只写等策略, 在调用map完之后,会对应调用一个unmap的方法来释放jvm引用内存的指针,因此手动调用unmap方法则可以完美解决问题。bug地址在文末
unmap在FileChannelImpl的实现是酱紫的,传入一个buffer,然后手动调用cleaner方法来进行释放内存指针。注意这里面调用的是DirectBuffer类的cleaner,和Cleaner的clean方法,这些类都是sun公司提供的,位置是在jdk包下的rt.jar
private static void unmap(MappedByteBuffer var0) {
Cleaner var1 = ((DirectBuffer)var0).cleaner();
if (var1 != null) {
var1.clean();
}
}
因为内部都是引用了sun下的包,在代码checkstyle的时候会报错,于是为了追求好的写法,我翻阅了大量的资料来进行代码优化,于是想起来rocketmq内部的mappedfile(commitlog/index/consumeLog)也是通过nio来分配堆外来进行操作文件,或许他们的项目里会有更优解,于是找到了MappedFile类,我copy下来解读
private void init(final String fileName, final int fileSize) throws IOException {
this.fileName = fileName;
this.fileSize = fileSize;
this.file = new File(fileName);
this.fileFromOffset = Long.parseLong(this.file.getName());
boolean ok = false;
...
try {
//创建一个文件通道 读取文件到堆外内存,和我们这里创建通道操作一样的
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
...
} catch (FileNotFoundException e) {
log.error("Failed to create file " + this.fileName, e);
throw e;
} catch (IOException e) {
log.error("Failed to map file " + this.fileName, e);
throw e;
}
}
}
在翻阅到它的刷盘操作:
public int flush(final int flushLeastPages) {
//可刷盘?
if (this.isAbleToFlush(flushLeastPages)) {
if (this.hold()) {
//获取指针的值
int value = getReadPosition();
try {
//刷盘 将buffer的值写入到磁盘
//We only append data to fileChannel or mappedByteBuffer, never both.
if (writeBuffer != null || this.fileChannel.position() != 0) {
this.fileChannel.force(false);
} else {
this.mappedByteBuffer.force();
}
} catch (Throwable e) {
log.error("Error occurred when force data to disk.", e);
}
this.flushedPosition.set(value);
//注意这里》》》最关键的一步,释放内存和锁
this.release();
} else {
log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
this.flushedPosition.set(getReadPosition());
}
}
return this.getFlushedPosition();
}
//贴release方法
public void release() {
long value = this.refCount.decrementAndGet();
if (value > 0)
return;
synchronized (this) {
//发现又是一层调用 于是继续
this.cleanupOver = this.cleanup(value);
}
}
@Override
public boolean cleanup(final long currentRef) {
...
//这里调用了cleanup 是自己封装的释放buffer的方法
clean(this.mappedByteBuffer);
log.info("unmap file[REF:" + currentRef + "] " + this.fileName + " OK");
return true;
}
于是,找到clean方法,即下面:
//
public static void clean(final ByteBuffer buffer) {
if (buffer == null || !buffer.isDirect() || buffer.capacity() == 0)
return;
//反射调用buffer的cleaner方法,然后再调用cleaner的clean方法,于是在
//不用引用sun包的情况下解决了这个问题
invoke(invoke(viewed(buffer), "cleaner"), "clean");
}
private static Object invoke(final Object target, final String methodName, final Class<?>... args) {
return AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
try {
//这里反射调用
Method method = method(target, methodName, args);
method.setAccessible(true);
return method.invoke(target);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
});
}
private static Method method(Object target, String methodName, Class<?>[] args)
throws NoSuchMethodException {
try {
return target.getClass().getMethod(methodName, args);
} catch (NoSuchMethodException e) {
return target.getClass().getDeclaredMethod(methodName, args);
}
}
private static ByteBuffer viewed(ByteBuffer buffer) {
String methodName = "viewedBuffer";
Method[] methods = buffer.getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals("attachment")) {
methodName = "attachment";
break;
}
}
ByteBuffer viewedBuffer = (ByteBuffer) invoke(buffer, methodName);
if (viewedBuffer == null)
return buffer;
else
return viewed(viewedBuffer);
}
于是借鉴了它的代码,完成了buffer释放的操作
注:在macoS系统下未出现这个问题,猜测是操作系统的内存管理机制的不同;当然,还有一种情况就是当前账户确实是没有权限,但是这种情况下读取文件应该都会报错的,所以不考虑这种情况