netty源码浅析-池化内存回收

Netty 高性能内存管理设计(2) Netty 的内存规格分类以及内存管理的核心组件,今天我们继续介绍 Netty 内存分配与回收的实现原理。有了上节课的基础,相信接下来的学习过程会事半功倍。 本文章会侧重于详细分析不同场景下 Netty 内存分配和回收的实现过程,让你对 Netty 内存池的整体设计有一个更加清晰的认识。 内存分配实现原理 Netty 中负责线程分配的组件有两个:PoolArena和PoolThreadCache。PoolArena 是多个线程共享的,每个线程会固定绑定一个 PoolArena,PoolThreadCache 阅读详情
内存回收

netty内部是通过计数来实现内存的回收的,具体的实现是在AbstractReferenceCountedByteBuf中。所以我们在使用完pooledBytebuf后,要记得调用release方法。

我们跟踪release方法看到会执行到io.netty.buffer.PooledByteBuf#deallocate方法

protected final void deallocate() {
        if (handle >= 0) {//唯一指定一段内存
            final long handle = this.handle;
            this.handle = -1;
            memory = null;
            chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
            tmpNioBuf = null;
            chunk = null;
            //回收对象
            recycle();
        }
    }

这里会调用chunk所属arena的free方法,我们继续跟进

void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
        //如果是非池化的
        if (chunk.unpooled) {
            int size = chunk.chunkSize();
            //释放内存
            destroyChunk(chunk);
            activeBytesHuge.add(-size);
            deallocationsHuge.increment();
        } else {
            //如果是池化的查看是什么类型
            SizeClass sizeClass = sizeClass(normCapacity);
            //放入队列中下次继续复用
            if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
                // cached so not free it.
                return;
            }
            //如果队列满了或者其他原因导致加入队列失败则标记这个内存未被使用
            freeChunk(chunk, handle, sizeClass, nioBuffer, false);
        }
    }

这里如果是非池化的的直接释放内存

protected void destroyChunk(PoolChunk<ByteBuffer> chunk) {
            //通过unsafe释放内存
            if (PlatformDependent.useDirectBufferNoCleaner()) {
                PlatformDependent.freeDirectNoCleaner(chunk.memory);
            } else {
                //通过cleaner释放直接内存
                PlatformDependent.freeDirectBuffer(chunk.memory);
            }
        }

直接内存通过两种方式释放堆外内存,一种是使用cleaner执行clean方法释放内存,一种是执行unsafe方法释放内存。如果是堆内内存则可以等待GC释放内存。

如果是池化的内存,判断当时申请时候的类型,然后将这块内存放入PoolThreadCache中等待下次使用。

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

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

public final boolean add(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle) {
            //从对象池中获取一个entry,并且将所有的chunk、内存对象和handle信息进行赋值
            Entry<T> entry = newEntry(chunk, nioBuffer, handle);
            //加入队列中
            boolean queued = queue.offer(entry);
            //如果放入失败将entry回收
            if (!queued) {
                // If it was not possible to cache the chunk, immediately recycle the entry
                entry.recycle();
            }

            return queued;
        }

这里就是将entry放入到线程本地缓存的MemoryRegionCache的队列中,等下次复用

如果队列满了或者其他原因导致加入队列失败则标记这个内存未被使用

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);
        }
        //如果高峰时期申请了很多内存特别是堆外内存,现在使用率慢慢下来了,当poolChunk使用率为0了,要将内存释放
        if (destroyChunk) {
            // destroyChunk not need to be called while holding the synchronized lock.
            destroyChunk(chunk);
        }
    }

我们跟踪到free方法

boolean free(PoolChunk<T> chunk, long handle, ByteBuffer nioBuffer) {
        chunk.free(handle, nioBuffer);
        //如果整个chunk的使用率下降到当前这个chunkList的最小值了就移动这个chunk到符合使用率的chunkList中
        if (chunk.usage() < minUsage) {
            remove(chunk);
            // Move the PoolChunk down the PoolChunkList linked-list.
            return move0(chunk);
        }
        return true;
    }

继续跟进

void free(long handle, ByteBuffer nioBuffer) {
        int memoryMapIdx = memoryMapIdx(handle);//二叉树中位置
        int bitmapIdx = bitmapIdx(handle);//位图信息
        //如果是subpage
        if (bitmapIdx != 0) { // free a subpage
            //找到在PoolSubpage数组中的位置
            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.
            //找到tinySubpagePools中的head
            PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
            synchronized (head) {
                //bitmapIdx & 0x3FFFFFFF:表示属于第一个subpage
                if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
                    return;
                }
            }
        }
        //如果释放的是page级别的
        //修改可用字节数
        freeBytes += runLength(memoryMapIdx);
        //修改内存管理数组memoryMap的值,设置为没有占用
        setValue(memoryMapIdx, depth(memoryMapIdx));
        //修改父节点,和分配是一个反向过程
        updateParentsFree(memoryMapIdx);

        if (nioBuffer != null && cachedNioBuffers != null &&
                cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
            cachedNioBuffers.offer(nioBuffer);
        }
    }

继续跟进如果是释放subpage基本的内存,跟踪subpage.free方法

boolean free(PoolSubpage<T> head, int bitmapIdx) {
        if (elemSize == 0) {
            return true;
        }
        //在bitmap数组中的索引位置
        int q = bitmapIdx >>> 6;
        //相对第几个subpage
        int r = bitmapIdx & 63;
        assert (bitmap[q] >>> r & 1) != 0;
        //将这个位置设置为0
        bitmap[q] ^= 1L << r;
        //设置下一个可用位置
        setNextAvail(bitmapIdx);
        //head指向下一个
        if (numAvail ++ == 0) {
            addToPool(head);
            return true;
        }
        //表示subpage还在被占用
        if (numAvail != maxNumElems) {
            return true;
        } else {
            //如果只剩下一个标记分配大小的head节点直接返回
            if (prev == next) {
                return true;
            }

            //pool中有别的poolSubPage
            doNotDestroy = false;
            //从tinyPoolSubpagePools[]数组上移除
            removeFromPool();
            return false;
        }
    }

这里根据bitmapIdx计算在位图数组中的位置,然后将bit为重新设置为0,也就是没有被占用状态,然后设置下一个可用位置就是当前释放的位置,如果可用大小和最大可分配内容不相同则直接返回,说明这个poolSubpage还在被占用,如果相等,查看是不是就一个head节点,如果是也直接返回,如果不是可能是别的poolSubpage加入了pool,则将释放的subpage移除。

下面我们看看如果释放内存是大于一个page的情况

//如果释放的是page级别的
        //修改可用字节数
        freeBytes += runLength(memoryMapIdx);
        //修改内存管理数组memoryMap的值,设置为没有占用
        setValue(memoryMapIdx, depth(memoryMapIdx));
        //修改父节点,和分配是一个反向过程
        updateParentsFree(memoryMapIdx);

        if (nioBuffer != null && cachedNioBuffers != null &&
                cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
            cachedNioBuffers.offer(nioBuffer);
        }

首先更新可用内存大小,然后更新内存管理数组memoryMap中的id的位置为可用状态,然后循环向上更新父节点

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));
            } else {
                //如果兄弟节点还被占用,则跟新父节点的值为刚刚释放的id的值
                byte val = val1 < val2 ? val1 : val2;//去小的值
                setValue(parentId, val);
            }
            //向上传递,继续跟新这颗树
            id = parentId;
        }
    }

这里其实就是分配过程的反过程,更新内存管理数组memoryMap。我们继续回到PoolChunkList#free方法

boolean free(PoolChunk<T> chunk, long handle, ByteBuffer nioBuffer) {
        chunk.free(handle, nioBuffer);
        //如果整个chunk的使用率下降到当前这个chunkList的最小值了就移动这个chunk到符合使用率的chunkList中
        if (chunk.usage() < minUsage) {
            remove(chunk);
            return move0(chunk);
        }
        return true;
    }

private boolean move0(PoolChunk<T> chunk) {
        //前面已经没有poolChunkList了
        if (prevList == null) {
            assert chunk.usage() == 0;//使用率为0
            return false;
        }
        return prevList.move(chunk);
    }

如果释放后使用率低于最小值了,将poolChunk从当前的poolChunkList移除,移动到前一个中去,如果前边一个list为null了,也就是移动到了q000了,并且使用率为0,则说明这个poolChunk没有任何占用可以移除了。回到

io.netty.buffer.PoolArena#freeChunk方法执行销毁chunk操作。

//如果高峰时期申请了很多内存特别是堆外内存,现在使用率慢慢下来了,并且poolChunk使用率为0了,要将内存释放
if (destroyChunk) {
    // destroyChunk not need to be called while holding the synchronized lock.
    destroyChunk(chunk);
}

所以我们在使用池化内存的时候切记要执行release操作

第二章 深入理解 Netty ByteBuf:组成、读写、内存释放与头尾释放源码解析 摘要: 本文深入解析Netty中的ByteBuf核心机制,涵盖其结构设计、读写操作、内存管理和释放策略。ByteBuf通过分离读写指针解决ByteBuffer的缺陷,支持堆/堆外内存及池化分配。文章详细介绍了写入、读取、查看操作的方法与注意事项,强调引用计数机制在内存释放中的关键作用,并对比分析了discardReadBytes()和clear()的底层实现差异。最后通过Echo服务器示例演示实践应用,帮助开发者高效使用ByteBuf并避免内存泄漏。 阅读详情

相关推荐

基于STM32的电子时钟设计

基于STM32的电子时钟设计1、设计目的2、设计具体要求3、proteus仿真图4、程序代码 1、设计目的 (1)掌握使用KEIL MDK5进行软件开发的具体流程; (2)掌握硬件电路的设计方法; (3)掌握STM32微控制器的程序设计及代码编写方法。 2、设计具体要求 (1)通过STM32自带的实时时钟功能获取时间信息; (2)在OLED显示屏上显示日期、时、分、秒等时间信息; (3)通过按键可对时间信息进行修改; (4)具有闹钟设定功能; (5)加分项:自行增加其他功能。 3、proteus仿真图 4

唯一的博客 2万+

Netty学习:Netty对于堆外内存的管理与回收

对于Netty对堆外内存的管理以及回收机制的个人总结

m0_55755849的博客 2893

交叉编译Qt6.5.3(RK3568开发板)

注意,如果使用其它版本的交叉编译器的话,请确保你的交叉编译器能够支持 C++17标准。为确保后续交叉编译没问题,需要确保开发板使用的GLIBC版本不小于2.34版本。到 linaro 网站上下载交叉编译工具,我下载的是 gcc-linaro-11.3.1-2022.06-x86_64_aarch64-linux-gnu.tar.xz。安装 CMake ,最低要求3.16版本,如果要静态编译,需要3.21及以上版本。我使用的3.27版本。:指定为最后交叉编译后的Qt6.5.3库的安装目录。

m0_46309425的博客 5100

Netty之ByteBuf介绍(三)

Netty之ByteBuf介绍(三)

weixin_38598961的博客 918

Codex CLI 接入智谱 GLM-5.1 完整教程

Codex CLI 是 OpenAI 推出的命令行 AI 编程工具,支持自主读取文件、执行命令、编写代码等操作,是目前最火的终端 AI 编程工具之一。通过 CLIProxyAPI 代理,我们成功将 Codex CLI 与智谱 GLM-5.1 模型对接,核心在于解决 Responses API 与 Chat Completions API 的协议转换问题。✅成熟稳定:CLIProxyAPI 社区活跃(4.7k fork),持续维护✅完整兼容:支持流式/非流式、function calling✅低成本。

u011831527的博客 2573

Dify 本地部署教程

到这里,Dify 就安装完成了,更多操作设置可以参考官方文档,Dify 和 FastGPT 基本上差不多,也可以同时安装一下 FastGPT 进行体验~Docker Compose 部署 | Dify。

Muti-Agent 6645

9. Netty中内存分配和回收,PooledByteBufAllocator,PoolArena,PoolThreadCache,PoolChunk,PoolSubpage

当客户端建立连接后,客户端向服务端发送消息,服务端需要读取消息,一般在常见的在worker线程中会有如下操作: NioByteUnsafe AbstractNioByteChannel 进行实际read public final void read() { final ChannelConfig config = config(); ...

LeoHan 1203

【EB-03】 AUTOSAR builder与EB RTE集成

打开EB工具后,在EB的步骤是:执行MultiTask(2)-> Mapping Component to Partitions(4)->Task Mapping(5)-> SvcAs_Trigger(7)-> Code Generation(8)如涉及新增BSW Module,也需要重复以上步骤。

libuping2023的博客 960

高并发网络通信Netty之内存池机制设计及参数调优

介绍Netty内存池设计机制、常见参数调优及应用场景

技术改变命运,创新驱动中国 1128

构建AI Agent驱动的智能健康管理系统

数据碎片化:健康数据分散在不同的设备和平台中,难以统一管理和分析。个性化不足:健康管理方案通常是通用的,难以根据个人的健康状况实时调整。用户参与度低:用户难以获得及时、个性化的健康建议,导致健康管理效果不佳。AI Agent是一个能够感知环境、采取行动以实现目标的智能实体。根据智能体的智能水平,可以分为简单反应式智能体和复杂适应式智能体。实时监测健康数据。获取个性化的健康建议。收到健康风险预警。

AI天才研究院 749

YOLO26改进策略【卷积层】| HWD,引入`Haar小波变换`到下采样模块中,减少信息丢失

本文记录的是。传统的卷积神经网络中常用的最大池化平均池化和步长为2的卷积等操作进行下采样可能会导致信息丢失,为了解决信息丢失问题,HWD作者受无损信息变换方法的启发,引入Haar小波变换到下采样模块中,

Limiiiing的博客 344

netty源码浅析-byteBuf介绍

byteBuf byteBuf介绍 在介绍netty的byteBuf之前我们先来回顾一下nio的byteBuffer,byteBuffer内部维护了一个byte[]数组,而且提供了几个属性position、limit、capacity、mark四个属性来组织byteBuffer。 position:当前操作的位置 limit:最大可以读或者可写的位置 capacity:最大的空间容量是多少,byteBuffer在创建时,就固定下来 mark:标记当前的位置,下次可以回到这个位置 ByteBuffer b

GeekerJava的博客 172

华为OD技术面八股_C++_05

汇总华为OD技术面C++方向技术面八股文,提供详细解答。

qq_45776114的博客 413

netty源码浅析-PooledByteBuf扩容操作

在AbstractByteBuf中会有很多调用ensureWritable0的操作,判断需不需要扩容,我们跟踪到这个方法中,如果需要写入的空间比可写内存小则直接返回,不需要扩容,如果不是则重新计算需要扩容的长度。这里会先判断需要扩容的大小不能超过最大值,如果大小超过了4M,则每次向上取整获取扩容的大小,如果小于4M,则以64开始每次翻一倍,获取大于minNewCapacity的扩容值。我们上面分析了分配操作和回收操作,部分代码我们在上面已经分析过了,现在我们看下扩容操作。

GeekerJava的博客 249

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

Netty学习之旅----源码分析内存分配与释放原理,java基础面试集合

subpage = new PoolSubpage(this, id, runOffset(id), pageSize, normCapacity); // 代码@3 subpages[subpageIdx] = subpage; } else { //@4 subpage.init(normCapacity); } return subpage.allocate(); //代码@5 } 首先讲解一下整体分配思路,先根据PoolChunk内部维护的各个PoolChunk的占用情况,返回一个可以PoolChu

m0_63176399的博客 565

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

Netty内存管理详解

前言 正是Netty的易用性和高性能成就了Netty,让其能够如此流行。 而作为一款通信框架,首当其冲的便是对IO性能的高要求。 不少读者都知道Netty底层通过使用Direct Memory,减少了内核态与用户态之间的内存拷贝,加快了IO速率。但是频繁的向系统申请Direct Memory,并在使用完成后释放本身就是一件影响性能的事情。为此,Netty内部实现了一套自己的内存管理机制,在申请时,Netty会一次性向操作系统申请较大的一块内存,然后再将大内存进行管理,按需拆分成小块分配。而释放时,Nett

Felix_阳的博客 2097

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Netty堆外内存泄漏如何应对?

文章目录一、背景二、Netty堆外内存回收机制什么是堆外内存?Netty堆外内存回收机制具体如何?如何回收?三、堆外内存监控配置反射监控堆外内存服务器上监控堆外内存四、其他案例 一、背景 开发过程中,我们遇到了堆外内存泄漏的问题——服务会出现假死现象(看似服务进程还跑着,但是没有业务响应)。 查看logback的error日志中记录下下面的信息: [location-center:192.168.5.14:8017] [,] 2021-07-27 09:38:39.640 ERROR 53950 [lett

子涵先生 2117

Netty(28)Netty的内存管理和垃圾回收机制是如何工作的?

Netty通过高效的ByteBuf内存管理机制提升性能,包括引用计数和内存池技术。其支持堆内存和直接内存两种ByteBuf类型,通过PooledByteBufAllocator实现内存分配和回收,采用Arena、Chunk、Page等分级结构优化管理。引用计数机制确保内存及时释放,同时提供内存泄漏检测功能。这些机制共同降低了GC压力,保障了Netty的高性能网络通信能力。

qq_43012298的博客 840

Netty内存分配

文章目录基本概念PooledByteBufAllocatornewDirectBufferPoolThreadCacheinitCachePoolArenaPoolChunkListPoolChunkmemoryMapdepthMapsubpagesPoolSubpagebitmap分配原理★遵循的原则PoolThreadCache分配内存PoolChunk里面分配内存allocateallocateRunallocateNodeallocateSubpagePoolSubpage#allocate回收原理

weixin_42260270的博客 1588

Netty之内存池的原理和实战

Netty的内存池机制通过精细化的内存管理,显著提高了内存的利用率,减少了内存分配和回收的开销,特别适用于高并发、高负载的网络应用。理解Netty的内存池原理,并结合实际需求进行优化配置,能够有效提升系统性能和稳定性。在实际应用中,开发者应根据业务需求、系统资源和负载情况,灵活调整内存池的大小、内存分配策略以及高低水位线等参数,从而获得最佳的系统表现。

HaSaKing的博客 980

Netty ByteBuf的回收

以下三种情况下对于ByteBuf的回收策略: ChannelOutboundHandler中write的ByteBuf,在pipeline中的tail handler将负责释放,详见AbstractChannelHandlerContext中的如下函数 private void write(Object msg, boolean flush, ChannelPromise promise) { ObjectUtil.checkNotNull(msg, "msg");

kakaweb的博客 783

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)

上文描述过,ByteBuf根据内存分可以分为堆内存和堆外内存实现,此处以非池化实现介绍,两个重要子类是UnpooledHeapByteBuf和UnpooledDirectByteBuf,因为堆外内存和堆内存除了内存分配之外,其他实现十分相似,先简单描述下堆内存实现,然后重点分析堆外内存的分配及回收策略

走在路上 1932

netty中的bytebuf(脱离JVM的垃圾收集实现手动回收)及引用和内存泄漏

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_33872566的博客 1383

Netty学习之旅----源码分析内存分配与释放原理

if (~bits!代码@2:遍历bitmap,根据当前的elmSize可以得知当前用来多少个long来代表总的长度。代码@3:如果取反不为0,则说明用这个long代表的64个PoolSubpage未使用完,可用从这里获取一个,故进入到代码@4。//@5j < 64;j ++) { //遍历,一个long总代表64个PoolSubpage//@8return val;

2301_82242988的博客 867

Netty内存池ByteBuf 内存回收

内存池ByteBuf 内存回收:   在前面的章节中我们有提到, 堆外内存是不受JVM 垃圾回收机制控制的, 所以我们分配一块堆外内存进行ByteBuf 操作时, 使用完毕要对对象进行回收, 本节就以PooledUnsafeDirectByteBuf 为例讲解有关内存分配的相关逻辑。PooledUnsafeDirectByteBuf 中内存释放的入口方法是其父类AbstractRefe...

weixin_30908941的博客 313

Netty中ByteBuf的回收机制ReferenceCounted

Netty中ByteBuf采用引用计数的方式进行资源的回收,当ByteBuf中的引用计数值为0时,Netty则不允许应用程序采用任何的方式对这个资源进行访问。它通过实现ReferenceCounted维护引用计数。package io.netty.util; /** * A reference-counted object that requires explicit deallocation...

u013828625的博客 1053
上一篇: netty源码浅析-池化内存直接申请
下一篇: netty源码浅析-PooledByteBuf扩容操作
胖柯G
博客等级 码龄10年 453粉丝 57原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值