spark source code analysis of twenty-two - Task memory management

statement of problem

This article will answer the following questions:

1. spark in the implementation of the task, its memory is how to manage?

2. How to address within the heap memory is designed for? How to avoid memory address because of the JVM GC due to changes? How memory buffer pool recovery mechanism is its internal design?

3. heap outside and inside the heap memory are allocated by means of what? Offset their data is how to calculate?

4. What consumers MemoryConsumer that?

5. Data in memory page is addressed how?

 

Individual tasks of memory management is managed by the org.apache.spark.memory.TaskMemoryManager.

TaskMemoryManager

It is primarily responsible for managing the single task of memory.

First of heap memory is divided into internal and external memory heap memory.

For the external memory heap, the memory address may be used directly addressed 64-bit address long integer.

It represents the inner heap memory, a memory address in combination of objects and a base target offset.

Class of problems encountered in the design process:

To save the address of the configuration of the internal structure other problems, such as the pointer in the sorting buffer or hashmap records, although we decided to use 128-bit addressing, we can not only save the address of the base objects, since due gc the existence of this address can not be guaranteed to be stable and unchanging. (Because of generational collection mechanism, in-memory object will continue to move, each move, the memory address of the object will change, but for the developers do not address the object of interest, it is transparent)

The final program:

For the external memory heap, which saves only the original address, because the outer gc not affect the memory stack; for the heap, we use a 64-bit high memory pages to store 13, 51 to hold the lower page offset, use page tables to hold base object whose index in the page table memory is the memory pages. Up to 8192 pages, theoretically allows index 8192 * (2 ^ 31 -1) * 8 bytes, corresponding to the data 140TB. Wherein the maximum value of 2 ^ 31-1 is an integer, because the page table is a long record index array, this array is the maximum length of 2 ^ 31-1. In fact, not so great. Because the 64-bit design, in addition to pages and page offset outer partition information for further storage of data.

MemoryLocation

Wherein the base and the object are encapsulated into objects offset MemoryLocation object, it means that this type of memory addressing is used, as follows:

The only realize its class org.apache.spark.unsafe.memory.MemoryBlock.

MemoryBlock

It represents a contiguous block of memory, and includes a start position of a fixed size. Starting Location MemoryLocation represented.

That it has four attributes:

This continuous block of memory starting address: Succeeding and offset from the base objects from a parent class.

Fixed size and length to uniquely identify the memory block - memory page number (page number)

 

The main methods are as follows, where Platform is the operating system with a class-related, do not do too much explanation.

MemoryAllocator

Primarily responsible for memory applications work. This interface implementation class is really allocate memory. TaskMemoryManager described later is only responsible for managing memory, but is not responsible for specific memory allocation issues.

Its inheritance as follows, there are two sub-categories:

The main methods and the constants defined as follows:

The main method is mainly used to allocate and free memory block. The following look at it to achieve two main subclasses.

HeapMemoryAllocator

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

Responsible for allocating memory in the heap, its main distribution long array, allocate memory for a maximum 16GB.

Member variables

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包含大量的数据,是需要内存管理的。

Guess you like

Origin www.cnblogs.com/johnny666888/p/11277947.html