其他答案中涉及使用((DirectBuffer) byteBuffer).cleaner().clean()
的方法在JDK 9+上不起作用(即使以反射形式),会显示An illegal reflective access operation has occurred
警告。这将在某些未来的JDK版本中完全停止工作。幸运的是,sun.misc.Unsafe.invokeCleaner(ByteBuffer)
可以为您执行完全相同的调用而无需警告:(来自OpenJDK 11源代码):
public void invokeCleaner(java.nio.ByteBuffer directBuffer) {
if (!directBuffer.isDirect())
throw new IllegalArgumentException("buffer is non-direct");
DirectBuffer db = (DirectBuffer)directBuffer;
if (db.attachment() != null)
throw new IllegalArgumentException("duplicate or slice");
Cleaner cleaner = db.cleaner();
if (cleaner != null) {
cleaner.clean();
}
}
作为一个
sun.misc
类,它将在某个时候被移除。有趣的是,在
sun.misc.Unsafe
中除了这个方法之外的所有调用都直接代理到
jdk.internal.misc.Unsafe
。我不知道为什么
invokeCleaner(ByteBuffer)
没有像其他所有方法一样被代理 -- 这可能是因为从 JDK 15 开始会有一种新的方式直接释放内存引用(包括
DirectByteBuffer
实例)。
我编写了以下代码,可以在 JDK 7/8 上清理/关闭/取消映射 DirectByteBuffer/MappedByteBuffer 实例,以及在 JDK 9+ 上运行,并且不会出现反射警告:
private static boolean PRE_JAVA_9 =
System.getProperty("java.specification.version","9").startsWith("1.");
private static Method cleanMethod;
private static Method attachmentMethod;
private static Object theUnsafe;
static void getCleanMethodPrivileged() {
if (PRE_JAVA_9) {
try {
cleanMethod = Class.forName("sun.misc.Cleaner").getMethod("clean");
cleanMethod.setAccessible(true);
final Class<?> directByteBufferClass =
Class.forName("sun.nio.ch.DirectBuffer");
attachmentMethod = directByteBufferClass.getMethod("attachment");
attachmentMethod.setAccessible(true);
} catch (final Exception ex) {
}
} else {
try {
Class<?> unsafeClass;
try {
unsafeClass = Class.forName("sun.misc.Unsafe");
} catch (Exception e) {
unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
}
cleanMethod = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
cleanMethod.setAccessible(true);
final Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
theUnsafe = theUnsafeField.get(null);
} catch (final Exception ex) {
}
}
}
static {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
getCleanMethodPrivileged();
return null;
}
});
}
private static boolean closeDirectByteBufferPrivileged(
final ByteBuffer byteBuffer, final LogNode log) {
try {
if (cleanMethod == null) {
if (log != null) {
log.log("Could not unmap ByteBuffer, cleanMethod == null");
}
return false;
}
if (PRE_JAVA_9) {
if (attachmentMethod == null) {
if (log != null) {
log.log("Could not unmap ByteBuffer, attachmentMethod == null");
}
return false;
}
if (attachmentMethod.invoke(byteBuffer) != null) {
return false;
}
final Method cleaner = byteBuffer.getClass().getMethod("cleaner");
cleaner.setAccessible(true);
cleanMethod.invoke(cleaner.invoke(byteBuffer));
return true;
} else {
if (theUnsafe == null) {
if (log != null) {
log.log("Could not unmap ByteBuffer, theUnsafe == null");
}
return false;
}
try {
cleanMethod.invoke(theUnsafe, byteBuffer);
return true;
} catch (final IllegalArgumentException e) {
return false;
}
}
} catch (final Exception e) {
if (log != null) {
log.log("Could not unmap ByteBuffer: " + e);
}
return false;
}
}
public static boolean closeDirectByteBuffer(final ByteBuffer byteBuffer,
final Log log) {
if (byteBuffer != null && byteBuffer.isDirect()) {
return AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
@Override
public Boolean run() {
return closeDirectByteBufferPrivileged(byteBuffer, log);
}
});
} else {
return false;
}
}
请注意,在JDK 9+的模块化运行时中,您需要将
requires jdk.unsupported
添加到您的模块描述符中(需要使用
Unsafe
)。
您的jar文件还可能需要
RuntimePermission("accessClassInPackage.sun.misc")
、
RuntimePermission("accessClassInPackage.jdk.internal.misc")
和
ReflectPermission("suppressAccessChecks")
。
对于垃圾回收
MappedByteBuffer
或
DirectByteBuffer
,在ClassGraph(由我编写)中实现了更完整的方法——入口点是
FileUtils
末尾的
closeDirectByteBuffer()
方法:
https://github.com/classgraph/classgraph/blob/latest/src/main/java/nonapi/io/github/classgraph/utils/FileUtils.java#L543
该代码使用反射编写,因为Java API(包括
Unsafe
)即将消失。
请注意,在JDK 16+中还存在另一个问题:
除非您使用
Narcissus或
JVM-Driver库来绕过强封装,否则此代码将无法在JDK 16+中正常工作。这是因为
MappedByteBuffer.clean()
是一个私有方法,而JDK 16强制实施强封装。ClassGraph通过在运行时调用Narcissus或JVM-driver来反射抽象出对私有封装方法的访问:
https://github.com/classgraph/classgraph/blob/latest/src/main/java/nonapi/io/github/classgraph/reflection/ReflectionUtils.java
警告:如果您在清理(释放)后尝试访问
DirectByteBuffer
,它将导致虚拟机崩溃。
还有其他安全注意事项,请参阅此错误报告中的最后一条评论:
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-4724038
OutOfMemoryError
错误,我认为这是文件句柄耗尽的结果。由于没有工具可以取消映射,这会导致不可预测的行为,即使在OutOfMemoryError
时,映射会尝试执行sleep(100)
和System.gc()
- 它只会推迟痉挛的发生。 - dma_k