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;
}});
}
}
}

LWJGL3的内存管理,第三篇,剩下的两种策略的更多相关文章

  1. LWJGL3的内存管理,第一篇,基础知识

    LWJGL3的内存管理,第一篇,基础知识 为了讨论LWJGL在内存分配方面的设计,我将会分为数篇随笔分开介绍,本篇将主要介绍一些大方向的问题和一些必备的知识. 何为"绑定(binding)& ...

  2. LWJGL3的内存管理,第二篇,栈上分配

    LWJGL3的内存管理,第二篇,栈上分配 简介 为了讨论LWJGL在内存分配方面的设计,本文将作为该系列随笔中的第二篇,用来讨论在栈上进行内存分配的策略,该策略在 LWJGL3 中体现为以 Memor ...

  3. LWJGL3的内存管理

    LWJGL3的内存管理 LWJGL3 (Lightweight Java Game Library 3),是一个支持OpenGL,OpenAl,Opengl ES,Vulkan等的Java绑定库.&l ...

  4. LWJGL3的内存管理,简介及目录

    LWJGL3的内存管理,简介及目录 LWJGL3 (Lightweight Java Game Library 3),是一个支持OpenGL,OpenAl,Opengl ES,Vulkan等的Java ...

  5. 垃圾回收GC:.Net自己主动内存管理 上(三)终结器

    垃圾回收GC:.Net自己主动内存管理 上(三)终结器 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己主 ...

  6. JAVA高级篇(二、JVM内存模型、内存管理之第二篇)

    本文转自https://zhuanlan.zhihu.com/p/25713880. JVM的基础概念 JVM的中文名称叫Java虚拟机,它是由软件技术模拟出计算机运行的一个虚拟的计算机. JVM也充 ...

  7. spark内存管理这一篇就够了

    1. 堆内和堆外内存规划 1.1 堆内内存 堆内内存的大小,由 Spark 应用程序启动时的 –executor-memory 或 spark.executor.memory 参数配置.Executo ...

  8. 死磕Spring之AOP篇 - Spring AOP两种代理对象的拦截处理

    该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读. Spring 版本:5.1 ...

  9. Linux内存管理解析(三) : 内核对内核空间的内存管理

    内核采用 struct page 来表示一个物理页,在其中记载了诸多物理页的属性,比如 物理页被几个线程使用(如若没有则表示该页可以释放),页对应的虚拟地址. 首先需要知道的是,分配物理页可以分为两个 ...

随机推荐

  1. Jquery的一键上传组件OCUpload及POI解析Excel文件

    第一步:将js文件引入页面 <script type="text/javascript" src="${pageContext.request.contextPat ...

  2. 搭建实用深度学习环境(Ubuntu16.10+Theano0.8.2+Tensorflow0.11.0rc1+Keras1.1.0)

    在动手安装之前,首先要确定硬件,系统,准备安装软件的版本,确定这些软硬件之间是否相互支持或兼容.本文安装的主要环境和软件如下: Ubuntu16.10+CUDA8.0(cudnn5.1,CNMEM)+ ...

  3. C++ format 函数

    转载原文链接:https://blog.csdn.net/nowhaha/article/details/38710571 原博主很有心,文字标有颜色,奥利给!  Thanks C++ format ...

  4. 【题解】Tree

    题目戳我 \(\text{Solution:}\) 考虑点分治.对于这个两点之间,它意味着这点对必须是不一样的. 考虑用双指针统计答案.显然,对于两个数\(a,b\),要让\(a+b=k,a\)越大则 ...

  5. Java中类型判断的几种方式

    1. 前言 在Java这种强类型语言中类型转换.类型判断是经常遇到的.今天就细数一下Java中类型判断的方法方式. 2. instanceof instanceof是Java的一个运算符,用来判断一个 ...

  6. GDB将所有线程堆栈输出到文件

    在调试多线程程序时,经常需要查看线程堆栈信息,如果线程数目过多,每次查看一个线程堆栈,繁琐耗时.下面介绍一种一次性将所有线程堆栈输出到文件的方法. 首先,将gdb attach到调试线程 gdb -p ...

  7. 超好用的UnixLinux 命令技巧 大神为你详细解读

    1.删除一个大文件 我在生产服务器上有一个很大的200GB的日志文件需要删除.我的rm和ls命令已经崩溃,我担心这是由于巨大的磁盘IO造成的,要删除这个大文件,输入: > /path/to/fi ...

  8. MeteoInfoLab脚本示例:CloudSAT Swath HDF数据

    读取CloudSAT HDF Swath数据,绘图分上下两部分,上面是时间和高度维的Radar Reflectivity Factor二维图,下面是卫星轨迹图.示例程序: # Add file f = ...

  9. 从Linux源码看Socket(TCP)的bind

    从Linux源码看Socket(TCP)的bind 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的角度看下Server ...

  10. kettle学习笔记(三)— 定时任务的脚本执行

    kettle-定时任务 Kettle 的定时任务可以用kettle中的job工作来定时转换(缺点窗口不可关闭),同时也可以使用bat脚本来启动kettle的 '.ktr'转换. 注:这里对定时任务的时 ...