Spark源码阅读03-Spark存储原理之存储分析,2024年最新快手java面试算法

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

RDD 包含多个 Partition, 每个 Partition 对应一个数据块 Block, 那么每个RDD 中包含一个或多个数据块 Block, 每个 Block 拥有唯一的编号BlockId,对应数据块编号规则:“rdd_” + rddId + ‘’_" + splitIndex。其中splitIndex为该数据块对应Partition的序列号。

在 persist 方法中并没有发生数据存储操作动作, 实际发生数据操作是任务运行过程中, RDD 调用 iterator 方法时发生的。 在调用过程中, 先根据数据块 Block 编号在 判断是否已经按照指定的存储级别进行存储, 如果存在该数据块 Block, 则从本地或远程节点读取数据; 如果不存在该数据块 Block, 则调用 RDD 的计算方法得出结果, 并把结果按照指定 的存储级别进行存储。 RDD 的 iterator 方法代码如下:

final def iterator(split: Partition, context: TaskContext): Iterator[T] = {

if (storageLevel != StorageLevel.NONE) {

//如果存在存储级别,尝试读取内存的数据进行迭代计算

getOrCompute(split, context)

} else {

//如果不存在存储级别,则直接读取数据进行迭代计算或者读取检查点结果进行迭代计算

computeOrReadCheckpoint(split, context)

}

}

其中调用的getOrCompute 方法是存储逻辑的核心, 代码如下:

private[spark] def getOrCompute(partition: Partition, context: TaskContext): Iterator[T] = {

//通过RDD的编号和partition序号获取数据块block的编号

val blockId = RDDBlockId(id, partition.index)

var readCachedBlock = true

// This method is called on executors, so we need call SparkEnv.get instead of sc.env.

//由于该方法由executor调用,可使用sparkEnv代替sc.env

//根据数据块block编号先读取数据,然后再更新数据,这里是读写数据的入口点(getOrElseUpdate)

SparkEnv.get.blockManager.getOrElseUpdate(blockId, storageLevel, elementClassTag, () => {

//如果数据块不在内存,则尝试读取检查点结果进行迭代计算

readCachedBlock = false

computeOrReadCheckpoint(partition, context)

}) match {

//对getOrElseUpdate返回结果进行处理,该结果表示处理成功,记录结果度量信息

case Left(blockResult) =>

if (readCachedBlock) {

val existingMetrics = context.taskMetrics().inputMetrics

existingMetrics.incBytesRead(blockResult.bytes)

new InterruptibleIterator[T](context, blockResult.data.asInstanceOf[Iterator[T]]) {

override def next(): T = {

existingMetrics.incRecordsRead(1)

delegate.next()

}

}

} else {

new InterruptibleIterator(context, blockResult.data.asInstanceOf[Iterator[T]])

}

//对getOrElseUpdate返回结果进行处理,该结果表示处理失败,把该结果返回给调用者,由其决定如何处理

case Right(iter) =>

new InterruptibleIterator(context, iter.asInstanceOf[Iterator[T]])

}

}

在getOrCompute调用getOrElseUpdate方法, 该方法是存储读写数据的入口点:

//该方法是存储读写数据的入口点

def getOrElseUpdate[T](

blockId: BlockId,

level: StorageLevel,

classTag: ClassTag[T],

makeIterator: () => Iterator[T]): Either[BlockResult, Iterator[T]] = {

// Attempt to read the block from local or remote storage. If it’s present, then we don’t need

// to go through the local-get-or-put path.

//读取数据块入口,尝试从本地数据或者远程读取数据

getT(classTag) match {

case Some(block) =>

return Left(block)

case _ =>

// Need to compute the block.

}

// Initially we hold no locks on this block.

//写输入入口

doPutIterator(blockId, makeIterator, level, classTag, keepReadLock = true) match {

case None =>

val blockResult = getLocalValues(blockId).getOrElse {

releaseLock(blockId)

throw new SparkException(s"get() failed for block $blockId even though we held a lock")

}

releaseLock(blockId)

Left(blockResult)

case Some(iter) =>

Right(iter)

}

}

读数据过程

BlockManager的get方法是读数据的入口点, 在读取时分为本地读取和远程节点读取两个步骤。本地读取使用getLocalValues方法,在该方法中根据不同的存储级别直接调用不同存储实现的方法:而远程节点读取使用getRemote Values方法,在getRemoteValues 方法中调用了 getRemoteBytes方法,在方法中调用远程数据传输服务类BlockTransferService的fetchBlockSync 进行处理,使用Netty的fetchBlocks方法获取数据。整个数据读取类调用如下:

在这里插入图片描述

本地读取

本地读取根据不同的存储级别分为内存和磁盘两种读取方式。其介绍分别如下:

1. 内存读取

在getLocalValues方法中,读取内存中的数据根据返回的是封装成BlockResult类型还是数据流,分别调用MemoryStore的getValues和getBytes两种方法,代码如下:

def getLocalValues(blockId: BlockId): Option[BlockResult] = {

//使用内存存储级别,并且数据存储在内存情况

if (level.useMemory && memoryStore.contains(blockId)) {

val iter: Iterator[Any] = if (level.deserialized) {

//如果存储时使用反序列化,则直接读取内存中的数据

memoryStore.getValues(blockId).get

} else {

//如果存储时未使用反序列化,则内存中的数据后做反序列化处理

serializerManager.dataDeserializeStream(

blockId, memoryStore.getBytes(blockId).get.toInputStream())(info.classTag)

}

// We need to capture the current taskId in case the iterator completion is triggered

// from a different thread which does not have TaskContext set; see SPARK-18406 for

// discussion.

//数据读取完毕后,返回数据及数据块大小、读取方法等信息

val ci = CompletionIterator[Any, Iterator[Any]](iter, {

releaseLock(blockId, taskAttemptId)

})

Some(new BlockResult(ci, DataReadMethod.Memory, info.size))

}

}

在MemoryStore的getValues和getBytes方法中,最终都是通过数据块编号获取内存中的数据, 其代码为:

val entry = en七ries.synchronized { entries.get(blockld) }

2.磁盘读取

磁盘读取在getLocalValues方法中, 调用的是DiskStore的getBytes方法, 在读取磁盘中的数据后需要把这些数据缓存到内存中, 代码实现如下:

def getLocalValues(blockId: BlockId): Option[BlockResult] = {

else if (level.useDisk && diskStore.contains(blockId)) {

//从磁盘获取数据,由于保存到磁盘中的数据是序列化的,读取到的数据也是序列化的

val diskData = diskStore.getBytes(blockId)

val iterToReturn: Iterator[Any] = {

if (level.deserialized) {

//如果存储级别需要反序列化,则把读取数据反序列化,然后存储到内存中去

val diskValues = serializerManager.dataDeserializeStream(

blockId,

diskData.toInputStream())(info.classTag)

maybeCacheDiskValuesInMemory(info, blockId, level, diskValues)

} else {

//如果存储级别不需要反序列化,则直接把这些序列化数据存储到内存中

val stream = maybeCacheDiskBytesInMemory(info, blockId, level, diskData)

.map { _.toInputStream(dispose = false) }

.getOrElse { diskData.toInputStream() }

//返回的数据需进行反序列化处理

serializerManager.dataDeserializeStream(blockId, stream)(info.classTag)

}

}

//数据读取完毕后,返回数据及数据块大小,读取方法等信息

val ci = CompletionIterator[Any, Iterator[Any]](iterToReturn, {

releaseLockAndDispose(blockId, diskData, taskAttemptId)

})

Some(new BlockResult(ci, DataReadMethod.Disk, info.size))

}

在DiskStore中的getBytes方法中, 调用DiskBlockManager的getfile方法获取数据块所在文件的句柄。该文件名为数据块的文件名,文件所在一级目录和二级子目录索引值通过文件名的哈希值取模获取,其代码实现如下:

def getFile(filename: String): File = {

//根据文件名的哈希值获取一级目录和二级目录索引值,其中一级目录索引值为哈希值与一级目录个数的模,

// 而二级目录索引值为哈希值与二级子目录个数的模

val hash = Utils.nonNegativeHash(filename)

val dirId = hash % localDirs.length

val subDirId = (hash / localDirs.length) % subDirsPerLocalDir

//先通过一级目录和二级目录索引值获取该目录,然后判断该目录是否存在

val subDir = subDirs(dirId).synchronized {

val old = subDirs(dirId)(subDirId)

if (old != null) {

old

} else {

//如果该目录不存在则创建该目录,范围为00-63

val newDir = new File(localDirs(dirId), “%02x”.format(subDirId))

if (!newDir.exists() && !newDir.mkdir()) {

throw new IOException(s"Failed to create local dir in $newDir.")

}

//判断该文件是否存在,如果不存在,则创建

subDirs(dirId)(subDirId) = newDir

newDir

}

}

//通过文件的路径获取文件的句柄并返回

new File(subDir, filename)

}

获取文件句柄后, 读取整个文件内容。其代码如下:

def getBytes(blockId: BlockId): BlockData = {

//获取数据块所在文件的句柄

val file = diskManager.getFile(blockId.name)

val blockSize = getSize(blockId)

securityManager.getIOEncryptionKey() match {

case Some(key) =>

new EncryptedBlockData(file, blockSize, conf, key)

case _ =>

new DiskBlockData(minMemoryMapBytes, maxMemoryMapBytes, file, blockSize)

}

}

远程读取

在远程节点读取数据的时候, Spark只提供了Netty远程读取方式,下面分析Netty远程数据读取过程。 在Spark中主要由下而两个类处理Netty远程数据读取:

  • NettyBlockTransferService: 该类向Shuffle、 存储模块提供了数据存取的接口, 接收到数据存取的命令时, 通过Netty的RPC架构发送消息给指定节点, 请求进行数据存取操作。

  • NettyBlockRpcServer: 当Executor启动时, 同时会启动RCP监听器, 当监听到消息 把消息传递到该类进行处理, 消息内容包括读取数据OpenBlocks 和写入数据Upload Block两种。

使用Netty处理远程数据读取流程如下:

(1)Spark远程读取数据入口为getRemoteValues, 然后调用getRemoteBytes方法, 在该方法中调用sortLocations方法先向BlockManagerMasterEndpoint 终端点发送SortLocations消息,请求据数据块所在的位置信息。 当Driver的终端点接收到请求消息时, 根据数据块的编号获取该数据块所在的位置列表, 根据是否是本地节点数据对位置列表进行排序。 其中BlockManager 类中的sortLocations方法代码片段如下:

private def sortLocations(locations: Seq[BlockManagerId]): Seq[BlockManagerId] = {

//获取数据块节点所在节点的信息

val locs = Random.shuffle(locations)

//从获取的节点信息中,优先读取本地节点数据

val (preferredLocs, otherLocs) = locs.partition { loc => blockManagerId.host == loc.host }

blockManagerId.topologyInfo match {

case None => preferredLocs ++ otherLocs

case Some(_) =>

val (sameRackLocs, differentRackLocs) = otherLocs.partition {

loc => blockManagerId.topologyInfo == loc.topologyInfo

}

preferredLocs ++ sameRackLocs ++ differentRackLocs

}

}

获取数据块的位置列表后,在BlockManager.getRemoteBytes方法中调用BlockTransferService提供的fetchBlockSync方法进行读取远程数据。代码实现如下:

def getRemoteBytes(blockId: BlockId): Option[ChunkedByteBuffer] = {

var runningFailureCount = 0

var totalFailureCount = 0

//获取数据块的位置

val locations = sortLocations(blockLocations)

val maxFetchFailures = locations.size

var locationIterator = locations.iterator

while (locationIterator.hasNext) {

val loc = locationIterator.next()

logDebug(s"Getting remote block $blockId from $loc")

//通过blockTransferService提供的fetchBlockSync方法远程获取数据

val data = try {

blockTransferService.fetchBlockSync(

loc.host, loc.port, loc.executorId, blockId.toString, tempFileManager)

} catch {

}

//获取到数据后,返回该数据块

if (data != null) {

if (remoteReadNioBufferConversion) {

return Some(new ChunkedByteBuffer(data.nioByteBuffer()))

} else {

return Some(ChunkedByteBuffer.fromManagedBuffer(data))

}

}

logDebug(s"The value of block $blockId is null")

}

logDebug(s"Block $blockId not found")

None

}

(2)调用远程数据传输服务BlockTransferService的fetchBlockSync方法后,在该方法继续调用fetchBlocks方法。

override def fetchBlocks(

host: String,

port: Int,

execId: String,

blockIds: Array[String],

listener: BlockFetchingListener,

tempFileManager: DownloadFileManager): Unit = {

logTrace(s"Fetch blocks from h o s t : host: host:port (executor id $execId)")

try {

val blockFetchStarter = new RetryingBlockFetcher.BlockFetchStarter {

override def createAndStart(blockIds: Array[String], listener: BlockFetchingListener) {

//根据远程节点的节点和端口创建通信客户端

val client = clientFactory.createClient(host, port)

//通过该客户端向指定节点发送获取数据消息

new OneForOneBlockFetcher(client, appId, execId, blockIds, listener,

transportConf, tempFileManager).start()

}

}

}

其中发送读取消息是在OneForOneBlockFetcher类中实现,在该类中的构造函数定义了该消息this.openMessage = new OpenBlocks(appld, execld, blocklds),然后在该类的start方法中向RPC客户端发送消息:

public void start() {

if (blockIds.length == 0) {

throw new IllegalArgumentException(“Zero-sized blockIds array”);

}

//通过客户端发送读取数据块的消息

client.sendRpc(openMessage.toByteBuffer(), new RpcResponseCallback() {

@Override

public void onSuccess(ByteBuffer response) {

try {

}

});

}

(3)远程节点的RPC服务端接收到客户端发送消息时, 在NettyBI ockRpcServer类中对悄息进行匹配。 如果是诮求读取悄息时, 则调用BlockManager的getBlockData方法议取该节点 卜的数据,读取的数据块封装为ManagedBuffer序列缓存在内存中,然后使用Netty提供的传输心迫, 把数据传递到谓求节点上, 完成远程传输任务。

override def receive(

client: TransportClient,

rpcMessage: ByteBuffer,

responseContext: RpcResponseCallback): Unit = {

val message = BlockTransferMessage.Decoder.fromByteBuffer(rpcMessage)

logTrace(s"Received request: $message")

message match {

case openBlocks: OpenBlocks =>

val blocksNum = openBlocks.blockIds.length

//调用blockManager的getBlockData读取该节点上的数据

val blocks = for (i <- (0 until blocksNum).view)

yield blockManager.getBlockData(BlockId.apply(openBlocks.blockIds(i)))

//注册ManagedBuffer序列,利用Netty传输通道进行传输数据

val streamId = streamManager.registerStream(appId, blocks.iterator.asJava,

client.getChannel)

logTrace(s"Registered streamId $streamId with $blocksNum buffers")

responseContext.onSuccess(new StreamHandle(streamId, blocksNum).toByteBuffer)

}

}

写数据过程

BlockManager的doPutlterator方法是写数据的入口点。 在该方法中, 根据数据是否缓存到内存中进行处理。 如果不缓存到内存中, 则调用BlockManager 的putIterator方法直接存储磁盘: 如果缓存到内存中, 则先判断数据存储级别是否进行了反序列化。如果设置反序列化, 则说明获取的数据为值类型, 调用putlteratorAsValues方法把数据存入内存;如果没有设置反序列化,则获取的数据为字节类型,调用putIteratorAsBytes方法把数据存入内存中。在把数据存入内存过程中,要判断在内存中展开该数据大小是否足够,当足够时调用BlockManager的putArray方法写入内存,否则把数据写入到磁盘。

在写入数据完成时,一方面把数据块的元数据发送给Driver端的BockManagerMasterEndpoint终端点,请求其更新数据元数据,另一方面判断是否需要创建数据副本,如果需要则调用replicate方法,把数据写到远程节点上,类似于读取远程节点数据,Spark提供Netty方式写输入。数据写入类调用关系如下:

在这里插入图片描述

通过上面的方法调用图, 可以知道在BlockManager 的 doPutlterator 方法中根据存储级别和 数据类型确定调用的方法, 当存储级别为内存时, 调用 MemoryStore的写入方法; 当存储级别为硬盘时, 调用 DiskStore的写入方法。 BlockManager.doPutlterator 代码如下所示:

private def doPutIterator[T](

blockId: BlockId,

iterator: () => Iterator[T],

level: StorageLevel,

classTag: ClassTag[T],

tellMaster: Boolean = true,

keepReadLock: Boolean = false): Option[PartiallyUnrolledIterator[T]] = {

//辅助类,用于获取数据块信息,并对写数据结果进行处理

doPut(blockId, level, classTag, tellMaster = tellMaster, keepReadLock = keepReadLock) { info =>

val startTimeMs = System.currentTimeMillis

var iteratorFromFailedMemoryStorePut: Option[PartiallyUnrolledIterator[T]] = None

var size = 0L

//把数据写到内存中

if (level.useMemory) {

//如果设置反序列化,则说明获取的数据为数值类型,调用putIteratorAsValues方法

//把数据存入内存

if (level.deserialized) {

memoryStore.putIteratorAsValues(blockId, iterator(), classTag) match {

//写入数据成功,返回数据块的大小

case Right(s) =>

size = s

//数据写入内存失败,如果存储级别设置写入磁盘,则写到磁盘中,否则返回结果

case Left(iter) =>

// Not enough space to unroll this block; drop to disk if applicable

if (level.useDisk) {

logWarning(s"Persisting block $blockId to disk instead.")

diskStore.put(blockId) { channel =>

val out = Channels.newOutputStream(channel)

serializerManager.dataSerializeStream(blockId, out, iter)(classTag)

}

size = diskStore.getSize(blockId)

} else {

iteratorFromFailedMemoryStorePut = Some(iter)

}

}

} else { // !level.deserialized

//如果没有设置反序列化,则获取的数据类型为字节类型,调用putIteratorAsBytes方法

//把数据存到内存中

memoryStore.putIteratorAsBytes(blockId, iterator(), classTag, level.memoryMode) match {

//写入数据成功,返回数据块的大小

case Right(s) =>

size = s

//数据写入内存失败,如果存储级别设置写入磁盘,则写到磁盘中,否则返回结果

case Left(partiallySerializedValues) =>

if (level.useDisk) {

logWarning(s"Persisting block $blockId to disk instead.")

diskStore.put(blockId) { channel =>

val out = Channels.newOutputStream(channel)

partiallySerializedValues.finishWritingToStream(out)

}

size = diskStore.getSize(blockId)

} else {

iteratorFromFailedMemoryStorePut = Some(partiallySerializedValues.valuesIterator)

}

}

}

}

//调用diskStore的put方法把数据写到磁盘中

else if (level.useDisk) {

diskStore.put(blockId) { channel =>

val out = Channels.newOutputStream(channel)

serializerManager.dataSerializeStream(blockId, out, iterator())(classTag)

}

size = diskStore.getSize(blockId)

}

val putBlockStatus = getCurrentBlockStatus(blockId, info)

val blockWasSuccessfullyStored = putBlockStatus.storageLevel.isValid

if (blockWasSuccessfullyStored) {

// Now that the block is in either the memory or disk store, tell the master about it.

//如果成功写入,则把该数据块的元数据发送给Driver端

info.size = size

if (tellMaster && info.tellMaster) {

reportBlockStatus(blockId, putBlockStatus)

}

addUpdatedBlockStatusToTaskMetrics(blockId, putBlockStatus)

logDebug(“Put block %s locally took %s”.format(blockId, Utils.getUsedTimeMs(startTimeMs)))

//如果需要创建副本,则根据数据块编号获取数据复制到其他节点上

if (level.replication > 1) {

val remoteStartTime = System.currentTimeMillis

val bytesToReplicate = doGetLocalBytes(blockId, info)

// [SPARK-16550] Erase the typed classTag when using default serialization, since

// NettyBlockRpcServer crashes when deserializing repl-defined classes.

// TODO(ekl) remove this once the classloader issue on the remote end is fixed.

val remoteClassTag = if (!serializerManager.canUseKryo(classTag)) {

scala.reflect.classTag[Any]

} else {

classTag

}

try {

replicate(blockId, bytesToReplicate, level, remoteClassTag)

} finally {

bytesToReplicate.dispose()

}

logDebug(“Put block %s remotely took %s”

.format(blockId, Utils.getUsedTimeMs(remoteStartTime)))

}

}

assert(blockWasSuccessfullyStored == iteratorFromFailedMemoryStorePut.isEmpty)

iteratorFromFailedMemoryStorePut

}

}

在Spark中写入数据分别内存和磁盘两种方式,其对应写入过程如下:

写入内存

在内存处理类 MemoryStore 中, 存在两种写入方法, 分别为 putlteratorAsValues 和putlteratorAsBytes。这两个方法区别在于写入内存的数据类型不同, putfteratorAsValues 针对的是值类型的数据写入, 而 putiteratorAsBytes 针对的是字节码数据的写入。 这两个方法写入内存过程基本类似, 下面以 putlteratorAsValues 讲解写入过程。

  • (1)在数据块展升前,为该展开线程获取初始化内存,该内存大小为 unrollMemoryThreshold,获取完毕后返回是否成功的结果keepUnrolling 。

  • (2)如果lterator[T]存在元素且keepUnrolling为真, 则继续向前遍历lterator[T], 内存展开元素的数量elementsUnrolled自增1。 如果遍历Iterator[T]到头或者keepUnrolling为假, 则跳到 步骤(4)。

  • (3)当每memoryCheckPeriod即16次展升动作后,进行yi 次检查展开的内存大小是否超过当前分配的内存。 如果没有超过则继续展开, 如果不足则根据增长因子计算需要增加的内存大小, 然后根据该大小申请, 申请增加的内存大小: 当前展开大小*内存增长因子-当前分配的内 存大小。如果申请成功,则把内存大小加入到已使用内存中,而该展开线桯获取的内存大小为: 当前展开大小*内存增长因子。

  • (4)判断数据块是否在内存中成功展开,如果展开失败,则记录内存不足并退出: 如果展开成功,则继续下一步骤。

  • (5)先估算该数据块在内存中存储的大小, 然后比较数据块展开的内存和数据块在内存中存储的大小, 如果数据块展开的内存<=数据块存储的大小, 说明屈开内存的大小不足以存储数 据块, 需要申请它们之间的差值, 如果申请成功, 则调用transferUnrollToStorage方法处理:数据块展开的内存>数据块存储的大小,说明展开内存的大小足以存储数据块,那么先释放多余的内存, 然后调用transferUnrollToStorage方法处理。

  • (6)在transferUnrollTotorage方法中释放该数据块在内存展开的空间,然后判断内存是否足够用于写入数据。如果有足够的内存,则把数据块放到内存的entries中,否则返回内存不足, 写入内存失败的消息。

private[storage] def putIteratorAsValues[T](

blockId: BlockId,

values: Iterator[T],

classTag: ClassTag[T]): Either[PartiallyUnrolledIterator[T], Long] = {

val valuesHolder = new DeserializedValuesHolderT

putIterator(blockId, values, classTag, MemoryMode.ON_HEAP, valuesHolder) match {

case Right(storedSize) => Right(storedSize)

case Left(unrollMemoryUsedByThisBlock) =>

val unrolledIterator = if (valuesHolder.vector != null) {

valuesHolder.vector.iterator

} else {

valuesHolder.arrayValues.toIterator

}

Left(new PartiallyUnrolledIterator(

this,

MemoryMode.ON_HEAP,

unrollMemoryUsedByThisBlock,

unrolled = unrolledIterator,

rest = values))

}

}

private def putIterator[T](

blockId: BlockId,

values: Iterator[T],

classTag: ClassTag[T],

memoryMode: MemoryMode,

valuesHolder: ValuesHolder[T]): Either[Long, Long] = {

require(!contains(blockId), s"Block $blockId is already present in the MemoryStore")

最后

分享一些资料给大家,我觉得这些都是很有用的东西,大家也可以跟着来学习,查漏补缺。

《Java高级面试》

《Java高级架构知识》

《算法知识》

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

t(unrollMemoryUsedByThisBlock) =>

val unrolledIterator = if (valuesHolder.vector != null) {

valuesHolder.vector.iterator

} else {

valuesHolder.arrayValues.toIterator

}

Left(new PartiallyUnrolledIterator(

this,

MemoryMode.ON_HEAP,

unrollMemoryUsedByThisBlock,

unrolled = unrolledIterator,

rest = values))

}

}

private def putIterator[T](

blockId: BlockId,

values: Iterator[T],

classTag: ClassTag[T],

memoryMode: MemoryMode,

valuesHolder: ValuesHolder[T]): Either[Long, Long] = {

require(!contains(blockId), s"Block $blockId is already present in the MemoryStore")

最后

分享一些资料给大家,我觉得这些都是很有用的东西,大家也可以跟着来学习,查漏补缺。

《Java高级面试》

[外链图片转存中…(img-SOsJF0V4-1713647995549)]

《Java高级架构知识》

[外链图片转存中…(img-2qUKGvWl-1713647995549)]

《算法知识》

[外链图片转存中…(img-HC7LJLgw-1713647995550)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-Vh4Kyfco-1713647995550)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值