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. throw throws try catch finally return

    throw throw 语句用于抛出异常,例如 throw new EOFException().   throws 当使用throw 语句抛出checked 异常后,可以不用捕获异常并处理,而是使用 ...

  2. matlab中set用法

    来源:https://www.cnblogs.com/sddai/p/5467500.html 1.MATLAB给每种对象的每一个属性规定了一个名字,称为属性名,而属性名的取值成为属性值.例如,Lin ...

  3. #ifndef, #define, #endif三者的作用

    #ifndef, #define, #endif 作用   #ifndef 它是if not define 的简写,是宏定义的一种,实际上确切的说,这应该是预处理功能三种(宏定义.文件包含.条件编译) ...

  4. 【题解】[CQOI]动态逆序对

    题目链接 题意如题,维护一个动态序列的逆序对总数. 注意题目给的是\([1,n]\)的排列,所以没必要离散化了. 考虑逆序对:二维偏序可以用树状数组做,现在是三维偏序,即加了一个时间维度. 找一个数前 ...

  5. Python 自动化测试全攻略:五种自动化测试模型实战详解

    随着移动互联网的发展,软件研发模型逐步完善,软件交付质量越来越受到软件公司的重视,软件测试技术特别是自动化测试技术开始在软件系统研发过程中发挥着越来越重要的作用. 与传统的手工测试技术相比,自动化测试 ...

  6. 使用MVC 5、Web API 2、KnockoutJS、Ninject和NUnit开发、架构和测试Web应用程序

    做一名微软软件开发人员就像在国际煎饼屋订早餐一样.每道菜都有一堆煎饼,你必须从各种各样的煎饼和糖浆口味中选择.对于web应用程序,解决方案堆栈是一组软件子系统或组件,用于交付功能完整的解决方案(无论是 ...

  7. mysql字段大小写敏感设置

    mysql中varchar类型的字符集一般设置成utf8,然而mysql默认是对大小写不敏感(不区分),如果想要mysql区分大小写需要设置排序规则,规则详解如下:在mysql中存在着各种排序规则:1 ...

  8. 安装memcache,步骤

    1.先下载emcache.rar安装包<ps:随便安装在哪里文件夹,或者新建文件夹都是可以的> 2.解压安装包<ps:运行里面的程序> 3.然后根据自己的phpstudy的版本 ...

  9. nginx的脚本引擎(二)rewrite

    其实rewrite指令和上一篇说的if/set/return/break之类的没多大差别,但是rewrite用起来相对复杂,我就把他单独放到了这里.想要弄懂nginx的脚本引擎需要先明白处理reque ...

  10. Python数据类型--列表(list)

    Python中列表对应的表示形式是"[]".列表中的元素可以是任何数据类型. 本文以List=[i for i in range(20)]为例进行论述:等价于List=[0, 1, ...