SizeTrackingAppendOnlyMap 在 ExternalAppendOnlyMap 中使用,当 map 中对象占用的内存大小超过一定阈值时,把 数据 spill 到磁盘。所以 SizeTrackingAppendOnlyMap extends AppendOnlyMap[K, V] with SizeTracker
。
AppendOnlyMap
AppendOnlyMap 是只能增加和修改元素的 Map,不能删除元素。key 可以为 null, key 为 null 的 value 专门用一个字段 nullValue 存储. 用一个字段 haveNullValue 标识是否包含 key 为 null 的 value. 其他数据用一个数组 data 存储。
AppendOnlyMap capacity 默认是 64。mask 为 catacity - 1. data 容量为 (2 * capacity),因为同时存储 key 和 value。 LOAD_FACTOR=0.7。当存储了 capacity * 0.7个元素时,构建新的 data 数组,capacity 翻倍。然后读取原来 data 中的内容,安照新的容量重新计算对应位置,然后放到新的数组里。
插入数据时,先计算 var pos = rehash(k.hashCode) & mask
,key 存储在 data[2pos], value 存储在 data[2pos + 1]。如果已经有数据存在,则 pos + 1.
AppendOnlyMap 最多存储 375809638 个元素。
- destructiveSortedIterator() 方法,提供按 key 排序的 Iterator,可以按原址排序,不需要额外的内存空间, 但是破坏了数据在 data 中的索引, 不能再插入数据。
SizeTracker
SizeTracker 跟踪估计的内存使用量。使用以下字段。
numUpdates: Map 总共插入和修改的次数。
nextSampleNum: 当 numUpdates 等于时,计算
SAMPLE_GROWTH_RATE: 1.1, 计算内存计算量后, nextSampleNum = nextSampleNum * SAMPLE_GROWTH_RATE
samples: 存储最后两次评估的数据。Sample 有两个字段 size: Long(占用内存), numUpdates: Long(更新次数)
bytesPerUpdate: 最后两次 samples 的每次 update 平均字节数。(latest.size - previous.size).toDouble / (latest.numUpdates - previous.numUpdates)
SizeEstimator.estimate 估计对象占用内存
评估一个 class 实例占用的内存,如果字段是基本类型,占用的内存大小如下表。
private val BYTE_SIZE = 1
private val BOOLEAN_SIZE = 1
private val CHAR_SIZE = 2
private val SHORT_SIZE = 2
private val INT_SIZE = 4
private val LONG_SIZE = 8
private val FLOAT_SIZE = 4
private val DOUBLE_SIZE = 8
如果字段是 object, 则最小是 8,根据是否64位系统和是否开启指针压缩,可能为 12 或者 16.
- estimate 过程
使用广度优先算法,需要遍历的 class 对象放到队列里。依次从队列里取 class 对象,
先获取本 class 的 classInfo。拿到本对象的 fileds 的大小,然后把所有对象类型的字段,都放到队列里。
基本数据类型的字段不需要。
val classInfo = getClassInfo(cls)
state.size += alignSize(classInfo.shellSize)
for (field <- classInfo.pointerFields) {state.enqueue(field.get(obj))
}