溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Netty內(nèi)存管理怎么理解

發(fā)布時(shí)間:2022-01-06 09:13:26 來(lái)源:億速云 閱讀:159 作者:iii 欄目:大數(shù)據(jù)

本篇內(nèi)容主要講解“Netty內(nèi)存管理怎么理解”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“Netty內(nèi)存管理怎么理解”吧!

前言

正是Netty的易用性和高性能成就了Netty,讓其能夠如此流行。
而作為一款通信框架,首當(dāng)其沖的便是對(duì)IO性能的高要求。
不少讀者都知道Netty底層通過(guò)使用Direct Memory,減少了內(nèi)核態(tài)與用戶(hù)態(tài)之間的內(nèi)存拷貝,加快了IO速率。但是頻繁的向系統(tǒng)申請(qǐng)Direct Memory,并在使用完成后釋放本身就是一件影響性能的事情。為此,Netty內(nèi)部實(shí)現(xiàn)了一套自己的內(nèi)存管理機(jī)制,在申請(qǐng)時(shí),Netty會(huì)一次性向操作系統(tǒng)申請(qǐng)較大的一塊內(nèi)存,然后再將大內(nèi)存進(jìn)行管理,按需拆分成小塊分配。而釋放時(shí),Netty并不著急直接釋放內(nèi)存,而是將內(nèi)存回收以待下次使用。
這套內(nèi)存管理機(jī)制不僅可以管理Directory Memory,同樣可以管理Heap Memory。

內(nèi)存的終端消費(fèi)者——ByteBuf

這里,我想向讀者們強(qiáng)調(diào)一點(diǎn),ByteBuf和內(nèi)存其實(shí)是兩個(gè)概念,要區(qū)分理解。
ByteBuf是一個(gè)對(duì)象,需要給他分配一塊內(nèi)存,它才能正常工作。
而內(nèi)存可以通俗的理解成我們操作系統(tǒng)的內(nèi)存,雖然申請(qǐng)到的內(nèi)存也是需要依賴(lài)載體存儲(chǔ)的:堆內(nèi)存時(shí),通過(guò)byte[], 而Direct內(nèi)存,則是Nio的ByteBuffer(因此Java使用Direct Memory的能力是JDK中Nio包提供的)。
為什么要強(qiáng)調(diào)這兩個(gè)概念,是因?yàn)镹etty的內(nèi)存池(或者稱(chēng)內(nèi)存管理機(jī)制)涉及的是針對(duì)內(nèi)存的分配和回收,而Netty的ByteBuf的回收則是另一種叫做對(duì)象池的技術(shù)(通過(guò)Recycler實(shí)現(xiàn))。
雖然這兩者總是伴隨著一起使用,但這二者是獨(dú)立的兩套機(jī)制??赡艽嬖谥炒蝿?chuàng)建ByteBuf時(shí),ByteBuf是回收使用的,而內(nèi)存卻是新向操作系統(tǒng)申請(qǐng)的。也可能存在某次創(chuàng)建ByteBuf時(shí),ByteBuf是新創(chuàng)建的,而內(nèi)存卻是回收使用的。
因?yàn)閷?duì)于一次創(chuàng)建過(guò)程而言,可以分成三個(gè)步驟:

  1. 獲取ByteBuf實(shí)例(可能新建,也可能是之間緩存的)

  2. 向Netty內(nèi)存管理機(jī)制申請(qǐng)內(nèi)存(可能新向操作系統(tǒng)申請(qǐng),也可能是之前回收的)

  3. 將申請(qǐng)到的內(nèi)存分配給ByteBuf使用

本文只關(guān)注內(nèi)存的管理機(jī)制,因此不會(huì)過(guò)多的對(duì)對(duì)象回收機(jī)制做解釋。

Netty中內(nèi)存管理的相關(guān)類(lèi)

Netty中與內(nèi)存管理相關(guān)的類(lèi)有很多??蚣軆?nèi)部提供了PoolArena,PoolChunkList,PoolChunk,PoolSubpage等用來(lái)管理一塊或一組內(nèi)存。
而對(duì)外,提供了ByteBufAllocator供用戶(hù)進(jìn)行操作。
接下來(lái),我們會(huì)先對(duì)這幾個(gè)類(lèi)做一定程度的介紹,在通過(guò)ByteBufAllocator了解內(nèi)存分配和回收的流程。
為了篇幅和可讀性考慮,本文不會(huì)涉及到大量很詳細(xì)的代碼說(shuō)明,而主要是通過(guò)圖輔之必要的代碼進(jìn)行介紹。
針對(duì)代碼的注解,可以見(jiàn)我GitHub上的netty項(xiàng)目。

PoolChunck——Netty向OS申請(qǐng)的最小內(nèi)存

上文已經(jīng)介紹了,為了減少頻繁的向操作系統(tǒng)申請(qǐng)內(nèi)存的情況,Netty會(huì)一次性申請(qǐng)一塊較大的內(nèi)存。而后對(duì)這塊內(nèi)存進(jìn)行管理,每次按需將其中的一部分分配給內(nèi)存使用者(即ByteBuf)。這里的內(nèi)存就是PoolChunk,其大小由ChunkSize決定(默認(rèn)為16M,即一次向OS申請(qǐng)16M的內(nèi)存)。

Page——PoolChunck所管理的最小內(nèi)存單位

PoolChunk所能管理的最小內(nèi)存叫做Page,大小由PageSize(默認(rèn)為8K),即一次向PoolChunk申請(qǐng)的內(nèi)存都要以Page為單位(一個(gè)或多個(gè)Page)。
當(dāng)需要由PoolChunk分配內(nèi)存時(shí),PoolChunk會(huì)查看通過(guò)內(nèi)部記錄的信息找出滿(mǎn)足此次內(nèi)存分配的Page的位置,分配給使用者。

PoolChunck如何管理Page

我們已經(jīng)知道PoolChunk內(nèi)部會(huì)以Page為單位組織內(nèi)存,同樣以Page為單位分配內(nèi)存。
那么PoolChunk要如何管理才能兼顧分配效率(指盡可能快的找出可分配的內(nèi)存且保證此次分配的內(nèi)存是連續(xù)的)和使用效率(盡可能少的避免內(nèi)存浪費(fèi),做到物盡其用)的?
Netty采用了Jemalloc的想法。
首先PoolChunk通過(guò)一個(gè)完全二叉樹(shù)來(lái)組織內(nèi)部的內(nèi)存。以默認(rèn)的ChunkSize為16M, PageSize為8K為例,一個(gè)PoolChunk可以劃分成2048個(gè)Page。將這2048個(gè)Page看作是葉子節(jié)點(diǎn)的寬度,可以得到一棵深度為11的樹(shù)(2^11=2048)。
我們讓每個(gè)葉子節(jié)點(diǎn)管理一個(gè)Page,那么其父節(jié)點(diǎn)管理的內(nèi)存即為兩個(gè)Page(其父節(jié)點(diǎn)有左右兩個(gè)葉子節(jié)點(diǎn)),以此類(lèi)推,樹(shù)的根節(jié)點(diǎn)管理了這個(gè)PoolChunk所有的Page(因?yàn)樗械娜~子結(jié)點(diǎn)都是其子節(jié)點(diǎn)),而樹(shù)中某個(gè)節(jié)點(diǎn)所管理的內(nèi)存大小即是以該節(jié)點(diǎn)作為根的子樹(shù)所包含的葉子節(jié)點(diǎn)管理的全部Page。
這樣做的好處就是當(dāng)你需要內(nèi)存時(shí),很快可以找到從何處分配內(nèi)存(你只需要從上往下找到所管理的內(nèi)存為你需要的內(nèi)存的節(jié)點(diǎn),然后將該節(jié)點(diǎn)所管理的內(nèi)存分配出去即可),并且所分配的內(nèi)存還是連續(xù)的(只要保證相鄰葉子節(jié)點(diǎn)對(duì)應(yīng)的Page是連續(xù)的即可)。
Netty內(nèi)存管理怎么理解

上圖中編號(hào)為512的節(jié)點(diǎn)管理了4個(gè)Page,為Page0, Page1, Page2, Page3(因?yàn)槠湎旅嬗兴膫€(gè)葉子節(jié)點(diǎn)2048,2049,2050, 2051)。
而編號(hào)為1024的節(jié)點(diǎn)管理了2個(gè)Page,為Page0和Page1(其對(duì)應(yīng)的葉子節(jié)點(diǎn)為Page0和Page1)。
當(dāng)需要分配32K的內(nèi)存時(shí),只需要將編號(hào)512的節(jié)點(diǎn)分配出去即可(512分配出去后會(huì)默認(rèn)其下所有子節(jié)點(diǎn)都不能分配)。而當(dāng)需要分配16K的內(nèi)存時(shí),只需要將編號(hào)1024的節(jié)點(diǎn)分配出去即可(一旦節(jié)點(diǎn)1024被分配,下面的2048和2049都不允許再被分配)。

了解了PoolChunk內(nèi)部的內(nèi)存管理機(jī)制后,讀者可能會(huì)產(chǎn)生幾個(gè)問(wèn)題:

  • PoolChunk內(nèi)部如何標(biāo)記某個(gè)節(jié)點(diǎn)已經(jīng)被分配?

  • 當(dāng)某個(gè)節(jié)點(diǎn)被分配后,其父節(jié)點(diǎn)所能分配的內(nèi)存如何更新?即一旦節(jié)點(diǎn)2048被分配后,當(dāng)你再需要16K的內(nèi)存時(shí),就不能從節(jié)點(diǎn)1024分配,因?yàn)楝F(xiàn)在節(jié)點(diǎn)1024可用的內(nèi)存僅有8K。

為了解決以上這兩點(diǎn)問(wèn)題,PoolChunk都是內(nèi)部維護(hù)了的byte[] memeoryMap和byte[] depthMap兩個(gè)變量。
這兩個(gè)數(shù)組的長(zhǎng)度是相同的,長(zhǎng)度等于樹(shù)的節(jié)點(diǎn)數(shù)+1。因?yàn)樗鼈儼迅?jié)點(diǎn)放在了1的位置上。而數(shù)組中父節(jié)點(diǎn)與子節(jié)點(diǎn)的位置關(guān)系為:

假設(shè)parnet的下標(biāo)為i,則子節(jié)點(diǎn)的下標(biāo)為2i和2i+1

用數(shù)組表示一顆二叉樹(shù),你們是不是想到了堆這個(gè)數(shù)據(jù)結(jié)構(gòu)。

已經(jīng)知道了兩個(gè)數(shù)組都是表示二叉樹(shù),且數(shù)組中的每個(gè)元素可以看成二叉樹(shù)的節(jié)點(diǎn)。那么再來(lái)看看元素的值分別代碼什么意思。
對(duì)于depthMap而言,該值就代表該節(jié)點(diǎn)所處的樹(shù)的層數(shù)。例如:depthMap[1] == 1,因?yàn)樗歉?jié)點(diǎn),而depthMap[2] = depthMap[3] = 2,表示這兩個(gè)節(jié)點(diǎn)均在第二層。由于樹(shù)一旦確定后,結(jié)構(gòu)就不在發(fā)生改變,因此depthMap在初始化后,各元素的值也就不發(fā)生變化了。

而對(duì)于memoryMap而言,其值表示該節(jié)點(diǎn)下可用于完整內(nèi)存分配的最小層數(shù)(或者說(shuō)最靠近根節(jié)點(diǎn)的層數(shù))。
這話理解起來(lái)可能有點(diǎn)別扭,還是用上文的例子為例 。
首先在內(nèi)存都未分配的情況下,每個(gè)節(jié)點(diǎn)所能分配的內(nèi)存大小就是該層最初始的狀態(tài)(即memoryMap的初始狀態(tài)和depthMap的一致的)。而一旦其有個(gè)子節(jié)點(diǎn)被分配出后去,父節(jié)點(diǎn)所能分配的完整內(nèi)存(完整內(nèi)存是指該節(jié)點(diǎn)所管理的連續(xù)的內(nèi)存塊,而非該節(jié)點(diǎn)剩余的內(nèi)存大小)就減小了(內(nèi)存的分配和回收會(huì)修改關(guān)聯(lián)的mermoryMap中相關(guān)節(jié)點(diǎn)的值)。
譬如,節(jié)點(diǎn)2048被分配后,那么對(duì)于節(jié)點(diǎn)1024來(lái)說(shuō),能完整分配的內(nèi)存(原先為16K)就已經(jīng)和編號(hào)2049節(jié)點(diǎn)(其右子節(jié)點(diǎn))相同(減為了8K),換句話說(shuō)節(jié)點(diǎn)1024的能力已經(jīng)退化到了2049節(jié)點(diǎn)所在的層節(jié)點(diǎn)所擁有的能力。
這一退化可能會(huì)影響所有的父節(jié)點(diǎn)。
而此時(shí),512節(jié)點(diǎn)能分配的完整內(nèi)存是16K,而非24K(因?yàn)閮?nèi)存分配都是按2的冪進(jìn)行分配,盡管一個(gè)消費(fèi)者真實(shí)需要的內(nèi)存可能是21K,但是Netty的內(nèi)存管理機(jī)制會(huì)直接分配32K的內(nèi)存)。

但是這并不是說(shuō)節(jié)點(diǎn)512管理的另一個(gè)8K內(nèi)存就浪費(fèi)了,8K內(nèi)存還可以用來(lái)在申請(qǐng)內(nèi)存為8K的時(shí)候分配。

用圖片演示PoolChunk內(nèi)存分配的過(guò)程。其中value表示該節(jié)點(diǎn)在memoeryMap的值,而depth表示該節(jié)點(diǎn)在depthMap的值。
第一次內(nèi)存分配,申請(qǐng)者實(shí)際需要6K的內(nèi)存:
Netty內(nèi)存管理怎么理解

這次分配造成的后果是其所有父節(jié)點(diǎn)的memoryMap的值都往下加了一層。
之后申請(qǐng)者需要申請(qǐng)12K的內(nèi)存:
Netty內(nèi)存管理怎么理解

由于節(jié)點(diǎn)1024已經(jīng)無(wú)法分配所需的內(nèi)存,而節(jié)點(diǎn)512還能夠分配,因此節(jié)點(diǎn)512讓其右節(jié)點(diǎn)再?lài)L試。

上述介紹的是內(nèi)存分配的過(guò)程,而內(nèi)存回收的過(guò)程就是上述過(guò)程的逆過(guò)程——回收后將對(duì)應(yīng)節(jié)點(diǎn)的memoryMap的值修改回去。這里不過(guò)多介紹。

PoolChunkList——對(duì)PoolChunk的管理

PoolChunkList內(nèi)部有一個(gè)PoolChunk組成的鏈表。通常一個(gè)PoolChunkList中的所有PoolChunk使用率(已分配內(nèi)存/ChunkSize)都在相同的范圍內(nèi)。
每個(gè)PoolChunkList有自己的最小使用率或者最大使用率的范圍,PoolChunkList與PoolChunkList之間又會(huì)形成鏈表,并且使用率范圍小的PoolChunkList會(huì)在鏈表中更加靠前。
而隨著PoolChunk的內(nèi)存分配和使用,其使用率發(fā)生變化后,PoolChunk會(huì)在PoolChunkList的鏈表中,前后調(diào)整,移動(dòng)到合適范圍的PoolChunkList內(nèi)。
這樣做的好處是,使用率的小的PoolChunk可以先被用于內(nèi)存分配,從而維持PoolChunk的利用率都在一個(gè)較高的水平,避免內(nèi)存浪費(fèi)。

PoolSubpage——小內(nèi)存的管理者

PoolChunk管理的最小內(nèi)存是一個(gè)Page(默認(rèn)8K),而當(dāng)我們需要的內(nèi)存比較小時(shí),直接分配一個(gè)Page無(wú)疑會(huì)造成內(nèi)存浪費(fèi)。
PoolSubPage就是用來(lái)管理這類(lèi)細(xì)小內(nèi)存的管理者。

小內(nèi)存是指小于一個(gè)Page的內(nèi)存,可以分為T(mén)iny和Smalll,Tiny是小于512B的內(nèi)存,而Small則是512到4096B的內(nèi)存。如果內(nèi)存塊大于等于一個(gè)Page,稱(chēng)之為Normal,而大于一個(gè)Chunk的內(nèi)存塊稱(chēng)之為Huge。

而Tiny和Small內(nèi)部又會(huì)按具體內(nèi)存的大小進(jìn)行細(xì)分。
對(duì)Tiny而言,會(huì)分成16,32,48...496(以16的倍數(shù)遞增),共31種情況。
對(duì)Small而言,會(huì)分成512,1024,2048,4096四種情況。
PoolSubpage會(huì)先向PoolChunk申請(qǐng)一個(gè)Page的內(nèi)存,然后將這個(gè)page按規(guī)格劃分成相等的若干個(gè)內(nèi)存塊(一個(gè)PoolSubpage僅會(huì)管理一種規(guī)格的內(nèi)存塊,例如僅管理16B,就將一個(gè)Page的內(nèi)存分成512個(gè)16B大小的內(nèi)存塊)。
每個(gè)PoolSubpage僅會(huì)選一種規(guī)格的內(nèi)存管理,因此處理相同規(guī)格的PoolSubpage往往是通過(guò)鏈表的方式組織在一起,不同的規(guī)格則分開(kāi)存放在不同的地方。
并且總是管理一個(gè)規(guī)格的特性,讓PoolSubpage在內(nèi)存管理時(shí)不需要使用PoolChunk的完全二叉樹(shù)方式來(lái)管理內(nèi)存(例如,管理16B的PoolSubpage只需要考慮分配16B的內(nèi)存,當(dāng)申請(qǐng)32B的內(nèi)存時(shí),必須交給管理32B的內(nèi)存來(lái)處理),僅用 long[] bitmap (可以看成是位數(shù)組)來(lái)記錄所管理的內(nèi)存塊中哪些已經(jīng)被分配(第幾位就表示第幾個(gè)內(nèi)存塊)。
實(shí)現(xiàn)方式要簡(jiǎn)單很多。

PoolArena——內(nèi)存管理的統(tǒng)籌者

PoolArena是內(nèi)存管理的統(tǒng)籌者。
它內(nèi)部有一個(gè)PoolChunkList組成的鏈表(上文已經(jīng)介紹過(guò)了,鏈表是按PoolChunkList所管理的使用率劃分)。
此外,它還有兩個(gè)PoolSubpage的數(shù)組,PoolSubpage[] tinySubpagePools 和 PoolSubpage[] smallSubpagePools。
默認(rèn)情況下,tinySubpagePools的長(zhǎng)度為31,即存放16,32,48...496這31種規(guī)格的PoolSubpage(不同規(guī)格的PoolSubpage存放在對(duì)應(yīng)的數(shù)組下標(biāo)中,相同規(guī)格的PoolSubpage在同一個(gè)數(shù)組下標(biāo)中形成鏈表)。
同理,默認(rèn)情況下,smallSubpagePools的長(zhǎng)度為4,存放512,1024,2048,4096這四種規(guī)格的PoolSubpage。
PoolArena會(huì)根據(jù)所申請(qǐng)的內(nèi)存大小決定是找PoolChunk還是找對(duì)應(yīng)規(guī)格的PoolSubpage來(lái)分配。

值得注意的是,PoolArena在分配內(nèi)存時(shí),是會(huì)存在競(jìng)爭(zhēng)的,因此在關(guān)鍵的地方,PoolArena會(huì)通過(guò)sychronize來(lái)保證線程的安全。
Netty對(duì)這種競(jìng)爭(zhēng)做了一定程度的優(yōu)化,它會(huì)分配多個(gè)PoolArena,讓線程盡量使用不同的PoolArena,減少出現(xiàn)競(jìng)爭(zhēng)的情況。

PoolThreadCache——線程本地緩存,減少內(nèi)存分配時(shí)的競(jìng)爭(zhēng)

PoolArena免不了產(chǎn)生競(jìng)爭(zhēng),Netty除了創(chuàng)建多個(gè)PoolArena減少競(jìng)爭(zhēng)外,還讓線程在釋放內(nèi)存時(shí)緩存已經(jīng)申請(qǐng)過(guò)的內(nèi)存,而不立即歸還給PoolArena。
緩存的內(nèi)存被存放在PoolThreadCache內(nèi),它是一個(gè)線程本地變量,因此是線程安全的,對(duì)它的訪問(wèn)也不需要上鎖。
PoolThreadCache內(nèi)部是由MemeoryRegionCache的緩存池(數(shù)組),同樣按等級(jí)可以分為T(mén)iny,Small和Normal(并不緩存Huge,因?yàn)镠uge效益不高)。
其中Tiny和Small這兩個(gè)等級(jí)下的劃分方式和PoolSubpage的劃分方式相同,而Normal因?yàn)榻M合太多,會(huì)有一個(gè)參數(shù)控制緩存哪些規(guī)格(例如,一個(gè)Page, 兩個(gè)Page和四個(gè)Page等...),不在Normal緩存規(guī)格內(nèi)的內(nèi)存塊將不會(huì)被緩存,直接還給PoolArena。
再看MemoryRegionCache, 它內(nèi)部是一個(gè)隊(duì)列,同一隊(duì)列內(nèi)的所有節(jié)點(diǎn)可以看成是該線程使用過(guò)的同一規(guī)格的內(nèi)存塊。同時(shí),它還有個(gè)size屬性控制隊(duì)列過(guò)長(zhǎng)(隊(duì)列滿(mǎn)后,將不在緩存該規(guī)格的內(nèi)存塊,而是直接還給PoolArena)。
當(dāng)線程需要內(nèi)存時(shí),會(huì)先從自己的PoolThreadCache中找對(duì)應(yīng)等級(jí)的緩存池(對(duì)應(yīng)的數(shù)組)。然后再?gòu)臄?shù)組中找出對(duì)應(yīng)規(guī)格的MemoryRegionCache。最后從其隊(duì)列中取出內(nèi)存塊進(jìn)行分配。

Netty內(nèi)存機(jī)構(gòu)總覽和PooledByteBufAllocator申請(qǐng)內(nèi)存步驟

在了解了上述這么多概念后,通過(guò)一張圖給讀者加深下印象。
Netty內(nèi)存管理怎么理解

上圖僅詳細(xì)畫(huà)了針對(duì)Heap Memory的部分,Directory Memory也是類(lèi)似的。

最后在由PooledByteBufAllocator作為入口,重頭梳理一遍內(nèi)存申請(qǐng)的過(guò)程:

  1. PooledByteBufAllocator.newHeapBuffer()開(kāi)始申請(qǐng)內(nèi)存

  2. 獲取線程本地的變量PoolThreadCache以及和線程綁定的PoolArena

  3. 通過(guò)PoolArena分配內(nèi)存,先獲取ByteBuf對(duì)象(可能是對(duì)象池回收的也可能是創(chuàng)建的),在開(kāi)始內(nèi)存分配

  4. 分配前先判斷此次內(nèi)存的等級(jí),嘗試從PoolThreadCache的找相同規(guī)格的緩存內(nèi)存塊使用,沒(méi)有則從PoolArena中分配內(nèi)存

  5. 對(duì)于Normal等級(jí)內(nèi)存而言,從PoolChunkList的鏈表中找合適的PoolChunk來(lái)分配內(nèi)存,如果沒(méi)有則先像OS申請(qǐng)一個(gè)PoolChunk,在由PoolChunk分配相應(yīng)的Page

  6. 對(duì)于Tiny和Small等級(jí)的內(nèi)存而言,從對(duì)應(yīng)的PoolSubpage緩存池中找內(nèi)存分配,如果沒(méi)有PoolSubpage,線會(huì)到第5步

到此,相信大家對(duì)“Netty內(nèi)存管理怎么理解”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢(xún),關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI