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. Raft 协议

    Paxos 存在的问题 Paxos 算法的描述偏学术化,缺失了很多细节,无法直接应用于工程领域.实际工程应用中的分布式算法大多是 Paxos 的变种,验证这些算法的正确性也成为了一个难题. 举个例子: ...

  2. Unicode、UTF8、GB2312、ANSI

    来源:https://blog.csdn.net/osanwenyu/article/details/48439461 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原 ...

  3. dockerfile-maven-plugin极简教程

    目录 一.简介 二.概述 三.将spring-boot-app打包成docker镜像 创建示例应用 修改pom文件 增加Dockerfile文件 使用Maven打包应用 运行应用镜像 四.分析mvn ...

  4. 用 shell 脚本做 restful api 接口监控

    问题的提出 基于历史原因,公司有一个"三无"采集服务--无人员.无运维.无监控--有能力做的部门不想接.接了的部门没能力.于是就一直这样裸奔,直到前几天一个依赖于这个采集服务的大数 ...

  5. markdown的基本使用

    1.什么是markdown? markdown是一种轻量级的标记语言 可以转换为html/xhtml和其它格式 可读.直观.学习成本低 当你学会使用markdown编写文档时,你会感觉自己发现了一个新 ...

  6. 发布MeteoInfo 2.3

    主要的更新如下: Using SVG icons in GUI. Update netCDF java library to 5.3.3. Update FlatLaf to 0.40. Update ...

  7. MeteoInfoLab脚本示例:中尺度气旋散点图

    全球长时间序列中尺度气旋数据(http://cioss.coas.oregonstate.edu/eddies/)有netCDF格式,散点数据类型,只有一个很大的维Nobs = 2590938.尝试读 ...

  8. day24 Pyhton学习 反射

    一.isinstance,type,issubclass issubclass() 这个内置函数可以帮我们判断x类是否是y类的子类 issubclass(x,y) class Base: pass c ...

  9. centos8安装php7.4

    一,下载php7.4 1,官方网站: https://www.php.net/ 2,下载 [root@yjweb source]# wget https://www.php.net/distribut ...

  10. centos8平台给sudo配置日志

    一,sudo日志的用途: 我们可以记录下来用户账号在哪个时间进行过sudo 这样不需要再从secure日志中查找用户的sudo记录 说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://w ...