Spark 內(nèi)存管理的前世今生(上)

歡迎關(guān)注我的微信公眾號(hào):FunnyBigData

作為打著 “內(nèi)存計(jì)算” 旗號(hào)出道的 Spark,內(nèi)存管理是其非常重要的模塊。作為使用者,搞清楚 Spark 是如何管理內(nèi)存的,對我們編碼、調(diào)試及優(yōu)化過程會(huì)有很大幫助。本文之所以取名為 "Spark 內(nèi)存管理的前世今生" 是因?yàn)樵?Spark 1.6 中引入了新的內(nèi)存管理方案,而在之前一直使用舊方案。

剛剛提到自 1.6 版本引入了新的內(nèi)存管理方案,但并不是說在 1.6 及之后的版本中不能使用舊的方案,而是默認(rèn)使用新方案。我們可以通過設(shè)置 spark.memory.userLegacyMode 值來選擇,該值為 false 表示使用新方案,true 表示使用舊方案,默認(rèn)為 false。該值是如何發(fā)揮作用的呢?如下:

val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)
val memoryManager: MemoryManager =
  if (useLegacyMemoryManager) {
    new StaticMemoryManager(conf, numUsableCores)
  } else {
    UnifiedMemoryManager(conf, numUsableCores)
  }

根據(jù) spark.memory.useLegacyMode 值的不同,會(huì)創(chuàng)建 MemoryManager 不同子類的實(shí)例:

  • 值為 false:創(chuàng)建 UnifiedMemoryManager 類實(shí)例,為新的內(nèi)存管理的實(shí)現(xiàn)
  • 值為 true:創(chuàng)建 StaticMemoryManager類實(shí)例,為舊的內(nèi)存管理的實(shí)現(xiàn)

不管是在新方案中還是舊方案中,都根據(jù)內(nèi)存的不同用途,都包含三大塊。

  • storage 內(nèi)存:用于緩存 RDD、展開 partition、存放 Direct Task Result、存放廣播變量。在 Spark Streaming receiver 模式中,也用來存放每個(gè) batch 的 blocks
  • execution 內(nèi)存:用于 shuffle、join、sort、aggregation 中的緩存、buffer

storage 和 execution 內(nèi)存都通過 MemoryManager 來申請和管理,而另一塊內(nèi)存則不受 MemoryManager 管理,主要有兩個(gè)作用:

  • 在 spark 運(yùn)行過程中使用:比如序列化及反序列化使用的內(nèi)存,各個(gè)對象、元數(shù)據(jù)、臨時(shí)變量使用的內(nèi)存,函數(shù)調(diào)用使用的堆棧等
  • 作為誤差緩沖:由于 storage 和 execution 中有很多內(nèi)存的使用是估算的,存在誤差。當(dāng) storage 或 execution 內(nèi)存使用超出其最大限制時(shí),有這樣一個(gè)安全的誤差緩沖在可以大大減小 OOM 的概率

這塊不受 MemoryManager 管理的內(nèi)存,由系統(tǒng)預(yù)留以及 storage 和 execution 安全系數(shù)之外的內(nèi)存組成,這個(gè)會(huì)在下文中詳述。

接下來,讓我們先來看看 “前世”

前世

舊方案的內(nèi)存結(jié)構(gòu)如下圖所示:

讓我們結(jié)合上圖做進(jìn)一步說明:

execution 內(nèi)存

execution 最大可用內(nèi)存為 jvm space * spark.storage.memoryFraction * spark.storage.safetyFraction,默認(rèn)為 jvm space * 0.2 * 0.8。

spark.shuffle.memoryFraction 很大程度上影響了 spill 的頻率,如果 spill 過于頻繁,可以適當(dāng)增大 spark.shuffle.memoryFraction 的值,增加用于 shuffle 的內(nèi)存,減少Spill的次數(shù)。這樣一來為了避免內(nèi)存溢出,可能需要減少 storage 的內(nèi)存,即減小spark.storage.memoryFraction 的值,這樣 RDD cache 的容量減少,在某些場景下可能會(huì)對性能造成影響。

由于 shuffle 數(shù)據(jù)的大小是估算出來的(這主要為了減少計(jì)算數(shù)據(jù)大小的時(shí)間消耗),會(huì)存在誤差,當(dāng)實(shí)際使用的內(nèi)存比估算大的時(shí)候,這里 spark.shuffle.safetyFraction 用來作為一個(gè)保險(xiǎn)系數(shù),增加一定的誤差緩沖,降低實(shí)際內(nèi)存占用超過用戶配置值的可能性。所以 execution 真是最大可用的內(nèi)存為 0.2*0.8=0.16。shuffle 時(shí),一旦 execution 內(nèi)存使用超過該比例,就會(huì)進(jìn)行 spill。

storage 內(nèi)存

storage 最大可用內(nèi)存為 jvm space * spark.storage.memoryFraction * spark.storage.safetyFraction,默認(rèn)為 jvm space * 0.6 * 0.9。

由于在 cache block 時(shí)大小也是估算的,所以也需要一個(gè)保險(xiǎn)系數(shù)用來防止誤差引起 OOM,即 spark.storage.safetyFraction,所以真實(shí)能用來進(jìn)行 memory cache block 的內(nèi)存大小的比例為 0.6*0.9=0.54。一旦 storage 使用內(nèi)存超過該比例,將根據(jù) StorageLevel 決定不緩存 block 還是 OOM 或是存儲(chǔ)到磁盤。

storage 內(nèi)存中有 spark.shuffle.unrollFraction 的部分是用來 unroll,即用于 “展開” 一個(gè) partition 的數(shù)據(jù),這部分默認(rèn)為 0.2

不由 MemoryManager 管理的內(nèi)存

系統(tǒng)預(yù)留的大小為:1 - spark.storage.memoryFraction - spark.shuffle.memoryFraction,默認(rèn)為 0.2。另一部分是 storage 和 execution 保險(xiǎn)系數(shù)之外的內(nèi)存大小,默認(rèn)為 0.1。

存在的問題

舊方案最大的問題是 storage 和 execution 的內(nèi)存大小都是固定的,不可改變,即使 execution 有大量的空閑內(nèi)存且 storage 內(nèi)存不足,storage 也無法使用 execution 的內(nèi)存,只能進(jìn)行 spill,反之亦然。所以,在很多情況下存在資源浪費(fèi)。

另外,舊方案中,只有 execution 內(nèi)存支持 off heap,storage 內(nèi)存不支持 off heap。

今生

上面我們提到舊方案的兩個(gè)不足之處,在新方案中都得到了解決,即:

  • 新方案 storage 和 execution 內(nèi)存可以互相借用,當(dāng)一方內(nèi)存不足可以向另一方借用內(nèi)存,提高了整體的資源利用率
  • 新方案中 execution 內(nèi)存和 storage 內(nèi)存均支持 off heap

這兩點(diǎn)將在后文中進(jìn)一步展開,我們先來看看新方案中,默認(rèn)的內(nèi)存結(jié)構(gòu)是怎樣的?依舊分為三塊(這里將 storage 和 execution 內(nèi)存放在一起講):

  • 不受 MemoryManager 管理內(nèi)存,由以下兩部分組成:
    • 系統(tǒng)預(yù)留:大小默認(rèn)為 RESERVED_SYSTEM_MEMORY_BYTES,即 300M,可以通過設(shè)置 spark.testing.reservedMemory 改變,一般只有測試的時(shí)候才會(huì)設(shè)置該配置,所以我們可以認(rèn)為系統(tǒng)預(yù)留大小為 300M。另外,executor 的最小內(nèi)存限制為系統(tǒng)預(yù)留內(nèi)存的 1.5 倍,即 450M,若 executor 的總內(nèi)存大小小于 450M,則會(huì)拋出異常
    • storage、execution 安全系數(shù)外的內(nèi)存:大小為 (heap space - RESERVED_SYSTEM_MEMORY_BYTES)*(1 - spark.memory.fraction),默認(rèn)為 (heap space - 300M)* 0.4
  • storage + execution:storage、execution 內(nèi)存之和又叫 usableMemory,總大小為 (heap space - 300) * spark.memory.fractionspark.memory.fraction 默認(rèn)為 0.6。該值越小,發(fā)生 spill 和 block 踢除的頻率就越高。其中:
    • storage 內(nèi)存:默認(rèn)占其中 50%(包含 unroll 部分)
    • execution 內(nèi)存:默認(rèn)同樣占其中 50%

由于新方案是 1.6 后默認(rèn)的內(nèi)存管理方案,也是目前絕大部分 spark 用戶使用的方案,所以我們有必要更深入且詳細(xì)的展開分析。

初探統(tǒng)一內(nèi)存管理類

在最開始我們提到,新方案是由 UnifiedMemoryManager 實(shí)現(xiàn)的,我們先來看看該類的成員及方法,類圖如下:

通過這個(gè)類圖,我想告訴你這幾點(diǎn):

  • UnifiedMemoryManager 具有 4 個(gè) MemoryPool,分別是堆內(nèi)的 onHeapStorageMemoryPool 和 onHeapExecutionMemoryPool 以及堆外的 offHeapStorageMemoryPool 和 offHeapExecutionMemoryPool(其中,execution 和 storage 使用堆外內(nèi)存的方式不同,后面會(huì)講到)
  • UnifiedMemoryManager 申請、釋放 storage、execution、unroll 內(nèi)存的方法(看起來像廢話)
  • tungstenMemoryAllocator 會(huì)根據(jù)不同的 MemoryMode 來生成不同的 MemoryAllocator
    • 若 MemoryMode 為 ON_HEAP 為 HeapMemoryAllocator
    • 若 MemoryMode 為 OFF_HEAP 則為 UnsafeMemoryAllocator(使用 unsafe api 來申請堆外內(nèi)存)

如何申請 storage 內(nèi)存

有了上面的這些基礎(chǔ)知識(shí),再來看看是怎么申請 storage 內(nèi)存的。申請 storage 內(nèi)存是通過調(diào)用

UnifiedMemoryManager#acquireStorageMemory(blockId: BlockId,
                           numBytes: Long,
                           memoryMode: MemoryMode): Boolean

更具體的說法應(yīng)該是為某個(gè) block(blockId 指定)以那種內(nèi)存模式(on heap 或 off heap)申請多少字節(jié)(numBytes)的 storage 內(nèi)存,該函數(shù)的主要流程如下圖:

對于上圖,還需要做一些補(bǔ)充來更好理解:

MemoryMode

  • 如果 MemoryMode 是 ON_HEAP,那么 executionMemoryPool 為 onHeapExecutionMemoryPool、storageMemoryPool 為 onHeapStorageMemoryPool。maxMemory 為 (jvm space - 300M)* spark.memory.fraction,如果你還記得的話,這在文章最開始的時(shí)候有介紹
  • 如果 MemoryMode 是 OFF_HEAP,那么 executionMemoryPool 為 offHeapExecutionMemoryPool、storageMemoryPool 為 offHeapMemoryPool。maxMemory 為 maxOffHeapMemory,由 spark.memory.offHeap.size 指定,由 execution 和 storage 共享

要向 execution 借用多少?

計(jì)算要向 execution 借用多少內(nèi)存的代碼如下:

val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree, numBytes)

為 execution 空閑內(nèi)存和申請內(nèi)存 size 的較小值,這說明了兩點(diǎn):

  • 能借用到的內(nèi)存大小可能是小于申請的內(nèi)存大小的(當(dāng) executionPool.memoryFree < numBytes),更進(jìn)一步說,成功借用到的內(nèi)存加上 storage 原本空閑的內(nèi)存之和有可能還是小于要申請的內(nèi)存大小
  • execution 只可能把自己當(dāng)前空閑的內(nèi)存借給 storage,即使在這之前 execution 已經(jīng)從 storage 借來了大量內(nèi)存,也不會(huì)釋放自己已經(jīng)使用的內(nèi)存來 “還” 給 storage。execution 這么不講道理是因?yàn)橐獙?shí)現(xiàn)釋放 execution 內(nèi)存來歸還給 storage 復(fù)雜度太高,難以實(shí)現(xiàn)

還有一點(diǎn)需要注意的是,借用是發(fā)生在相同 MemoryMode 的 storageMemoryPool 和 executionMemoryPool 之間,不能在不同的 MemoryMode 間進(jìn)行借用

借到了就萬事大吉?

當(dāng) storage 空閑內(nèi)存不足以分配申請的內(nèi)存時(shí),從上面的分析我們知道會(huì)向 execution 借用,借來后是不是就萬事大吉了?當(dāng)然······不是,前面也提到了即使借到了內(nèi)存也可能還不夠,這也是上圖中紅色圓框中問號(hào)的含義,在我們再進(jìn)一步跟進(jìn)到 StorageMemoryPool#acquireMemory(blockId: BlockId, numBytes: Long): Boolean 中一探究竟,該函數(shù)主要流程如下:

同樣,對于上面這個(gè)流程圖需要做一些說明:

計(jì)算要釋放的內(nèi)存量
val numBytesToFree = math.max(0, numAcquireBytes - memoryFree)

如上,要釋放的內(nèi)存大小為再從 execution 借用了內(nèi)存,使得 storage 空閑內(nèi)存增大 n(n>=0) 后,還比申請的內(nèi)存少的那部分內(nèi)存,若借用后 storage 空閑內(nèi)存足以滿足申請的大小,則 numBytesToFree 為 0,無需進(jìn)行釋放

如何釋放 storage 內(nèi)存?

釋放的方式是踢除已緩存的 blocks,實(shí)現(xiàn)為 evictBlocksToFreeSpace(blockId: Option[BlockId], space: Long, memoryMode: MemoryMode): Long,有以下幾個(gè)原則:

  • 只能踢除相同 MemoryMode 的 block
  • 不能踢除屬于同一個(gè) RDD 的另一個(gè) block

首先會(huì)進(jìn)行預(yù)踢除(所謂預(yù)踢除就是計(jì)算假設(shè)踢除該 block 能釋放多少內(nèi)存),預(yù)踢除的具體邏輯是:遍歷一個(gè)已緩存到內(nèi)存的 blocks 列表(該列表按照緩存的時(shí)間進(jìn)行排列,約早緩存的在越前面),逐個(gè)計(jì)算預(yù)踢除符合原則的 block 是否滿足以下條件之一:

  • 預(yù)踢除的累計(jì)總大小滿足要踢除的大小
  • 所有的符合原則的 blocks 都被預(yù)踢除

若最終預(yù)踢除的結(jié)果是可以滿足要提取的大小,則對預(yù)踢除中記錄的要踢除的 blocks 進(jìn)行真正的踢除。具體的方式是:如果從內(nèi)存中踢除后,還具有其他 StorageLevel 或在其他節(jié)點(diǎn)有備份,依然保留該 block 信息;若無,則刪除該 block 信息。最終,返回踢除的總大?。赡苌源笥谝叱拇笮。?/p>

若最終預(yù)踢除的結(jié)果是無法滿足要提取的大小,則不進(jìn)行任何實(shí)質(zhì)性的踢除,直接返回踢除size 為 0。需要再次提醒的是,只能踢除相同 MemoryMode 的 block。

以上,結(jié)合兩幅流程圖及相應(yīng)的說明,相信你已經(jīng)搞清楚如何申請 storage 內(nèi)存了。我們再來看看 execution 內(nèi)存是如何申請的

如何申請 execution 內(nèi)存

我們知道,申請 storage 內(nèi)存是為了 cache 一個(gè) numBytes 的 block,結(jié)果要么是申請成功、要么是申請失敗,不存在申請到的內(nèi)存數(shù)比 numBytes 少的情況,這是因?yàn)椴荒軐?block 一部分放內(nèi)存,一部分 spill 到磁盤。但申請 execution 內(nèi)存則不同,申請 execution 內(nèi)存是通過調(diào)用

UnifiedMemoryManager#acquireExecutionMemory(numBytes: Long,
                                            taskAttemptId: Long,
                                            memoryMode: MemoryMode): Long

來實(shí)現(xiàn)的,這里的 numBytes 是指至多 numBytes,最終申請的內(nèi)存數(shù)比 numBytes 少也是成功的,比如在 shuffle write 的時(shí)候使用的時(shí)候,如果申請?的內(nèi)存不夠,則進(jìn)行 spill。

另一個(gè)特點(diǎn)是,申請 execution 時(shí)可能會(huì)一直阻塞,這是為了能確保每個(gè) task 在進(jìn)行 spill 之前都能占用至少 1/2N 的 execution pool 內(nèi)存數(shù)(N 為 active tasks 數(shù))。當(dāng)然,這也不是能完全確保的,比如 tasks 數(shù)激增但老的 tasks 還沒釋放內(nèi)存就不能滿足。

接下來,我們來看看如何申請 execution 內(nèi)存,流程圖如下:

從上圖可以看到,整個(gè)流程還是挺復(fù)雜的。首先,我先對上圖中的一些環(huán)節(jié)進(jìn)行進(jìn)一步說明以幫助理解,最后再以簡潔的語言來概括一下整個(gè)過程。

MemoryMode

同樣,不同的 MemoryMode 的情況是不同的,如下:

  • 如果 MemoryMode 為 ON_HEAP:
    • executionMemoryPool 為 onHeapExecutionMemoryPool
    • storageMemoryPool 為 onHeapStorageMemoryPool
    • storageRegionSize 為 onHeapStorageRegionSize,即 (heap space - 300M) * spark.memory.storageFraction
    • maxMemory 為 maxHeapMemory,即 (heap space - 300M)
  • 如果 MemoryMode 為 OFF_HEAP:
    • executionMemoryPool 為 offHeapExecutionMemoryPool
    • storageMemoryPool 為 offHeapStorageMemoryPool
    • maxMemory 為 maxOffHeapMemory,即 spark.memory.offHeap.size
    • storageRegionSize 為 offHeapStorageRegionSize,即 maxOffHeapMemory * spark.memory.storageFraction

這一小節(jié)描述的內(nèi)容非常重要,因?yàn)橹笏械牧鞒潭际腔诖?,看到后面的流程時(shí),還記著會(huì)有 ON_HEAP 和 OFF_HEAP 兩種情況

maybeGrowExecutionPool(向 storage 借用內(nèi)存)

只有當(dāng) executionMemoryPool 的空閑內(nèi)存不足以滿足申請的 numBytes 時(shí),該函數(shù)才會(huì)生效。那這個(gè)函數(shù)是怎么向 storage 借用內(nèi)存的呢?流程如下:

  1. 計(jì)算可從 storage 回收的內(nèi)存 memoryReclaimableFromStorage,為 storage 當(dāng)前的空閑內(nèi)存和之前 storage 從 execution 借走的內(nèi)存中較大的那個(gè)
  2. 如果 memoryReclaimableFromStorage 為 0,說明之前 storage 沒有從 execution 這邊借用過內(nèi)存并且 storage 自己已經(jīng)把內(nèi)存用完了,沒有任何內(nèi)存可以借給 execution,那么本次借用就失敗,直接返回;如果 memoryReclaimableFromStorage 大于 0,則進(jìn)入下一步
  3. 計(jì)算本次真正要借用的內(nèi)存 spaceToReclaim,即 execution 不足的內(nèi)存(申請的內(nèi)存減去 execution 的空閑內(nèi)存)與 memoryReclaimableFromStorage 中的較小值。原則是即使能借更多,也只借夠用的就行
  4. 執(zhí)行借用操作,如果需要 storage 的空閑內(nèi)存和之前 storage 從 execution 借用的的內(nèi)存加起來才能滿足,則會(huì)進(jìn)行踢除 cached blocks

以上就是整個(gè) execution 向 storage 借用內(nèi)存的過程,與 storage 向 execution 借用最大的不同是:execution 會(huì)踢除 storage 已經(jīng)使用的向 execution 的內(nèi)存,踢除的流程在文章的前面有描述。這是因?yàn)椋@本來就是屬于 execution 的內(nèi)存并且通過踢除來實(shí)現(xiàn)歸還實(shí)現(xiàn)上也不復(fù)雜

一個(gè) task 能使用多少 execution 內(nèi)存?

也就是流程圖中的 maxMemoryPerTask 和 minMemoryPerTask 是如何計(jì)算的,如下:

val maxPoolSize = computeMaxExecutionPoolSize()
val maxMemoryPerTask = maxPoolSize / numActiveTasks
val minMemoryPerTask = poolSize / (2 * numActiveTasks)

maxPoolSize 為從 storage 借用了內(nèi)存后,executionMemoryPool 的最大可用內(nèi)存,maxMemoryPerTask 和 minMemoryPerTask 的計(jì)算方式也如代碼所示。這樣做是為了使得每個(gè) task 使用的內(nèi)存都能維持在 1/2*numActiveTasks ~ 1/numActiveTasks 范圍內(nèi),使得在整體上能保持各個(gè) task 資源占用比較均衡并且一定程度上允許需要更多資源的 task 在一定范圍內(nèi)能分配到更多資源,也照顧到了個(gè)性化的需求

最后到底分配多少 execution 內(nèi)存?

首先要計(jì)算兩個(gè)值:

  • 最大可以分配多少,即 maxToGrant:是申請的內(nèi)存量與 (maxMemoryPerTask-已為該 task 分配的內(nèi)存值) 中的較小值,如果 maxMemoryPerTask < 已為該 task 分配的內(nèi)存值,則直接為 0,也就是之前已經(jīng)給該 task 分配的夠多了
  • 本次循環(huán)真正可以分配多少,即 toGrant:maxToGrant 與當(dāng)前 executionMemoryPool 空閑內(nèi)存(注意是借用后)的較小值

所以,本次最終能分配的量也就是 toGrant,如果 toGrant 加上已經(jīng)為該 task 分配的內(nèi)存量之和 還小于 minMemoryPerTask 并且 toGrant 小于申請的量,則就會(huì)觸發(fā)阻塞。否則,分配 toGrant 成功,函數(shù)返回。

阻塞釋放的條件有兩個(gè),如下:

  • 有 task 釋放了內(nèi)存:更具體的說是有 task 釋放了相同 MemoryMode 的 execution 內(nèi)存,這時(shí)空閑的 execution 內(nèi)存變多了
  • 有新 task 申請了內(nèi)存:同樣,更具體的說是有新 task 申請了相同 MemoryMode 的 execution 內(nèi)存,這時(shí) numActiveTasks 變大了,minMemoryPerTask 則變小了

用簡短的話描述整個(gè)過程如下:

  1. 申請 execution 內(nèi)存時(shí),會(huì)循環(huán)不停的嘗試,每次嘗試都會(huì)看是否需要從 storage 中借用或回收之前借給 storage 的內(nèi)存(這可能會(huì)觸發(fā)踢除 cached blocks),如果需要?jiǎng)t進(jìn)行借用或回收;
  2. 之后計(jì)算本次循環(huán)能分配的內(nèi)存,
    • 如果能分配的不夠申請的且該 task 累計(jì)分配的(包括本次)小于每個(gè) task 應(yīng)該獲得的最小值(1/2*numActiveTasks),則會(huì)阻塞,直到有新的 task 申請內(nèi)存或有 task 釋放內(nèi)存為止,然后進(jìn)入下一次循環(huán);
    • 否則,直接返回本次分配的值

使用建議

首先,建議使用新模式,所以接下來的配置建議都是基于新模式的。

  • spark.memory.fraction:如果 application spill 或踢除 block 發(fā)生的頻率過高(可通過日志觀察),可以適當(dāng)調(diào)大該值,這樣 execution 和 storage 的總可用內(nèi)存變大,能有效減少發(fā)生 spill 和踢除 block 的頻率
  • spark.memory.storageFraction:為 storage 占 storage、execution 內(nèi)存總和的比例。雖然新方案中 storage 和 execution 之間可以發(fā)生內(nèi)存借用,但總的來說,spark.memory.storageFraction 越大,運(yùn)行過程中,storage 能用的內(nèi)存就會(huì)越多。所以,如果你的 app 是更吃 storage 內(nèi)存的,把這個(gè)值調(diào)大一點(diǎn);如果是更吃 execution 內(nèi)存的,把這個(gè)值調(diào)小一點(diǎn)
  • spark.memory.offHeap.enabled:堆外內(nèi)存最大的好處就是可以避免 GC,如果你希望使用堆外內(nèi)存,將該值置為 true 并設(shè)置堆外內(nèi)存的大小,即設(shè)置 spark.memory.offHeap.size,這是必須的

另外,需要特別注意的是,堆外內(nèi)存的大小不會(huì)算在 executor memory 中,也就是說加入你設(shè)置了 --executor memory 10Gspark.memory.offHeap.size=10G,那總共可以使用 20G 內(nèi)存,堆內(nèi)和堆外分別 10G。

總結(jié)&引子

到這里,已經(jīng)比較籠統(tǒng)的介紹了 Spark 內(nèi)存管理的 “前世”,也比較細(xì)致的介紹了 “今生”。篇幅比較長,但沒有一大段一大段的代碼,應(yīng)該還算比較好懂。如果看到這里,希望你多少能有所收獲。

然后,請你在大致回顧下這篇文章,有沒有覺得缺了點(diǎn)什么?是的,是缺了點(diǎn)東西,所謂 “內(nèi)存管理” 怎么就沒看到具體是怎么分配內(nèi)存的呢?是怎么使用的堆外內(nèi)存?storage 和 execution 的堆外內(nèi)存使用方式會(huì)不會(huì)不同?execution 和 storage 又是怎么使用堆內(nèi)內(nèi)存的呢?以怎么樣的數(shù)據(jù)結(jié)構(gòu)呢?

如果你想搞清楚這些問題,關(guān)注公眾號(hào)并回復(fù) “內(nèi)存管理下”。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容