使用sun.misc.Unsafe强制释放由direct ByteBuffer分配的本机内存的示例?

24
JDK提供了分配所谓的直接ByteBuffer的能力,其中内存是在Java堆之外分配的。这可能是有益的,因为该内存不会被垃圾收集器触及,并且因此不会增加GC开销:对于像缓存这样的长期存在的东西来说,这是一个非常有用的属性。
然而,现有实现存在一个关键问题:当拥有的ByteBuffer被垃圾回收时,底层内存仅异步分配;没有办法强制提前释放。这可能是有问题的,因为GC周期本身并不受ByteBuffer处理的影响,并且考虑到ByteBuffers很可能驻留在旧一代内存区域中,可能会在ByteBuffer不再使用数小时后调用GC。
但是理论上应该可以直接使用sun.misc.Unsafe方法(freeMemory,allocateMemory):这就是JDK自身用于分配/释放本机内存的方法。看代码,我看到一个潜在的问题是内存双重释放的可能性——因此,我想确保状态会被正确清除。
有人能指引我找到做这个的代码吗?最好使用这个而不是JNA。
注意:我看到了与此相关的这个问题
看起来答案指出的方法是一个好方法:这里是Elastic Search的代码示例,使用了这个想法。谢谢大家!

这是可能的,在几年前在SO上发现...现在再看看。(过去我最终只使用了一个缓冲区的循环队列,这在我的情况下保持了低压力。) - user166390
1
哇,我现在找不到任何好的文章:( - user166390
我还没有看到过System.gc()被忽略的情况,至少在Sun上是这样,这似乎也是目标。 - nilskp
有一个系统属性可以让JVM忽略它,不过这有多少用处就不得而知了。 - StaxMan
1
最后一个链接已经失效。 - gouessej
3个回答

30

有一种更简单的方法来清理内存。

public static void clean(ByteBuffer bb) {
    if(bb == null) return;
    Cleaner cleaner = ((DirectBuffer) bb).cleaner();
    if (cleaner != null) cleaner.clean();
}

如果您需要快速丢弃直接或内存映射的ByteBuffer,使用Cleaner会有很大的帮助。

使用Cleaner的原因之一是因为您可以拥有底层内存资源的多个副本,例如使用slice()方法时。而Cleaner可以计算这些资源的数量。


1
嗯,我在看清理器,但不确定访问权限是否允许我直接调用它...但如果可以的话,这绝对是一个好方法。谢谢 - 也许我在放弃这种方法时太仓促了! - StaxMan
我在许多项目中使用了这种方法。在我看来,这比使用反射更好。 - Peter Lawrey
没错,只要类可访问,我同意。如果需要支持Unsafe本身丢失的潜在情况,则可以动态加载代码。再次感谢! - StaxMan
1
我的答案通过反射来执行cleaner.clean。有些人可能更喜欢不需要直接导入任何sun.*包。https://dev59.com/InA85IYBdhLWcg3wCe9Z#19447758 - Whome
2
sun.nio.ch.DirectBuffer(与sun.misc.Cleaner不同)在Java 1.9中默认情况下不能再使用,请参见JEP 260。 - gouessej
3
也许为弥补这一点,Java 9 中的 sun.misc.Unsafe 添加了一个 void invokeCleaner(ByteBuffer) 方法。 - Chapman Flack

5

使用sun.misc.Unsafe几乎是不可能的,因为分配的本地内存的基地址是java.nio.DirectByteBuffer构造函数的局部变量。

实际上,您可以使用以下代码强制释放本地内存:

import sun.misc.Cleaner;

import java.lang.reflect.Field;
import java.nio.ByteBuffer;

...

public static void main(String[] args) throws Exception {
    ByteBuffer direct = ByteBuffer.allocateDirect(1024);
    Field cleanerField = direct.getClass().getDeclaredField("cleaner");
    cleanerField.setAccessible(true);
    Cleaner cleaner = (Cleaner) cleanerField.get(direct);
    cleaner.clean();
}

这就是为什么我问的原因;清理器(Cleaner)与解除分配器(Deallocator)回调函数的整体编排是复杂的。我将尝试这种方法,假设类型清理器是可访问的(我注意到它调用的那个东西不是)。 - StaxMan
你不需要使用反射来滥用它。只需检查类型并像Peter Lawrey提出的解决方案一样使用显式转换即可。 - Alexander Nozik

2

基本上,您想要的是遵循与使用IO流相同的语义。就像您需要关闭流一样,您需要释放内存。因此,您可以编写自己的包装器,围绕本地调用进行早期内存释放。


1
是的,在非常高的层面上——但你看过具体细节吗?这就是问题所在:没有一种方法可以使用(直接)ByteBuffer来做到这一点,也没有close()方法。即使在底层,事情也相当复杂,不幸的是。 - StaxMan
是的,但是您可以通过使用unsafe实现自己的缓冲区来改变游戏。(这可能会非常好地使用填充内存以避免虚假共享,但那是完全不同的问题) - M Platvoet
足够正确。在我的情况下,我完全控制所有访问,因此底层原始存储既不会暴露,也不应该存在悬空引用的危险。 - StaxMan

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