本篇文章將回答以下問題:html
1. spark任務在執行的時候,其內存是如何管理的?java
2. 堆內內存的尋址是如何設計的?是如何避免因爲JVM的GC的存在引發的內存地址變化的?其內部的內存緩存池回收機制是如何設計的?apache
3. 堆外和堆內內存分別是經過什麼來分配的?其數據的偏移量是如何計算的?數組
4. 消費者MemoryConsumer是什麼?緩存
5. 數據在內存頁中是如何尋址的?源碼分析
單個任務的內存管理是由 org.apache.spark.memory.TaskMemoryManager 來管理的。post
它主要是負責管理單個任務的內存。編碼
首先內存分爲堆外內存和堆內內存。加密
對於堆外內存,能夠內存地址直接使用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位中除了用來設計頁數和頁內偏移量外還用於存放數據的分區信息。
其中這個base對象和offset對象被封裝進了 MemoryLocation對象中,也就是說,這個類就是用來內存尋址的,以下:
其惟一實現類爲 org.apache.spark.unsafe.memory.MemoryBlock。
它表示一段連續的內存塊,包括一個起始位置和一個固定大小。起始位置有MemoryLocation來表示。
也就是說它有四個屬性:
這段連續內存塊的起始地址:從父類繼承而來的base對象和offset。
固定大小 length以及對這個內存塊的惟一標識 - 內存頁碼(page number)
主要方法以下,其中Platform是跟操做系統有關的一個類,不作過多說明。
其主要負責內存的申請工做。這個接口的實現類是真正分配內存的。後面介紹的TaskMemoryManager只是負責管理內存,可是不負責具體的內存分配事宜。
其繼承關係以下,有兩個子類:
其定義的主要的常量和方法以下:
主要方法主要用來分配和釋放內存塊。下面主要來看一下它兩個子類的實現。
全稱: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影響,其真實數據的內存地址會發生改變,巧妙使用數組這種容器以及偏移量巧妙地將這個問題規避了,數據回收也可使用緩存池機制來減小數組頻繁初始化帶來的開銷。其內部使用虛引用來引用釋放的數組,也不會致使沒法回收致使內存泄漏。
全稱:org.apache.spark.unsafe.memory.UnsafeMemoryAllocator
負責分配堆外內存。
思路:底層使用unsafe這個類來分配堆外內存。這裏的offset就是操做系統的內存地址,base對象爲null。
堆外內存的釋放不能使用緩存池,由於堆外內存不受JVM的管理,將會致使遺留的不用的內存沒法回收從而引起更嚴重的內存泄漏,更甚者堆外內存使用的是系統內存,嚴重的話還會致使出現系統級問題。
簡言之,對於堆外內存的分配和回收,都是經過java內置的Unsafe類來實現的,其統一規範中的base對象爲null,其offset就是該內存頁在操做系統中的真實地址。
下面剖析一下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。
全稱:org.apache.spark.memory.MemoryConsumer
它是任務內存的消費者。
其類結構以下:
taskMemoryManager:是負責任務內存管理。
used:表示使用的內存。
mode:表示內存的模式是堆內內存仍是堆外內存。
pageSize:表示頁大小。
1. 內存數據溢出到磁盤,抽象方法,等待子類實現。
2. 申請釋放內存部分,再也不作詳細的分析,都是依賴於 TaskMemoryManager 作的操做。
關於更多MemoryConsumer的以及其子類的相關內容,將在下一篇文章Shuffle的寫操做中詳細剖析。
本篇文章主要剖析了Task在任務執行時內存的管理相關的內容,如今可能還看不出其重要性,後面在含有sort的shuffle過程當中,會頻繁的使用基於內存的sorter,此時的sorter包含大量的數據,是須要內存管理的。