PoolArena的free
上篇就讲到这里,我们来看看里面干了什么,我把跟PoolThreadCache
无关的都先略去了,后面再讲。
void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
...
SizeClass sizeClass = sizeClass(normCapacity);
if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
// cached so not free it. 缓存成功就返回
return;
}
...
}
}
首先进行了尺寸类型的获取,然后如果是有缓存的,就尝试添加到缓存,如果添加成功就返回,不然就要释放块内存了。
PoolThreadCache的add
获取缓存,尝试添加:
boolean add(PoolArena<?> area, PoolChunk chunk, ByteBuffer nioBuffer,
long handle, int normCapacity, SizeClass sizeClass) {
MemoryRegionCache<?> cache = cache(area, normCapacity, sizeClass);
if (cache == null) {
return false;
}
return cache.add(chunk, nioBuffer, handle);//存在就尝试添加
}
cache
即是根据尺寸类型进行获取,获取的3
个方法前面一片都讲过了。
private MemoryRegionCache<?> cache(PoolArena<?> area, int normCapacity, SizeClass sizeClass) {
switch (sizeClass) {
case Normal:
return cacheForNormal(area, normCapacity);
case Small:
return cacheForSmall(area, normCapacity);
case Tiny:
return cacheForTiny(area, normCapacity);
default:
throw new Error();
}
}
MemoryRegionCache的add
这里开始尝试添加,会先将要缓存的数据封装成一个实体,然后尝试放进队列里,如果队列满了,放不了,就把实体回收,返回false
。如果能放,就直接返回true
。
public final boolean add(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle) {
Entry<T> entry = newEntry(chunk, nioBuffer, handle);//获取一个entry
boolean queued = queue.offer(entry);//放进缓存队列
if (!queued) {
// If it was not possible to cache the chunk, immediately recycle the entry
entry.recycle();//不成功就立刻回收entry
}
return queued;//返回是否入队成功
}
至此添加到缓存讲完了, 其实就是缓冲区没用被引用的时候就会添加到线程本地缓存,以便于下一次用可以直接拿出来初始化,不需要进行内存分配算法了,提高了效率 。
PoolThreadCache的trim
前面这个方法没详细讲,现在可以讲下,其实就是清空缓存,但是值在缓存尝试分配后,如果到达分配的阈值就会开始清除,都分配了8k
次了,如果还有剩下的,基本就是release
后很久就没被复用的内存,这样的内存放久了就等于内存泄露,所以还是要回收掉。
所有数组都要进行清除:
数组中的所有MemoryRegionCache
都要清除:
MemoryRegionCache的trim
首先获取要释放的数量,没分配过也有size
个,没关系呀,后面会判断能否取从队列里取,取不到说明没了,就跳出来了,不会执行剩下的循环。当然分配出去的就要减掉啦。
public final void trim() {
int free = size - allocations;//还存在队列里的数量
allocations = 0;
// We not even allocated all the number that are
if (free > 0) {
free(free, false);
}
}
free
可以看到,主要还是考从队列里取,去完释放完为止,返回释放的个数。
private int free(int max, boolean finalizer) {
int numFreed = 0;
for (; numFreed < max; numFreed++) {
Entry<T> entry = queue.poll();
if (entry != null) {
freeEntry(entry, finalizer);
} else {
// all cleared 如果发现null了说明释放完了,不一定要遍历完max
return numFreed;
}
}
return numFreed;
}
freeEntry
private void freeEntry(Entry entry, boolean finalizer) {
PoolChunk chunk = entry.chunk;
long handle = entry.handle;
ByteBuffer nioBuffer = entry.nioBuffer;
if (!finalizer) {
// recycle now so PoolChunk can be GC'ed. This will only be done if this is not freed because of
// a finalizer.
entry.recycle();//回收entry
}
chunk.arena.freeChunk(chunk, handle, sizeClass, nioBuffer, finalizer);
}
最终是调用PoolArena
的freeChunk
进行回收的。如果整个PoolThreadCache
被回收了,会使得finalizer=true
:
那也就不回收entry
了。
其实这个跟刚开始的PoolArena
的free
里面最后释放的一样:
PoolArena的freeChunk
先统计释放的数量,然后调用chunk
块的PoolChunkList
释放,根据情况再看是否要释放chunk
块。
void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer, boolean finalizer) {
final boolean destroyChunk;
synchronized (this) {
// We only call this if freeChunk is not called because of the PoolThreadCache finalizer as otherwise this
// may fail due lazy class-loading in for example tomcat.
if (!finalizer) {//增加释放次数
switch (sizeClass) {
case Normal:
++deallocationsNormal;
break;
case Small:
++deallocationsSmall;
break;
case Tiny:
++deallocationsTiny;
break;
default:
throw new Error();
}
}
destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer);//返回是否要销毁
}
if (destroyChunk) {//是否释放块
// destroyChunk not need to be called while holding the synchronized lock.
destroyChunk(chunk);
}
}
PoolChunkList的free
最终还是要到chunk
中释放。释放完之后看使用率进行移动并从块列表中删除,如果移动到q000
不能往前移了,就说明块没利用率,没说没任何使用,得释放了,返回false
。否则返回true
。
boolean free(PoolChunk<T> chunk, long handle, ByteBuffer nioBuffer) {
chunk.free(handle, nioBuffer);//块释放内存
if (chunk.usage() < minUsage) {//小于最小使用率的
remove(chunk);//从当前快列表中删除
// Move the PoolChunk down the PoolChunkList linked-list.
return move0(chunk);//给上一个快列表
}
return true;
}
PoolChunk的free
这里做的事比较多,首先就是根据句柄handle
获取相应的信息,然后如果是子页类型的就去释放子页的部分空间,如果释放完后发现子页的内存还有被使用,那就返回,否则就把子页回收了,把内存映射索引对应的深度设置回初始深度值,更新其父节点深度。
void free(long handle, ByteBuffer nioBuffer) {
int memoryMapIdx = memoryMapIdx(handle);//获取内存映射索引
int bitmapIdx = bitmapIdx(handle);//获取位图索引
if (bitmapIdx != 0) { // free a subpage 表示有子页要释放的操作,normal类型是没有的
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];//获取子页
assert subpage != null && subpage.doNotDestroy;
// Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
// This is need as we may add it back and so alter the linked-list structure.
PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
synchronized (head) {
if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {//释放位图索引所在的内存,返回true表示子页内存有被用,不用销毁,直接返回,否则表示子页内存没被使用了,可以被销毁了
return;
}
}
}
freeBytes += runLength(memoryMapIdx);//回收空间数量
setValue(memoryMapIdx, depth(memoryMapIdx));//把内存映射索引设置回初始值
updateParentsFree(memoryMapIdx);//更新父节点的深度
if (nioBuffer != null && cachedNioBuffers != null &&
cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
cachedNioBuffers.offer(nioBuffer);//如果使用了nioBuffer,就放入缓存cachedNioBuffers
}
}
PoolSubpage的free
释放位图中的索引位,可以给下一次申请的直接用,如果可用的数量是0
,即已经是用完的子页的话,重新加入到head
后。返回true
,说明不需要删除子页,又复用了。如果可用数量不为0
,释放后数量页等于最大数量,说明这个子页现在没有任何内存被分配出去了,那就等于这个子页没什么用了, 但是如果就只剩head了,那就不删除了,直接返回,删除的话后面申请的又的重新创建,浪费时间。如果除了head
还有其他子页,那就从双向循环列表中删除掉当前子页,并设置需要销毁标志doNotDestroy=false
,返回false
,否则就返回true
。
boolean free(PoolSubpage<T> head, int bitmapIdx) {
if (elemSize == 0) {
return true;
}
int q = bitmapIdx >>> 6;//获取位图在数组中的索引
int r = bitmapIdx & 63;//获取位图内部索引
assert (bitmap[q] >>> r & 1) != 0;
bitmap[q] ^= 1L << r;//异或,设置为0
setNextAvail(bitmapIdx);//将索引释放,下一个可以直接获取整个索引
if (numAvail ++ == 0) {//如果已经使用完的子页,有空间了再加到head后
addToPool(head);
return true;
}
if (numAvail != maxNumElems) {//还有其他内存在使用
return true;//还在使用,返回true
} else {//不使用了,所以能分配的就等于最大分配数
// Subpage not in use (numAvail == maxNumElems)
if (prev == next) {//只有一个head了,那不能删除
// Do not remove if this subpage is the only one left in the pool.
return true;
}
//如果有其他子页的话,删除当前子页
// Remove this subpage from the pool if there are other subpages left in the pool.
doNotDestroy = false;//设置要销毁
removeFromPool();
return false;
}
}
updateParentsFree
这个就是申请时候的更新逆运算啦,反过来理解就好:
private void updateParentsFree(int id) {
int logChild = depth(id) + 1;
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
logChild -= 1; // in first iteration equals log, subsequently reduce 1 from logChild as we traverse up
if (val1 == logChild && val2 == logChild) {//两个子节点都被使用的情况下
setValue(parentId, (byte) (logChild - 1));//父节点只要将子节点深度-1就好
} else {//只用了其中一个,就需要设置为两个子节点中最小的即可
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
}
id = parentId;//继续向上找父节点
}
}
PoolChunkList的remove
如果发现chunk
块列表的使用率小的话,就从块列表中删除,并准备往前移。
private void remove(PoolChunk<T> cur) {
if (cur == head) {//是头结点处理
head = cur.next;
if (head != null) {
head.prev = null;
}
} else {//非头结点处理
PoolChunk<T> next = cur.next;
cur.prev.next = next;
if (next != null) {
next.prev = cur.prev;
}
}
}
PoolChunkList的move0
往以一个块列表移,如果发现符合条件就加入块列表,否则就继续往前移,直到移到q000
块列表位为止,返回false
,表示要销毁这个块。
private boolean move0(PoolChunk<T> chunk) {
if (prevList == null) {//当前块列表没有前一个块列表了,也就是Q0,可以直接删除块释放内存
// There is no previous PoolChunkList so return false which result in having the PoolChunk destroyed and
// all memory associated with the PoolChunk will be released.
assert chunk.usage() == 0;
return false;
}
return prevList.move(chunk);//移动到前一个快列表中
}
PoolArena的destroyChunk
这里就是真正的释放块内存了,两种情况,一种是堆内存,这个当然就给虚拟机处理器,所以实现方法是空的:
堆外内存的话就是根据是否有清理器来释放,其实就是直接释放堆外内存,前面文章有讲过的:
PoolThreadLocalCache的onRemoval
如何才可以在线程结束的时候调用方法呢,其实就是包装成FastThreadLocalRunnable
的效果:
前面有说PoolThreadLocalCache
在线程执行完会自动释放缓存,其实就是onRemoval
方法。
@Override
protected void onRemoval(PoolThreadCache threadCache) {
threadCache.free(false);
}
里面调用了缓存的释放方法。
PoolThreadCache的free(boolean finalizer)
里面也是每个缓存数组都释放,返回释放的总数量,然后对应的计数减少。
void free(boolean finalizer) {
// As free() may be called either by the finalizer or by FastThreadLocal.onRemoval(...) we need to ensure
// we only call this one time.
if (freed.compareAndSet(false, true)) {
int numFreed = free(tinySubPageDirectCaches, finalizer) +
free(smallSubPageDirectCaches, finalizer) +
free(normalDirectCaches, finalizer) +
free(tinySubPageHeapCaches, finalizer) +
free(smallSubPageHeapCaches, finalizer) +
free(normalHeapCaches, finalizer);
if (numFreed > 0 && logger.isDebugEnabled()) {
logger.debug("Freed {} thread-local buffer(s) from thread: {}", numFreed,
Thread.currentThread().getName());
}
if (directArena != null) {
directArena.numThreadCaches.getAndDecrement();
}
if (heapArena != null) {
heapArena.numThreadCaches.getAndDecrement();
}
}
}
最终还是到这里,其实就是返回所有回收的总数量:
现在我们知道了,其实最终释放的是整一个块。内存的申请和释放可以从缓存中获取,缓存又存在于线程本地变量中,当然线程回收了,会回收线程本地变量,就会释放对应的缓存,当然不一定会把块释放了,因为有可能多个线程对应一个块,其他线程还在使用块。
好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。