共计 5349 个字符,预计需要花费 14 分钟才能阅读完成。
1. 前言
自从 java 在 1.4 版本后有了 NIO,direct memory 就变得如此的常见。在 NIO 中,direct memory 充当缓冲区,应用的是本机内存而不是堆内存。这种形式缩小了数据在 java 堆和本机堆之间的复制操作,肯定水平上进步了数据流转的效率。然而 direct memory 的调配和回收性能不高,不倡议频繁的调配 direct memory。
通常咱们都是通过 allocateDirect()
调配一块间接内存。这实际上是在堆上新建了一个 DirectByteBuffer
的 java 对象,该对象援用了一块间接内存的地址(jvm 过程中的虚地址,通过缺页异样调配理论的物理地址)。上面就会介绍通过 allocateDirect()
调配间接内存的过程以及 DirectMemory 在 hotspot 源码中的一些细节。
hotspot 源码版本为:openjdk 11.0.14
2. DirectMemory 内存调配流程
通过如下办法能够调配 1M 的间接内存。
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
间接进 allocateDirect()
办法,能够看到实际上是新建了一个 DirectByteBuffer
对象。
public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity); | |
} |
核心内容在 DirectByteBuffer
类的构造方法中,源码如下:
DirectByteBuffer(int cap) { // package-private | |
// 应用父类构造方法初始化 ByteBuffer 指针 | |
super(-1, 0, cap, cap); | |
// 判断是否设置了 内存对齐(默认 false)boolean pa = VM.isDirectMemoryPageAligned(); | |
int ps = Bits.pageSize(); | |
// 如果不设置内存对齐,size 和 cap 值一样 | |
long size = Math.max(1L, (long)cap + (pa ? ps : 0)); | |
// 内存调配的一些检查和回收操作 | |
Bits.reserveMemory(size, cap); | |
long base = 0; | |
try { | |
// 分配内存,并返回间接内存地址 | |
base = unsafe.allocateMemory(size); | |
} catch (OutOfMemoryError x) {Bits.unreserveMemory(size, cap); | |
throw x; | |
} | |
unsafe.setMemory(base, size, (byte) 0); | |
if (pa && (base % ps != 0)) { | |
// Round up to page boundary | |
address = base + ps - (base & (ps - 1)); | |
} else {address = base;} | |
cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); | |
att = null; | |
} |
2.1 内存调配前的查看
外围办法就在 Bits.reserveMemory(size, cap);
内。源码如下:
static void reserveMemory(long size, int cap) {if (!memoryLimitSet && VM.isBooted()) { | |
// 获取最大间接内存大小 | |
maxMemory = VM.maxDirectMemory(); | |
memoryLimitSet = true; | |
} | |
// optimist! | |
// 这个办法内查看是否存在残余间接内存空间 | |
if (tryReserveMemory(size, cap)) { | |
// 如果还有空间进行调配,间接返回 | |
return; | |
} | |
// 获取 Reference 对象(这里须要 Reference 的一些知识点)final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess(); | |
// retry while helping enqueue pending Reference objects | |
// which includes executing pending Cleaner(s) which includes | |
// Cleaner(s) that free direct buffer memory | |
// 通过 Cleaner 尝试开释一部分间接内存 | |
while (jlra.tryHandlePendingReference()) { | |
// 再次查看残余间接内存容量 | |
if (tryReserveMemory(size, cap)) {return;} | |
} | |
// trigger VM's Reference processing | |
// 强制 Full GC | |
// 能够看到,如果间接内存余量查看不通过,就会触发 Full GC | |
System.gc(); | |
// a retry loop with exponential back-off delays | |
// (this gives VM some time to do it's job) | |
// 在循环中屡次查看残余间接内存容量 | |
boolean interrupted = false; | |
try { | |
long sleepTime = 1; | |
int sleeps = 0; | |
while (true) {if (tryReserveMemory(size, cap)) {return;} | |
// MAX_SLEEPS 为 9 | |
if (sleeps >= MAX_SLEEPS) {break;} | |
if (!jlra.tryHandlePendingReference()) { | |
try {// 每次循环 睡眠工夫 * 2(单位:ms) | |
Thread.sleep(sleepTime); | |
sleepTime <<= 1; | |
sleeps++; | |
} catch (InterruptedException e) {interrupted = true;} | |
} | |
} | |
// no luck | |
throw new OutOfMemoryError("Direct buffer memory"); | |
} finally {if (interrupted) { | |
// don't swallow interrupts | |
Thread.currentThread().interrupt(); | |
} | |
} | |
} |
tryReserveMemory(size, cap)
进行间接内存余量查看的源码如下:
private static boolean tryReserveMemory(long size, int cap) { | |
// -XX:MaxDirectMemorySize limits the total capacity rather than the | |
// actual memory usage, which will differ when buffers are page | |
// aligned. | |
long totalCap; | |
// totalCapacity 记录以后已应用间接内存大小 | |
// 须要调配的大小如果小于 最大间接内存和以后已应用的间接内存的差值,则为 true | |
// 否则,返回 false | |
while (cap <= maxMemory - (totalCap = totalCapacity.get())) { | |
// 通过 CAS 将以后已应用间接内存大小 更新为 以后新的值 | |
if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) { | |
// 将已预留间接内存大小 更新 为以后新的值 | |
reservedMemory.addAndGet(size); | |
// 计数器自增 | |
count.incrementAndGet(); | |
return true; | |
} | |
} | |
return false; | |
} |
2.2 DirectMemory 默认最大是多少
在下面进行残余内存查看的时候,最大间接内存用的是 maxMemory 变量的值,源码中它的取值如下:
private static volatile long maxMemory = VM.maxDirectMemory(); | |
/** | |
* 这里这个 directMemory 在 VM.java 中定义了个动态变量,容易误导人,让人 | |
* 认为 maxDirectMemory 的大小就是 64M,其实不是的。**/ | |
public static long maxDirectMemory() {return directMemory;} |
其实 maxMemory
的值并不是就是 64M,默认如果不设置,maxMemory
的值和最大堆内存大小(-Xmx 设置的值)差不多。如果咱们设置了 JVM 的运行时参数 -XX:MaxDirectMemorySize=xxx
,maxMemory
就是咱们自定义的值。间接看 hotspot 源码:
在 jvm 中会将 -XX:MaxDirectMemorySize
的属性转换成 sun.nio.MaxDirectMemorySize
属性,如果不设置,则默认设置为-1。
在 jvm 启动的时候会读取下面设置的值,如果是-1,则将 directMemory 设置为运行时的最大内存(即差不多 -Xmx 的值)。
maxMemory()
也是个 native 办法,源码如下:
至于为什么说 差不多等于最大堆内存的值,其实是少了一个 survivor 的空间大小。还是看 hotspot 源码(maxMemory 是如何计算的):
hotspot 中对应获取运行时内存的办法是max_capacity()
,这个办法的大小计算和垃圾收集器关系密切:
// The particular choice of collected heap. | |
static CollectedHeap* heap() { return _collectedHeap;} |
能够看到这个版本的 hotspot 中有 8 种垃圾收集器
以下以 CMS 垃圾算法为根底
能够看到 CMS 垃圾算法下的 heap 大小其实是:年老代最大内存 ➕ 老年代最大内存
再看年老代的最大内存,其实是减掉了一个 survivor 的大小,源码如下:
- 由此可见,DirectMemory 默认最大是(Xmx – 1 个 survivor)的大小;
- DirectMemory 有余会导致 Full GC;
2.3 DirectByteBuffer 的内存调配
真正分配内存的办法其实是unsafe.allocateMemory(size)
,这是个 native 办法:
hotspot 中的实现是在 unsafe.cpp 中,源码如下:
实际上底层是调用了操作系统的 malloc
函数进行内存调配,而后返回一个内存地址给 java。
2.3.1 总结下 direct memory 大抵的调配流程:
- new 一个
DirectByteBuffer
对象; DirectByteBuffer
对象在执行初始化执行构造方法的时候调用unsafe.allocateMemory(size)
分配内存,以内存地址作为返回后果;- jvm 调用操作系统
malloc
函数调配虚拟内存(而后在理论应用中通过缺页异样调配理论的物理内存),返回内存地址给 java; - 将内存地址保留至
DirectByteBuffer
对象的成员变量address
中进行援用;
因而 DirectByteBuffer
自身作为一个 java 对象存在于 jvm 堆中,然而持有一个本机内存的内存地址的援用。DirectByteBuffer
在堆中占用的内存很小,然而很可能持有一块很大的本机内存援用。
3. DirectMemory 关联的本机内存是如何清理的
既然间接内存不属于 jvm 堆内存的一部分,那 GC 必定是无奈间接治理这块内存区域的,那 direct memory 是如何进行内存回收的呢?
后面曾经理解到,间接内存实际上是通过操作系统的 malloc
函数进行内存调配的,因而内存开释也须要调用操作系统的 free
函数。java 中能够通过 unsafe.freeMemory()
来调用底层的 free
函数。
基于这个思路,开释间接内存大只有两种路径:
- 手动调用
unsafe.freeMemory()
进行开释,netty 中 ByteBuf.release()就是这种形式; - 利用 GC 机制,在 GC 的过程中主动调用
unsafe.freeMemory()
开释不再被援用的间接内存;
明天次要想分享第 2 种回收形式,也就是如何在 GC 的过程中开释不再被援用的间接内存。
在开始之前,须要理解一些对于 Reference 的前置常识。因为通过 GC 间接回收 direct memory 的形式,齐全基于 PhantomReference
虚援用来实现。
这里我间接贴上一位大佬的博客:《【java.lang.ref】PhantomReference & jdk.internal.ref.Cleaner》地址:https://blog.csdn.net/reliveI…
这篇文章很全面的介绍了 PhantomReference
虚援用的相干常识,并在 DirectByteBuffer 章节清晰的形容了通过与 GC 的交互联动,实现 direct memory 的回收过程。给大佬点个赞👍。
到这里也就晓得,为啥《2.1 内存调配前的查看》在 Bits.reserveMemory(size, cap)
办法中要显示调用 System.gc()
进行 Full GC。这是为了尽可能回收不可达的 DirectByteBuffer
对象,也只有通过 GC 才会主动触发 unsafe.freeMemory()
的调用,开释间接内存。