spark 源碼分析之二十二-- Task的內存管理

問題的提出

本篇文章將回答以下問題:html

1.  spark任務在執行的時候,其內存是如何管理的?java

2. 堆內內存的尋址是如何設計的?是如何避免因爲JVM的GC的存在引發的內存地址變化的?其內部的內存緩存池回收機制是如何設計的?apache

3. 堆外和堆內內存分別是經過什麼來分配的?其數據的偏移量是如何計算的?數組

4. 消費者MemoryConsumer是什麼?緩存

5. 數據在內存頁中是如何尋址的?源碼分析

 

單個任務的內存管理是由 org.apache.spark.memory.TaskMemoryManager 來管理的。post

TaskMemoryManager

它主要是負責管理單個任務的內存。編碼

首先內存分爲堆外內存和堆內內存。加密

對於堆外內存,能夠內存地址直接使用64位長整型地址尋址。spa

對於堆內內存,內存地址由一個base對象和一個offset對象組合起來表示。

類在設計的過程當中遇到的問題:

對於其餘結構內部的結構的地址的保存是存在問題的,好比在hashmap或者是 sorting buffer 中的記錄的指針,儘管咱們決定使用128位來尋址,咱們不能只存base對象的地址,由於因爲gc的存在,這個地址不能保證是穩定不變的。(因爲分代回收機制的存在,內存中的對象會不斷移動,每次移動,對象內存地址都會改變,但這對於不關注對象地址的開發者來講,是透明的)

最終的方案:

對於堆外內存,只保存其原始地址,由於堆外內存不受gc影響;對於堆內內存,咱們使用64位的高13位來保存內存頁數,低51位來保存這個頁中的offset,使用page表來保存base對象,其在page表中的索引就是該內存的內存頁數。頁數最多有8192頁,理論上容許索引 8192 * (2^31 -1)* 8 bytes,至關於140TB的數據。其中 2^31 -1 是整數的最大值,由於page表中記錄索引的是一個long型數組,這個數組的最大長度是2^31 -1。實際上沒有那麼大。由於64位中除了用來設計頁數和頁內偏移量外還用於存放數據的分區信息。

MemoryLocation

其中這個base對象和offset對象被封裝進了 MemoryLocation對象中,也就是說,這個類就是用來內存尋址的,以下:

其惟一實現類爲 org.apache.spark.unsafe.memory.MemoryBlock。

MemoryBlock

它表示一段連續的內存塊,包括一個起始位置和一個固定大小。起始位置有MemoryLocation來表示。

也就是說它有四個屬性:

這段連續內存塊的起始地址:從父類繼承而來的base對象和offset。

固定大小 length以及對這個內存塊的惟一標識 - 內存頁碼(page number)

 

主要方法以下,其中Platform是跟操做系統有關的一個類,不作過多說明。

MemoryAllocator

其主要負責內存的申請工做。這個接口的實現類是真正分配內存的。後面介紹的TaskMemoryManager只是負責管理內存,可是不負責具體的內存分配事宜。

其繼承關係以下,有兩個子類:

其定義的主要的常量和方法以下:

主要方法主要用來分配和釋放內存塊。下面主要來看一下它兩個子類的實現。

HeapMemoryAllocator

全稱:org.apache.spark.unsafe.memory.HeapMemoryAllocator

主要負責分配堆內內存,其主要分配long型數組,最大分配內存爲16GB。

成員變量

bufferPoolBySize是一個HashMap,其內部的value裏面存放的數據都是弱引用類型的數據,在JVM 發生GC時,數據可能會被回收。它裏面存放的數據都是已經不用的廢棄掉的內存塊。

是否使用內存緩存池

申請的內存塊的大小大於閥值才使用內存緩存池。

分配內存

思路:首先根據bytes大小計算處words的大小,而後字節對齊計算出對齊須要的字節,斷言對齊後的字節大小大於等於以前未對齊的字節大小。爲何要對齊呢?由於長整型數組的內存大小是對齊的。

若是對齊後的字節大小知足使用緩存池的條件,則先從緩存池中彈出對應的pool,而且若是彈出的pool不爲空,則逐一取出以前釋放的數組,並將其封裝進MmeoryBlock對象,而且使用標誌位清空以前的歷史數據返回之。

不然,則初始化指定的words長度的長整型數組,並將其封裝進MmeoryBlock對象,而且使用標誌位清空以前的歷史數據返回之。總之緩存的是長整型數組,存放數據的也是長整型數組。

釋放內存

 

首先把要釋放的內存數據使用free標誌位覆蓋,pageNumber置爲佔位的page number。

而後取出其內部的長整型數組賦值給臨時變量,而且把base對象置爲null,offset置爲0。

取出的長整型數組計算其對齊大小,內存頁的大小不必定等於數組的長度 * 8,此時的size是內存頁的大小,須要進行對齊操做。

對齊以後的內存頁大小若是知足緩存池條件,則將其暫存緩存池,等待下次回收再用或者JVM的GC回收。

這個方法結束以後,這個長整型數組被LinkedList對象(即pool)引用,但這是一個若引用,因此說,如今這個數組是一個遊離對象,當JVM回收時,會回收它。

對堆內內存的總結

對於堆內內存上的數據真實受JVM的GC影響,其真實數據的內存地址會發生改變,巧妙使用數組這種容器以及偏移量巧妙地將這個問題規避了,數據回收也可使用緩存池機制來減小數組頻繁初始化帶來的開銷。其內部使用虛引用來引用釋放的數組,也不會致使沒法回收致使內存泄漏。

UnsafeMemoryAllocator

全稱:org.apache.spark.unsafe.memory.UnsafeMemoryAllocator

負責分配堆外內存。

分配內存

思路:底層使用unsafe這個類來分配堆外內存。這裏的offset就是操做系統的內存地址,base對象爲null。

釋放內存

堆外內存的釋放不能使用緩存池,由於堆外內存不受JVM的管理,將會致使遺留的不用的內存沒法回收從而引起更嚴重的內存泄漏,更甚者堆外內存使用的是系統內存,嚴重的話還會致使出現系統級問題。

堆堆外內存的總結

簡言之,對於堆外內存的分配和回收,都是經過java內置的Unsafe類來實現的,其統一規範中的base對象爲null,其offset就是該內存頁在操做系統中的真實地址。

 

下面剖析一下TaskMemoryManager的成員變量和核心方法。

進一步剖析TaskMemoryManager

成員變量

下面,先來看一下其成員變量,截圖以下:

對主要的成員變量作以下解釋:

OFFSET_BITS:是指的page number 佔用的bit個數

MAXIMUM_PAGE_SIZE_BYTES:約17GB,每頁最大可存內存大小

pageTable:主要用來存放內存頁的

allocatedPages:主要用來追蹤內存頁是否爲空的

memoryManager:主要負責Spark內存管理,具體細節能夠參照 spark 源碼分析之十五 -- Spark內存管理剖析 作進一步瞭解。

taskAttemptId:任務id

tungstenMemoryMode:tungsten內存模式,是堆外內存仍是堆內內存

consumers:記錄了任務內存的全部消費者

核心方法

全部方法以下:

下面,咱們來逐一對其進行源碼剖析。

1. 獲取執行內存

思路:首先先去MemoryManager中去申請執行內存,若是內存不夠,則獲取全部的MemoryConsumer,調用其spill方法將內存數據溢出到磁盤,直到釋放內存空間知足申請的內存空間則中止spill操做。

2. 釋放執行內存

這其實不是真正意義上的內存釋放,只是管帳的把這筆內存佔用劃掉了,真正的內存釋放仍是須要調用MemoryConsumer的spill方法將內存數據溢出到磁盤來釋放內存。

3. 獲取內存頁大小

 

4. 分配內存頁

思路:首先獲取執行內存。執行內存獲取成功後,找到一個空的內存頁。

若是內存頁碼大於指定的最大頁碼,則釋放剛申請的內存,返回;不然使用MemoryAllocator分配內存頁、初始化內存頁碼並將其放入page表的管理,最後返回page。關於MemoryAllocator分配內存的細節,請參照上文關於其堆內內存或堆外內存的內存分配的詳細剖析。

 

5. 釋放內存頁

思路:首先調用EMmoryAllocator的free 方法來釋放內存,而且調用 方法2 來劃掉內存的佔用狀況。

 

6. 內存地址加密

思路:高13位保存的是page number,低51位保存的是地址的offset

 

7.內存地址解密

思路: 跟 方法6 的編碼思路相反

 

8.根據內存地址獲取內存的base對象,前提是必須是堆內內存頁,不然沒有base對象。

 

9.獲取內存地址在內存頁的偏移量offset

若是是堆內內存,則直接返回其解碼以後的offset便可。

若是是堆外內存,分配內存時的offset + 頁內的偏移量就是真正的偏移量,是針對操做系統的,也是絕對的偏移量。

 

10.清空全部內存頁

思路:使用MemoryAllocator釋放內存,而且請求管帳的MemoryManager釋放執行內存和task的全部內存。

 

11.獲取單個任務的執行內存使用狀況

思路:從MemoryManager處獲取指定任務的執行內存使用狀況。

 

下面看一下跟TaskMemoryManager交互的消費者對象 -- MemoryConsumer。

MemoryConsumer

類說明

全稱:org.apache.spark.memory.MemoryConsumer

它是任務內存的消費者。

其類結構以下:

成員變量

taskMemoryManager:是負責任務內存管理。

used:表示使用的內存。

mode:表示內存的模式是堆內內存仍是堆外內存。

pageSize:表示頁大小。

主要方法

1. 內存數據溢出到磁盤,抽象方法,等待子類實現。

 

2. 申請釋放內存部分,再也不作詳細的分析,都是依賴於 TaskMemoryManager 作的操做。

關於更多MemoryConsumer的以及其子類的相關內容,將在下一篇文章Shuffle的寫操做中詳細剖析。

 

總結

本篇文章主要剖析了Task在任務執行時內存的管理相關的內容,如今可能還看不出其重要性,後面在含有sort的shuffle過程當中,會頻繁的使用基於內存的sorter,此時的sorter包含大量的數據,是須要內存管理的。

相關文章
相關標籤/搜索