我有一个内存泄漏,我已将其隔离到错误放置的直接字节缓冲区。
ByteBuffer buff = ByteBuffer.allocateDirect(7777777);
GC 收集包含这些缓冲区的对象,但不处理缓冲区本身。如果我实例化足够多的包含缓冲区的瞬态对象,我会得到这个令人鼓舞的消息:
java.lang.OutOfMemoryError:直接缓冲内存
我一直在寻找这个问题,显然
buff.clear();
和
System.gc();
不工作。
我有一个内存泄漏,我已将其隔离到错误放置的直接字节缓冲区。
ByteBuffer buff = ByteBuffer.allocateDirect(7777777);
GC 收集包含这些缓冲区的对象,但不处理缓冲区本身。如果我实例化足够多的包含缓冲区的瞬态对象,我会得到这个令人鼓舞的消息:
java.lang.OutOfMemoryError:直接缓冲内存
我一直在寻找这个问题,显然
buff.clear();
和
System.gc();
不工作。
我怀疑您的应用程序在某个地方引用了 ByteBuffer 实例,这会阻止它被垃圾收集。
直接 ByteBuffer 的缓冲内存是在正常堆之外分配的(这样 GC 就不会移动它!!)。但是,ByteBuffer API 没有提供显式处理/释放缓冲区的方法。所以我假设垃圾收集器会这样做......一旦它确定不再引用 ByteBuffer 对象。
一旦 DBB 到达引用队列,它就会被释放,并且运行终结器。然而,由于我们不能依赖终结器来运行,我们可以使用反射来手动调用它的“清洁器”。
使用反射:
/**
* DirectByteBuffers are garbage collected by using a phantom reference and a
* reference queue. Every once a while, the JVM checks the reference queue and
* cleans the DirectByteBuffers. However, as this doesn't happen
* immediately after discarding all references to a DirectByteBuffer, it's
* easy to OutOfMemoryError yourself using DirectByteBuffers. This function
* explicitly calls the Cleaner method of a DirectByteBuffer.
*
* @param toBeDestroyed
* The DirectByteBuffer that will be "cleaned". Utilizes reflection.
*
*/
public static void destroyDirectByteBuffer(ByteBuffer toBeDestroyed)
throws IllegalArgumentException, IllegalAccessException,
InvocationTargetException, SecurityException, NoSuchMethodException {
Preconditions.checkArgument(toBeDestroyed.isDirect(),
"toBeDestroyed isn't direct!");
Method cleanerMethod = toBeDestroyed.getClass().getMethod("cleaner");
cleanerMethod.setAccessible(true);
Object cleaner = cleanerMethod.invoke(toBeDestroyed);
Method cleanMethod = cleaner.getClass().getMethod("clean");
cleanMethod.setAccessible(true);
cleanMethod.invoke(cleaner);
}
ByteBuffer
文档说:
可以通过调用
allocateDirect
此类的工厂方法来创建直接字节缓冲区。此方法返回的缓冲区通常比非直接缓冲区具有更高的分配和释放成本。直接缓冲区的内容可能驻留在正常的垃圾收集堆之外,因此它们对应用程序内存占用的影响可能并不明显。因此,建议将直接缓冲区主要分配给受底层系统的本机 I/O 操作影响的大型、长期存在的缓冲区。通常,最好仅在直接缓冲区对程序性能产生可测量的增益时才分配它们。
特别是,“可能位于正常垃圾收集堆之外”的语句似乎与您的示例相关。
分配的内存是通过本机库实现的。当调用 ByteBuffer#finalize 方法时,这个内存将被释放,当 Buffer 被 gc'd 时。看看DirectByteBufferImpl的 allocate() 和 finalize() 实现。
buff.clear()
没有必要,System.gc()
只有在没有更多对 ByteBuffer 对象的引用时才会有所帮助。
这是一个适用于任何直接缓冲区的改进实现:
public static void destroyBuffer(Buffer buffer) {
if(buffer.isDirect()) {
try {
if(!buffer.getClass().getName().equals("java.nio.DirectByteBuffer")) {
Field attField = buffer.getClass().getDeclaredField("att");
attField.setAccessible(true);
buffer = (Buffer) attField.get(buffer);
}
Method cleanerMethod = buffer.getClass().getMethod("cleaner");
cleanerMethod.setAccessible(true);
Object cleaner = cleanerMethod.invoke(buffer);
Method cleanMethod = cleaner.getClass().getMethod("clean");
cleanMethod.setAccessible(true);
cleanMethod.invoke(cleaner);
} catch(Exception e) {
throw new QuartetRuntimeException("Could not destroy direct buffer " + buffer, e);
}
}
}
只要您依赖于 sun (oracle) 特定的实现,比尝试更改 java.nio.DirectByteBuffer 的可见性更好的选择是通过反射使用 sun.nio.ch.DirectBuffer 接口。
/**
* Sun specific mechanisms to clean up resources associated with direct byte buffers.
*/
@SuppressWarnings("unchecked")
private static final Class<? extends ByteBuffer> SUN_DIRECT_BUFFER = (Class<? extends ByteBuffer>) lookupClassQuietly("sun.nio.ch.DirectBuffer");
private static final Method SUN_BUFFER_CLEANER;
private static final Method SUN_CLEANER_CLEAN;
static
{
Method bufferCleaner = null;
Method cleanerClean = null;
try
{
// operate under the assumption that if the sun direct buffer class exists,
// all of the sun classes exist
if (SUN_DIRECT_BUFFER != null)
{
bufferCleaner = SUN_DIRECT_BUFFER.getMethod("cleaner", (Class[]) null);
Class<?> cleanClazz = lookupClassQuietly("sun.misc.Cleaner");
cleanerClean = cleanClazz.getMethod("clean", (Class[]) null);
}
}
catch (Throwable t)
{
t.printStackTrace();
}
SUN_BUFFER_CLEANER = bufferCleaner;
SUN_CLEANER_CLEAN = cleanerClean;
}
public static void releaseDirectByteBuffer(ByteBuffer buffer)
{
if (SUN_DIRECT_BUFFER != null && SUN_DIRECT_BUFFER.isAssignableFrom(buffer.getClass()))
{
try
{
Object cleaner = SUN_BUFFER_CLEANER.invoke(buffer, (Object[]) null);
SUN_CLEANER_CLEAN.invoke(cleaner, (Object[]) null);
}
catch (Throwable t)
{
logger.trace("Exception occurred attempting to clean up Sun specific DirectByteBuffer.", t);
}
}
}
此问题的现有答案缺少许多警告,例如在 JDK 9+ 下运行时模块描述符包含的要求、由于强制封装而在 JDK 16+ 下没有变通办法而requires jdk.unsupported
无法访问、运行时的要求MappedByteBuffer.cleaner()
使用SecurityManager
JDK 7-16 等。我在这里详细介绍: