scaventz

LWJGL3的内存管理,第三篇,剩下的两种策略

这是讨论 LWJGL3 内存管理的一系列随笔的第三篇,其他内容参见 LWJGL3的内存管理

上一篇讨论的基于 MemoryStack 类的栈上分配方式,是效率最高的,但是有些情况下无法使用。比如需要分配的内存较大,又或许生命周期较长。这时候就可以考虑使用 MemoryUtil 类来进行内存分配。

MemoryUtil

在内部实现中,MemoryUtil 是通过JNI调用本地库用作Allocator来完成功能。截至目前,LWJGL3支持的内存库有:

MemoryUtil 对这些库进行了封装,提供了统一的API,使用 memAlloc 和 memFree 方法就可完成内存的分配与释放。

这种策略在redis的实现中也能看到,事实上 jemalloc 就是 redis 默认的内存分配器,当然它也支持别的分配器,比如 glibe、tcmalloc 等,在编译时通过参数指定即可。jemalloc 是一个通用的内存分配器实现,强调避免内存碎片和可伸缩的并发支持,它是从 FreeBSD 孵化出来的项目,官网有介绍:http://jemalloc.net/

在 LWJGL3 提供的一系列依赖包中,有一项依赖是

<dependency>
    <groupId>org.lwjgl</groupId>
    <artifactId>lwjgl-jemalloc</artifactId>
    <classifier>${lwjgl.natives}</classifier>
</dependency>

其中 ${lwjgl.natives} 是平台标识符。以Windows平台为例,填写 natives-windows。得到的包为 lwjgl-jemalloc-3.2.3-natives-windows.jar,里面其实是一个jemalloc.dll。LWJGL3会在启动时解压该Jar包,并进行DLL加载。

LWJGL3 在启动时,将会根据启动参数定位配置的内存分配器(MEMORY_ALLOCATOR),如果用户没有配置,则默认加载 “org.lwjgl.system.jemalloc.JEmallocAllocator”类

Class.forName(className);

类加载将会触发 JEmallocAllocator 内部的静态块进行执行

getLibrary 最终会通过JNI调用到native代码

JNIEXPORT jlong JNICALL Java_org_lwjgl_system_windows_WinBase_nLoadLibrary(JNIEnv *__env, jclass clazz, jlong nameAddress) {
    LPCTSTR name = (LPCTSTR)(intptr_t)nameAddress;
    jlong __result;
    UNUSED_PARAMS(__env, clazz)
    __result = (jlong)(intptr_t)LoadLibrary(name);
    saveLastError();
    return __result;
}

第一次看到这个代码是,一直没有找到 LoadLibrary 方法的实现。后来发现其实这是一个 Windows API,https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryw。 该 API 会将dll加载到本地进程空间,并返回方法地址,以支持随后的调用。

在某些场景中, MemoryUtil 也有可能不适用

  • 由于需要手动释放,可能在某些特定情况下会增加代码的复杂度
  • 不确定要分配的内存什么时候可以回收

如果遇到这些问题,就可以考虑最后一种策略,就是 JDK 提供的 ByteBuffer.allocateDirect()

BufferUtils (ByteBuffer.allocateDirect)

这是JDK自带的直接内存分配方式,使用简单。LWJGL3 提供了相应的API进行封装

public static ByteBuffer createByteBuffer(int capacity) {
    return ByteBuffer.allocateDirect(capacity).order(ByteOrder.nativeOrder());
}

封装的目的有两个

  1. 提供统一的API,将来内部实现变更时,依然可以向下兼容,API的使用者不需要太了解实现细节

  2. 内部实现可以进行优化,比如指定字节序

ByteBuffer::allocateDirect 作为JDK提供的标准方法,在LWJGL3中相当不推荐使用,还是因为性能的考虑,LWJGL3的应用通常对性能敏感,且可能负载较高。

ByteBuffer::allocateDirect 的缺点在于:

  1. 但是无法通过正常途径按需手动释放不需要的内存
  2. 而且通常需要两轮GC才能完成内存释放,在高负载情况下容易导致 OOM。

为什么 ByteBuffer::allocateDirect 的自动内存回收机制需要两轮 GC

这个说法来自于LWJGL3的内存管理FAQ,见 https://github.com/LWJGL/lwjgl3-wiki/wiki/1.3.-Memory-FAQ,下面我们分析一下。

DirectByteBuffer 类的构造函数中,会构建一个 Cleaner的实例

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

其中 Deallocator 是一个 Runnable 实例,run方法里是用于释放内存的代码

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

所以关键在于,run 方法何时得到执行。但遗憾的是本身该方法是由JVM来调度的,要像确定整个过程比较麻烦。暂时没有找到更好的方法来证实这一点,不过我发现在Phantom reference类的注释里其实也有类似的说法:

/**
 * Phantom reference objects, which are enqueued after the collector
 * determines that their referents may otherwise be reclaimed.  Phantom
 * references are most often used for scheduling pre-mortem cleanup actions in
 * a more flexible way than is possible with the Java finalization mechanism.
 *
 * <p> If the garbage collector determines at a certain point in time that the
 * referent of a phantom reference is <a
 * href="package-summary.html#reachability">phantom reachable</a>, then at that
 * time or at some later time it will enqueue the reference.
 *
 * <p> In order to ensure that a reclaimable object remains so, the referent of
 * a phantom reference may not be retrieved: The <code>get</code> method of a
 * phantom reference always returns <code>null</code>.
 *
 * <p> Unlike soft and weak references, phantom references are not
 * automatically cleared by the garbage collector as they are enqueued.  An
 * object that is reachable via phantom references will remain so until all
 * such references are cleared or themselves become unreachable.
 *
 * @author   Mark Reinhold
 * @since    1.2
 */
public class PhantomReference<T> extends Reference<T> {...

结合我们的例子,翻译一下就是说,对于一个 Cleaner 对象,GC在决定它的 referents(DirectByteBuffer)可以回收时,会把这个Cleaner入队。常用于执行 scheduling pre-mortem cleanup ,比 Java 的 finalization 机制(不明白这个机制具体指什么)更灵活。

如果GC在某个时间点认为一个 Cleaner 是 phantom reachable ,即只有虚引用能引用到 Cleaner,则GC将会将它 enqueue(通过执行Reference::enqueue)。

为了确保保留可回收对象,不能通过get方法获取到虚引用的referent,即虚引用的get方法将始终返回null。

与软引用或弱引用不同,虚引用不是垃圾收集器将其enqueue时自动清除。 虚引用可达的对象将保持不变,直到所有此类引用被清理或它们自身变得不可达。

总的来说,对于直接内存的回收,仍然是一个不确定的行为。

附:带中文注释的 Cleaner 类代码,中文注释来源于 https://zhuanlan.zhihu.com/p/29454205

public class Cleaner extends PhantomReference<Object>
{

    // Dummy reference queue, needed because the PhantomReference constructor
    // insists that we pass a queue.  Nothing will ever be placed on this queue
    // since the reference handler invokes cleaners explicitly.
    // dummyQueue 并没有实际用处,仅仅是因为 PhantomReference 的构造函数强制要求传入一个Queue
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

    // Doubly-linked list of live cleaners, which prevents the cleaners
    // themselves from being GC\'d before their referents
    // 所有的cleaner都会被加到一个双向链表中去,这样做是为了保证在referent被回收之前
    // 这些Cleaner都是存活的。
    static private Cleaner first = null;

    private Cleaner
        next = null,
        prev = null;

    // 构造的时候把自己加到双向链表中去
    private static synchronized Cleaner add(Cleaner cl) {
        if (first != null) {
            cl.next = first;
            first.prev = cl;
        }
        first = cl;
        return cl;
    }

    // clean方法会调用remove把当前的cleaner从链表中删除。
    private static synchronized boolean remove(Cleaner cl) {
        // If already removed, do nothing
        if (cl.next == cl)
            return false;

        // Update list
        if (first == cl) {
            if (cl.next != null)
                first = cl.next;
            else
                first = cl.prev;
        }
        if (cl.next != null)
            cl.next.prev = cl.prev;
        if (cl.prev != null)
            cl.prev.next = cl.next;

        // Indicate removal by pointing the cleaner to itself
        cl.next = cl;
        cl.prev = cl;
        return true;
    }

    // 用户自定义的一个Runnable对象,
    private final Runnable thunk;

    // 私有构造函数,保证了用户无法单独地使用new来创建Cleaner。
    private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue);
        this.thunk = thunk;
    }

    /**
     * 所有的Cleaner都必须通过create方法进行创建。
     */
    public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
            return null;
        return add(new Cleaner(ob, thunk));
    }

    /**
     * 这个方法会被Reference Handler线程调用,来清理资源。
     */
    public void clean() {
        if (!remove(this))
            return;
        try {
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }
}

分类:

技术点:

相关文章:

  • 2021-04-17
  • 2022-12-23
  • 2022-12-23
  • 2018-01-27
  • 2021-10-26
猜你喜欢
  • 2020-11-04
  • 2021-11-03
  • 2020-10-16
  • 2020-11-04
  • 2022-03-07
  • 2021-12-25
  • 2021-06-21
相关资源
相似解决方案