MemoryManager内存管理器

内存管理器可以说是spark内核中最重要的基础模块之一,shuffle时的排序,rdd缓存,展开内存,广播变量,Task运行结果的存储等等,凡是需要使用内存的地方都需要向内存管理器定额申请。我认为内存管理器的主要作用是为了尽可能减小内存溢出的同时提高内存利用率。旧版本的spark的内存管理是静态内存管理器StaticMemoryManager,而新版本(应该是从1.6之后吧,记不清了)则改成了统一内存管理器UnifiedMemoryManager,同一内存管理器相对于静态内存管理器最大的区别在于执行内存和存储内存二者之间没有明确的界限,可以相互借用,但是执行内存的优先级更高,也就是说如果执行内存不够用就会挤占存储内存,这时会将一部分缓存的rdd溢写到磁盘上直到腾出足够的空间。但是执行内存任何情况下都不会被挤占,想想这也可以理解,毕竟执行内存是用于shuffle时排序的,这只能在内存中进行,而rdd缓存的要求就没有这么严格。

有几个参数控制各个部分内存的使用比例,

  • spark.memory.fraction,默认值0.6,这个参数控制spark内存管理器管理的内存占内存存的比例(准确地说是:堆内存-300m,300m是为永久代预留),也就是说执行内存和存储内存加起来只有(堆内存-300m)的0.6,剩余的0.4是用于用户代码执行过程中的内存占用,比如你的代码中可能会加载一些较大的文件到内存中,或者做一些排序,用户代码使用的内存并不受内存管理器管理,所以需要预留一定的比例。
  • spark.memory.storageFraction,默认值0.5,顾名思义,这个值决定了存储内存的占比,注意是占内存管理器管理的那部分内存的比例,剩余的部分用作执行内存。例如,默认情况下,存储内存占堆内存的比例是0.6 * 0.5 = 0.3(当然准确地说是占堆内存-300m的比例)。

MemoryManager概述

我们首先整体看一下MemoryManager这个类,

    maxOnHeapStorageMemory
maxOffHeapStorageMemory
setMemoryStore
acquireStorageMemory
acquireUnrollMemory
acquireExecutionMemory
releaseExecutionMemory
releaseAllExecutionMemoryForTask
releaseStorageMemory
releaseAllStorageMemory
releaseUnrollMemory
executionMemoryUsed
storageMemoryUsed
getExecutionMemoryUsageForTask

可以发现,MemoryManager内部的方法比较少而且是有规律的,它将内存在功能上分为三种:StorageMemory,UnrollMemory,ExecutionMemory,

针对这三种内存分别有申请内存的方法和释放内存的方法,并且三种申请内存的方法都是抽象方法,由子类实现。

此外,我们看一下MemoryManager内部有哪些成员变量:

    protected val onHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.ON_HEAP)
protected val offHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.OFF_HEAP)
protected val onHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.ON_HEAP)
protected val offHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.OFF_HEAP)

这四个成员变量分别代表四种内存池。这里要注意的是,MemoryPool的构造其中有一个Object类型参数用于同步锁,MemoryPool内部的一些方法会获取该对象锁用于同步。

我们看一下他们的初始化:

    onHeapStorageMemoryPool.incrementPoolSize(onHeapStorageMemory)
onHeapExecutionMemoryPool.incrementPoolSize(onHeapExecutionMemory)
offHeapExecutionMemoryPool.incrementPoolSize(maxOffHeapMemory - offHeapStorageMemory)
offHeapStorageMemoryPool.incrementPoolSize(offHeapStorageMemory)

MemoryManager.releaseExecutionMemory

其实就是调用ExecutionMemoryPool的相关方法,

  private[memory]
def releaseExecutionMemory(
numBytes: Long,
taskAttemptId: Long,
memoryMode: MemoryMode): Unit = synchronized {
memoryMode match {
case MemoryMode.ON_HEAP => onHeapExecutionMemoryPool.releaseMemory(numBytes, taskAttemptId)
case MemoryMode.OFF_HEAP => offHeapExecutionMemoryPool.releaseMemory(numBytes, taskAttemptId)
}
}

ExecutionMemoryPool.releaseMemory

代码逻辑很简单,就不多说了。

其实从这个方法,我们大概可以看出,spark内存管理的含义,其实spark的内存管理说到底就是对内存使用量的记录和管理,而并不是像操作系统或jvm那样真正地进行内存的分配和回收。

def releaseMemory(numBytes: Long, taskAttemptId: Long): Unit = lock.synchronized {
// 从内部的簿记量中获取该任务使用的内存
val curMem = memoryForTask.getOrElse(taskAttemptId, 0L)
// 检查要释放的内存是否超过了该任务实际使用的内存,并打印告警日志
var memoryToFree = if (curMem < numBytes) {
logWarning(
s"Internal error: release called on $numBytes bytes but task only has $curMem bytes " +
s"of memory from the $poolName pool")
curMem
} else {
numBytes
}
if (memoryForTask.contains(taskAttemptId)) {
// 更新簿记量
memoryForTask(taskAttemptId) -= memoryToFree
// 如果该任务的内存使用量小于等于0,那么从簿记量中移除该任务
if (memoryForTask(taskAttemptId) <= 0) {
memoryForTask.remove(taskAttemptId)
}
}
// 最后通知其他等待的线程
// 因为可能会有其他的任务在等待获取执行内存
lock.notifyAll() // Notify waiters in acquireMemory() that memory has been freed
}

MemoryManager.releaseAllExecutionMemoryForTask

把堆上的执行内存和直接内存的执行内存中该任务使用的内存都释放掉,

onHeapExecutionMemoryPool和offHeapExecutionMemoryPool是同一个类,只是一个记录执行内存对直接内存的使用,一个记录执行内存对堆内存的使用。

private[memory] def releaseAllExecutionMemoryForTask(taskAttemptId: Long): Long = synchronized {
onHeapExecutionMemoryPool.releaseAllMemoryForTask(taskAttemptId) +
offHeapExecutionMemoryPool.releaseAllMemoryForTask(taskAttemptId)
}

MemoryManager.releaseStorageMemory

对于存储内存的使用的记录并没有执行内存那么细,不会记录每个RDD使用了多少内存

def releaseStorageMemory(numBytes: Long, memoryMode: MemoryMode): Unit = synchronized {
memoryMode match {
case MemoryMode.ON_HEAP => onHeapStorageMemoryPool.releaseMemory(numBytes)
case MemoryMode.OFF_HEAP => offHeapStorageMemoryPool.releaseMemory(numBytes)
}
}

MemoryManager.releaseUnrollMemory

这里,我们看一下释放展开内存的方法,发现展开内存使用的就是存储内存。回顾一下BlockManager部分,展开内存的申请主要是在将数据通过MemoryStore存储成块时需要将数据临时放在内存中,这时就需要申请展开内存。

final def releaseUnrollMemory(numBytes: Long, memoryMode: MemoryMode): Unit = synchronized {
releaseStorageMemory(numBytes, memoryMode)
}

小结

从上面分析的几个释放内存的方法不难看出,所谓的释放内存其实只是对内存管理器内部的一些簿记量的改变,这就要求外部的调用者必须确保它们确实释放了这么多的内存,否则内存管理就会和实际的内存使用情况出现很大偏差。当然,好在内存管理器是spark内部的模块,并不向用户开放,所以在用户代码中不会调用内存管理模块。

UnifiedMemoryManager

开篇我们讲到,spark的内存管理器分为两种,而新的版本默认都是使用统一内存管理器UnifiedMemoryManager,后面静态内存管理器会逐渐启用,所以这里我们也重点分析统一内存管理。

前面,我们分析了父类MemoryManager中释放内存的几个方法,而申请内存的几个方法都是抽象方法,这些方法的实现都是在子类中,也就是UnifiedMemoryManager中实现的。

UnifiedMemoryManager.acquireExecutionMemory

这个方法是用来申请执行内存的。其中定义了几个局部方法,maybeGrowExecutionPool方法用来挤占存储内存以扩展执行内存空间;

computeMaxExecutionPoolSize方法用来计算最大的执行内存大小。

最后调用了executionPool.acquireMemory方法实际申请执行内存。

override private[memory] def acquireExecutionMemory(
numBytes: Long,
taskAttemptId: Long,
memoryMode: MemoryMode): Long = synchronized {
// 检查内存大小是否正确
assertInvariants()
assert(numBytes >= 0)
// 根据堆内存还是直接内存决定使用不同的内存池和内存大小
val (executionPool, storagePool, storageRegionSize, maxMemory) = memoryMode match {
case MemoryMode.ON_HEAP => (
onHeapExecutionMemoryPool,
onHeapStorageMemoryPool,
onHeapStorageRegionSize,
maxHeapMemory)
case MemoryMode.OFF_HEAP => (
offHeapExecutionMemoryPool,
offHeapStorageMemoryPool,
offHeapStorageMemory,
maxOffHeapMemory)
} /**
* Grow the execution pool by evicting cached blocks, thereby shrinking the storage pool.
*
* When acquiring memory for a task, the execution pool may need to make multiple
* attempts. Each attempt must be able to evict storage in case another task jumps in
* and caches a large block between the attempts. This is called once per attempt.
*/
// 通过挤占存储内存来扩张执行内存,
// 通过将缓存的块溢写到磁盘上,从而为执行内存腾出空间
def maybeGrowExecutionPool(extraMemoryNeeded: Long): Unit = {
if (extraMemoryNeeded > 0) {
// There is not enough free memory in the execution pool, so try to reclaim memory from
// storage. We can reclaim any free memory from the storage pool. If the storage pool
// has grown to become larger than `storageRegionSize`, we can evict blocks and reclaim
// the memory that storage has borrowed from execution.
// 我们可以将剩余的存储内存都借过来用作执行内存
// 另外,如果存储内存向执行内存借用了一部分内存,也就是说此时存储内存的实际大小大于配置的值
// 那么我们就将所有的借用的存储内存都还回来
val memoryReclaimableFromStorage = math.max(
storagePool.memoryFree,
storagePool.poolSize - storageRegionSize)
if (memoryReclaimableFromStorage > 0) {
// Only reclaim as much space as is necessary and available:
// 只腾出必要大小的内存空间,这个方法会将内存中的block挤到磁盘中
val spaceToReclaim = storagePool.freeSpaceToShrinkPool(
math.min(extraMemoryNeeded, memoryReclaimableFromStorage))
// 更新一些簿记量,存储内存少了这么多内存,相应的执行内存增加了这么多内存
storagePool.decrementPoolSize(spaceToReclaim)
executionPool.incrementPoolSize(spaceToReclaim)
}
}
} /**
* The size the execution pool would have after evicting storage memory.
*
* The execution memory pool divides this quantity among the active tasks evenly to cap
* the execution memory allocation for each task. It is important to keep this greater
* than the execution pool size, which doesn't take into account potential memory that
* could be freed by evicting storage. Otherwise we may hit SPARK-12155.
*
* Additionally, this quantity should be kept below `maxMemory` to arbitrate fairness
* in execution memory allocation across tasks, Otherwise, a task may occupy more than
* its fair share of execution memory, mistakenly thinking that other tasks can acquire
* the portion of storage memory that cannot be evicted.
*/
def computeMaxExecutionPoolSize(): Long = {
maxMemory - math.min(storagePool.memoryUsed, storageRegionSize)
} executionPool.acquireMemory(
numBytes, taskAttemptId, maybeGrowExecutionPool, () => computeMaxExecutionPoolSize)
}

ExecutionMemoryPool.acquireMemory

这个方法的代码我就不贴了,主要是一些复杂的内存申请规则的计算,以及内部簿记量的维护,此外如果现有可用的内存量太小,则会等待(通过对象锁等待)直到其他任务释放一些内存;

除此之外最重要的就是对上面提到的maybeGrowExecutionPool方法的调用,所以我们重点还是看一下maybeGrowExecutionPool方法。

maybeGrowExecutionPool

由于这个方法在前面已经贴出来,并且标上了很详细的注释,所以代码逻辑略过,其中有一个关键的调用storagePool.freeSpaceToShrinkPool,这个方法实现了将内存中的块挤出去的逻辑。

storagePool.freeSpaceToShrinkPool

我们发现其中调用了memoryStore.evictBlocksToFreeSpace方法,

def freeSpaceToShrinkPool(spaceToFree: Long): Long = lock.synchronized {
val spaceFreedByReleasingUnusedMemory = math.min(spaceToFree, memoryFree)
val remainingSpaceToFree = spaceToFree - spaceFreedByReleasingUnusedMemory
if (remainingSpaceToFree > 0) {
// If reclaiming free memory did not adequately shrink the pool, begin evicting blocks:
val spaceFreedByEviction =
memoryStore.evictBlocksToFreeSpace(None, remainingSpaceToFree, memoryMode)
// When a block is released, BlockManager.dropFromMemory() calls releaseMemory(), so we do
// not need to decrement _memoryUsed here. However, we do need to decrement the pool size.
spaceFreedByReleasingUnusedMemory + spaceFreedByEviction
} else {
spaceFreedByReleasingUnusedMemory
}
}

memoryStore.evictBlocksToFreeSpace

这个方法看似很长,其实大概可以总结为一点。

因为MemoryStore存储了内存中所有块的实际数据,所以可以根据这些信息知道每个块实际大小,这样就能计算出需要挤出哪些块,当然这个过程中还有一些细节的处理,比如块的写锁的获取和释放等等。

这里面,实际将块从内存中释放(本质上就是将块的数据对应的MemoryEntry的引用设为null,这样gc就可以回收这个块)的功能代码在blockEvictionHandler.dropFromMemory方法中实现,也就是

BlockManager.dropFromMemory。

private[spark] def evictBlocksToFreeSpace(
blockId: Option[BlockId],
space: Long,
memoryMode: MemoryMode): Long = {
assert(space > 0)
memoryManager.synchronized {
var freedMemory = 0L
val rddToAdd = blockId.flatMap(getRddId)
val selectedBlocks = new ArrayBuffer[BlockId]
def blockIsEvictable(blockId: BlockId, entry: MemoryEntry[_]): Boolean = {
entry.memoryMode == memoryMode && (rddToAdd.isEmpty || rddToAdd != getRddId(blockId))
}
// This is synchronized to ensure that the set of entries is not changed
// (because of getValue or getBytes) while traversing the iterator, as that
// can lead to exceptions.
entries.synchronized {
val iterator = entries.entrySet().iterator()
while (freedMemory < space && iterator.hasNext) {
val pair = iterator.next()
val blockId = pair.getKey
val entry = pair.getValue
if (blockIsEvictable(blockId, entry)) {
// We don't want to evict blocks which are currently being read, so we need to obtain
// an exclusive write lock on blocks which are candidates for eviction. We perform a
// non-blocking "tryLock" here in order to ignore blocks which are locked for reading:
// 这里之所以要获取写锁是为了防止在块正在被读取或写入的时候将其挤出去
if (blockInfoManager.lockForWriting(blockId, blocking = false).isDefined) {
selectedBlocks += blockId
freedMemory += pair.getValue.size
}
}
}
} def dropBlock[T](blockId: BlockId, entry: MemoryEntry[T]): Unit = {
val data = entry match {
case DeserializedMemoryEntry(values, _, _) => Left(values)
case SerializedMemoryEntry(buffer, _, _) => Right(buffer)
}
// 这里的调用将块挤出内存,如果允许写到磁盘则溢写到磁盘上
// 注意blockEvictionHandler的实现类就是BlockManager
val newEffectiveStorageLevel =
blockEvictionHandler.dropFromMemory(blockId, () => data)(entry.classTag)
if (newEffectiveStorageLevel.isValid) {
// The block is still present in at least one store, so release the lock
// but don't delete the block info
// 因为前面获取了这些块的写锁,还没有释放,
// 所以在这里释放这些块的写锁
blockInfoManager.unlock(blockId)
} else {
// The block isn't present in any store, so delete the block info so that the
// block can be stored again
// 因为块由于从内存中移除又没有写到磁盘上,所以直接从内部的簿记量中移除该块的信息
blockInfoManager.removeBlock(blockId)
}
} // 如果腾出的内存足够多,比申请的量要大,这时才会真正释放相应的块
if (freedMemory >= space) {
var lastSuccessfulBlock = -1
try {
logInfo(s"${selectedBlocks.size} blocks selected for dropping " +
s"(${Utils.bytesToString(freedMemory)} bytes)")
(0 until selectedBlocks.size).foreach { idx =>
val blockId = selectedBlocks(idx)
val entry = entries.synchronized {
entries.get(blockId)
}
// This should never be null as only one task should be dropping
// blocks and removing entries. However the check is still here for
// future safety.
if (entry != null) {
dropBlock(blockId, entry)
// 这时为测试留的一个钩子方法
afterDropAction(blockId)
}
lastSuccessfulBlock = idx
}
logInfo(s"After dropping ${selectedBlocks.size} blocks, " +
s"free memory is ${Utils.bytesToString(maxMemory - blocksMemoryUsed)}")
freedMemory
} finally {
// like BlockManager.doPut, we use a finally rather than a catch to avoid having to deal
// with InterruptedException
// 如果不是所有的块都转移成功,那么必然有的块的写锁可能没有释放
// 所以在这里将这些没有移除成功的块的写锁释放掉
if (lastSuccessfulBlock != selectedBlocks.size - 1) {
// the blocks we didn't process successfully are still locked, so we have to unlock them
(lastSuccessfulBlock + 1 until selectedBlocks.size).foreach { idx =>
val blockId = selectedBlocks(idx)
blockInfoManager.unlock(blockId)
}
}
}
} else {// 如果不能腾出足够多的内存,那么取消这次行动,释放所有已经持有的块的写锁
blockId.foreach { id =>
logInfo(s"Will not store $id")
}
selectedBlocks.foreach { id =>
blockInfoManager.unlock(id)
}
0L
}
}
}

BlockManager.dropFromMemory

总结一下这个方法的主要逻辑:

  • 如果存储级别允许存到磁盘,那么先溢写到磁盘上
  • 将block从MemoryStore内部的map结构中移除掉
  • 向driver上的BlockManagerMaster汇报块更新
  • 向任务度量系统汇报块更新的统计信息

所以,七绕八绕,饶了这么一大圈,其实所谓的内存挤占,其实就是把引用设为null _当然肯定不是这么简单啦,其实在整个分析的过程中我们也能发现,所谓的内存管理大部分工作就是对任务使用内存一些簿记量的管理维护,这里面有一些比较复杂的逻辑,例如给每个任务分配多少内存的计算逻辑就比较复杂。

private[storage] override def dropFromMemory[T: ClassTag](
blockId: BlockId,
data: () => Either[Array[T], ChunkedByteBuffer]): StorageLevel = {
logInfo(s"Dropping block $blockId from memory")
val info = blockInfoManager.assertBlockIsLockedForWriting(blockId)
var blockIsUpdated = false
val level = info.level // Drop to disk, if storage level requires
// 如果存储级别允许存到磁盘,那么先溢写到磁盘上
if (level.useDisk && !diskStore.contains(blockId)) {
logInfo(s"Writing block $blockId to disk")
data() match {
case Left(elements) =>
diskStore.put(blockId) { channel =>
val out = Channels.newOutputStream(channel)
serializerManager.dataSerializeStream(
blockId,
out,
elements.toIterator)(info.classTag.asInstanceOf[ClassTag[T]])
}
case Right(bytes) =>
diskStore.putBytes(blockId, bytes)
}
blockIsUpdated = true
} // Actually drop from memory store
val droppedMemorySize =
if (memoryStore.contains(blockId)) memoryStore.getSize(blockId) else 0L
val blockIsRemoved = memoryStore.remove(blockId)
if (blockIsRemoved) {
blockIsUpdated = true
} else {
logWarning(s"Block $blockId could not be dropped from memory as it does not exist")
} val status = getCurrentBlockStatus(blockId, info)
if (info.tellMaster) {
reportBlockStatus(blockId, status, droppedMemorySize)
}
// 向任务度量系统汇报块更新的统计信息
if (blockIsUpdated) {
addUpdatedBlockStatusToTaskMetrics(blockId, status)
}
status.storageLevel
}

UnifiedMemoryManager.acquireStorageMemory

我们再来看一下对于存储内存的申请。

其中,存储内存向执行内存借用 的逻辑相对简单,仅仅是将两个内存池的大小改一下,执行内存池减少一定的大小,存储内存池则增加相应的大小。

override def acquireStorageMemory(
blockId: BlockId,
numBytes: Long,
memoryMode: MemoryMode): Boolean = synchronized {
assertInvariants()
assert(numBytes >= 0)
val (executionPool, storagePool, maxMemory) = memoryMode match {
case MemoryMode.ON_HEAP => (
onHeapExecutionMemoryPool,
onHeapStorageMemoryPool,
maxOnHeapStorageMemory)
case MemoryMode.OFF_HEAP => (
offHeapExecutionMemoryPool,
offHeapStorageMemoryPool,
maxOffHeapStorageMemory)
}
// 因为执行内存挤占不了,所以这里如果申请的内存超过现在可用的内存,那么就申请不了了
if (numBytes > maxMemory) {
// Fail fast if the block simply won't fit
logInfo(s"Will not store $blockId as the required space ($numBytes bytes) exceeds our " +
s"memory limit ($maxMemory bytes)")
return false
}
// 如果大于存储内存的可用内存,那么就需要向执行内存借用一部分内存
if (numBytes > storagePool.memoryFree) {
// There is not enough free memory in the storage pool, so try to borrow free memory from
// the execution pool.
val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree,
numBytes - storagePool.memoryFree)
// 存储内存向执行内存借用的逻辑很简单,
// 仅仅是将两个内存池的大小改一下,
// 执行内存池减少一定的大小,存储内存池则增加相应的大小
executionPool.decrementPoolSize(memoryBorrowedFromExecution)
storagePool.incrementPoolSize(memoryBorrowedFromExecution)
}
// 通过storagePool申请一定量的内存
storagePool.acquireMemory(blockId, numBytes)
}

StorageMemoryPool.acquireMemory

def acquireMemory(
blockId: BlockId,
numBytesToAcquire: Long,
numBytesToFree: Long): Boolean = lock.synchronized {
assert(numBytesToAcquire >= 0)
assert(numBytesToFree >= 0)
assert(memoryUsed <= poolSize)
// 首先调用MemoryStore的相关方法挤出一些块以释放内存
if (numBytesToFree > 0) {
memoryStore.evictBlocksToFreeSpace(Some(blockId), numBytesToFree, memoryMode)
}
// NOTE: If the memory store evicts blocks, then those evictions will synchronously call
// back into this StorageMemoryPool in order to free memory. Therefore, these variables
// should have been updated.
// 因为前面挤出一些块后释放内存时,BlockManager会通过MemoryManager相关方法更新内部的簿记量,
// 所以这里的memoryFree就会变化,会变大
val enoughMemory = numBytesToAcquire <= memoryFree
if (enoughMemory) {
_memoryUsed += numBytesToAcquire
}
enoughMemory
}

可以看到,这里也调用了memoryStore.evictBlocksToFreeSpace方法来讲一部分块挤出内存,以此来为新的block腾出空间。

UnifiedMemoryManager.acquireUnrollMemory

另外还有对展开内存的申请,实际就是申请存储内存。

override def acquireUnrollMemory(
blockId: BlockId,
numBytes: Long,
memoryMode: MemoryMode): Boolean = synchronized {
acquireStorageMemory(blockId, numBytes, memoryMode)
}

总结

内存管理,本质上是对shuffle排序过程中使用的内存和rdd缓存使用的内存的簿记,通过对内存使用量的详细精确的记录和管理,最大限度避免OOM的发生,同时尽量提高内存利用率。

spark内存管理器--MemoryManager源码解析的更多相关文章

  1. Python内存管理机制-《源码解析》

    Python内存管理机制 Python 内存管理分层架构 /* An object allocator for Python. Here is an introduction to the layer ...

  2. 内存管理pbuf.c源码解析——LwIP学习

    声明:个人所写所有博客均为自己在学习中的记录与感想,或为在学习中总结他人学习成果,但因本人才疏学浅,如果大家在阅读过程中发现错误,欢迎大家指正. 本文自己尚有认为写的不完整的地方,源代码没有完全理清, ...

  3. DRF-解析器组件源码解析

    解析器组件源码解析 解析器组件源码解析 1 执行request.data 开始找重装的request中的data方法 2 在dispatch找到重装的request def dispatch(self ...

  4. Django框架 之 admin管理工具(源码解析)

    浏览目录 单例模式 admin执行流程 admin源码解析 单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在.当你希望在 ...

  5. Spark Streaming运行流程及源码解析(一)

    本系列主要描述Spark Streaming的运行流程,然后对每个流程的源码分别进行解析 之前总听同事说Spark源码有多么棒,咱也不知道,就是疯狂点头.今天也来撸一下Spark源码. 对Spark的 ...

  6. Spark Core 1.3.1源码解析及个人总结

    本篇源码基于赵星对Spark 1.3.1解析进行整理.话说,我不认为我这下文源码的排版很好,不能适应的还是看总结吧. 虽然1.3.1有点老了,但对于standalone模式下的Master.Worke ...

  7. spring事务管理器的源码和理解

    原文出处: xieyu_zy 以前说了大多的原理,今天来说下spring的事务管理器的实现过程,顺带源码干货带上. 其实这个文章唯一的就是带着看看代码,但是前提你要懂得动态代理以及字节码增强方面的知识 ...

  8. 【Cocos2d-x 3.x】内存管理机制与源码分析

    侯捷先生说过这么一句话 :  源码之前,了无秘密. 要了解Cocos2d-x的内存管理机制,就得阅读源码. 接触Cocos2d-x时, Cocos2d-x的最新版本已经到了3.2的时代,在学习Coco ...

  9. 【面试】足够“忽悠”面试官的『Spring事务管理器』源码阅读梳理(建议珍藏)

    PS:文章内容涉及源码,请耐心阅读. 理论实践,相辅相成 伟大领袖毛主席告诉我们实践出真知.这是无比正确的.但是也会很辛苦. 就像淘金一样,从大量沙子中淘出金子一定是一个无比艰辛的过程.但如果真能淘出 ...

随机推荐

  1. 小学四则运算口算练习app---No.1

    因为对app不是很了解,对环境的配置也不是很舒心,今天主要配置了环境,了解了一些相关app的简单操作以及安卓stdiuo的使用!如下: 我自己连接的自己的手机(还是不要拿自己的手机做测试哦!模拟器虽然 ...

  2. 安卓设备连接Mac的简单方法

    mac设备是苹果出品的桌面系统,以高冷而闻名,不同于我们平常使用的windows系统,mac系统对软件硬件的兼容性很差,将iOS 设备(iPhone.iPad和iPod)连接至Mac是一件很简单的事, ...

  3. 网络测试工具netperf(转)

    http://pangyi.github.io/blog/20141210/wang-luo-ce-shi-gong-ju-netperf/ 网络测试工具netperf 2014年12月10日 一般我 ...

  4. Ansible之playbook的使用

    playbook介绍 一. 为什么引入playbook 我们完成一个任务,例如安装部署一个httpd服务,我们需要多个模块(一个模块也可以称之为task)提供功能来完成.而playbook就是组织多个 ...

  5. jvm参数设置实例

  6. gamma测试报告

    Gamma阶段测试报告 测试计划及结果 我们针对测试做了比较多的改进. 测试代码分为针对纯java部分的单元测试和需要android运行环境的自动化仪器化测试 单元测试 这一部分基本继承Beta阶段的 ...

  7. Shell脚本之八 函数

    一.函数定义 Linux shell 可以用户定义函数,然后在shell脚本中可以随便调用. shell中函数的定义格式如下: [ function ] funname [()] { action; ...

  8. cad.net 获取所有已经安装的cad版本信息

    计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\Hardcopy var ackey = Registry.LocalMachine.OpenSubKey(@&quo ...

  9. 【C++】C++中基类的析构函数为什么要用virtual虚析构函数?

    正面回答: 当基类的析构函数不是虚函数,并且基类指针指向一个派生类对象,然后通过基类指针来删除这个派生类对象时,如果基类的析构函数不是虚析构函数,那么派生类的析构函数就不会被调用,从而产生内存泄漏 # ...

  10. Java自学-类和对象 类属性

    Java的类属性和对象属性 当一个属性被static修饰的时候,就叫做类属性,又叫做静态属性 当一个属性被声明成类属性,那么所有的对象,都共享一个值 与对象属性对比: 不同对象的 对象属性 的值都可能 ...