2023-09-13  阅读(0)
原文作者:https://blog.csdn.net/wangwei19871103/category_9681495_2.html 原文地址: https://blog.csdn.net/wangwei19871103/article/details/104393301

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后很久就没被复用的内存,这样的内存放久了就等于内存泄露,所以还是要回收掉。

202309132204247351.png
所有数组都要进行清除:

202309132204259132.png
数组中的所有MemoryRegionCache都要清除:

202309132204265403.png

202309132204272104.png

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

最终是调用PoolArenafreeChunk进行回收的。如果整个PoolThreadCache被回收了,会使得finalizer=true

202309132204278795.png那也就不回收entry了。

其实这个跟刚开始的PoolArenafree里面最后释放的一样:

202309132204286386.png

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

这里就是真正的释放块内存了,两种情况,一种是堆内存,这个当然就给虚拟机处理器,所以实现方法是空的:

202309132204291657.png
堆外内存的话就是根据是否有清理器来释放,其实就是直接释放堆外内存,前面文章有讲过的:

202309132204298028.png

PoolThreadLocalCache的onRemoval

如何才可以在线程结束的时候调用方法呢,其实就是包装成FastThreadLocalRunnable的效果:

202309132204308279.png

2023091322043146510.png

2023091322043227411.png
前面有说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();
                }
            }
        }

2023091322043295312.png

2023091322043349313.png

2023091322043441714.png
最终还是到这里,其实就是返回所有回收的总数量:

2023091322043509315.png

现在我们知道了,其实最终释放的是整一个块。内存的申请和释放可以从缓存中获取,缓存又存在于线程本地变量中,当然线程回收了,会回收线程本地变量,就会释放对应的缓存,当然不一定会把块释放了,因为有可能多个线程对应一个块,其他线程还在使用块。

好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。


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] ,回复【面试题】 即可免费领取。

阅读全文