Go语言设计与实现 -- 内存管理器

news2024/12/28 5:39:48

不同的编程语言选择不同的方式管理内存,本节会介绍Go语言内存分配器。

Go内存分配的设计思想是:

  • 内存分配算法采用Google的TCMalloc算法,每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向加锁向全局内存池申请,减少系统调用并且避免不同线程对全局内存池的锁竞争
  • 把内存切分的非常细小,分为多级管理,以降低锁的粒度
  • 回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候才会尝试归还给操作系统,降低整体开销。

接下来就开始详细的讲解:

设计原理

  • 用户程序(mutator)
  • 内存分配器(allocator,简称分配器)
  • 垃圾收集器(collector,简称收集器)

在这里插入图片描述

当用户程序申请内存时,它会通过内存分配器重新申请新内存。内存分配器会负责从堆中初始化相应的内存区域。

分配方法

内存分配器一般包含两种分配方法:

  • 线性分配器
  • 空闲链表分配器

线性分配器

线性分配器具有较快的执行速度和简单的实现,但是它的缺点就是容易产生内存碎片。

bump-allocator

bump-allocator-reclaim-memory

图片来自于面向信仰编程

它很难利用红色的这个内存了。

因此我们需要结合其他的垃圾收集算法来整理存活对象的碎片,定期合并空闲内存。

例如有:

  • 标记压缩法
  • 复制回收
  • 分代回收

空闲的链表分配器

free-list-allocator

当用户程序申请内存时,空闲链表分配器会依次遍历空闲内存块,找到足够大的内存,然后申请新资源并修改链表。

因为不同的内存块通过指针构成了链表,所以使用这种方式的分配器可以重新利用回收的资源,但是因为分配器分配内存时需要遍历链表,所以它的时间复杂度是O(N)。空闲链表分配器可以选择不同的策略在链表的内存块中进行选择:

  • 首次适应 – 从链表头开始遍历,找到第一个大小大于申请内存的内存块
  • 循环首次适应 – 从上次遍历的结束位置开始遍历,选择第一个大小大于申请内存的内存块
  • 最优适应 – 从链表头遍历整个链表,选择最合适的内存块
  • 隔离适应 – 将内存分割成多个链表,每个链表中的内存块大小相同,申请内存时先找到满足条件的链表,再从链表中选择合适的内存块

Go语言使用的内存分配策略与第4种策略有些相似

segregated-list

该策略会将内存分割成4,8,16,32字节的内存块组成的链表,当我们向内存分配器申请8字节的内存时,它会找到满足条件的内存块并返回。这样可以减少遍历次数。

分级分配

线程缓存分配是用于分配内存的机制,它比glibc种的malloc还要快很多。Go语言的内存分配器借鉴了TCMalloc(线程缓存分配)。其核心理念是使用多级缓存将对象根据大小分类

对象大小

类型大小
微对象(0, 16B)
小对象[16B, 32KB]
大对象(32KB, 正无穷)

因为程序中的绝大多数对象的大小都在 32KB 以下,而申请的内存大小影响 Go 语言运行时分配内存的过程和开销,所以分别处理大对象和小对象有利于提高内存分配器的性能。

多级缓存

multi-level-cache

TCMalloc和Go语言运行时分配器都会引入:

  • 线程缓存
  • 中心缓存
  • 页堆

线程缓存属于每一个独立的线程,它能满足线程上绝大多数的分配要求。因为不涉及多线程,所以不需要锁来保护,减少了性能损耗。当线程缓存不能满足要求的时候,运行时会使用中心缓存作为补充解决小对象的内存分配。在遇到32KB以上的对象时,内存分配器会选择页堆直接分配大内存。

虚拟内存布局

Go 语言程序的 1.10 版本在启动时会初始化整片虚拟内存区域,如下所示的三个区域 spansbitmaparena 分别预留了 512MB、16GB 以及 512GB 的内存空间,这些内存并不是真正存在的物理内存,而是虚拟内存:

线性内存

heap-before-go-1-10

  • spans 区域存储了指向内存管理单元 runtime.mspan 的指针,每个内存单元会管理几页的内存空间,每页大小为 8KB;
  • bitmap 用于标识 arena 区域中的那些地址保存了对象,位图中的每个字节都会表示堆区中的 32 字节是否空闲;
  • arena 区域是真正的堆区,运行时会将 8KB 看做一页,这些内存页中存储了所有在堆上初始化的对象;

缺点:这种机制的实现依赖于一个重要的条件:堆区的内存是连续的

稀疏内存

heap-after-go-1-11

运行时使用二维heapArena数组管理所有内存,每个单元都会管理64MB的内存空间。

type heapArena struct {
	bitmap       [heapArenaBitmapBytes]byte
	spans        [pagesPerArena]*mspan
	pageInUse    [pagesPerArena / 8]uint8
	pageMarks    [pagesPerArena / 8]uint8
	pageSpecials [pagesPerArena / 8]uint8
	checkmarks   *checkmarksMap
	zeroedBase   uintptr
}

该结构体中的 bitmapspans 与线性内存中的 bitmapspans 区域一一对应,zeroedBase 字段指向了该结构体管理的内存的基地址。上述设计将原有的连续大内存切分成稀疏的小内存,而用于管理这些内存的元信息也被切成了小块。

地址空间

所有内存最终都要从操作系统中申请,所以Go语言的运行时构建了操作系统的内存管理抽象层

状态解释
None内存没有被保留或者映射,是地址空间的默认状态
Reserved运行时持有该地址空间,但是访问该内存会导致错误
Prepared内存被保留,一般没有对应的物理内存访问该片内存的行为是未定义的可以快速转换到 Ready 状态
Ready可以被安全访问

这个是各种状态之间的转换流程:

memory-regions-states-and-transitions

内存管理组件

Go内存分配器包含几个重要组件:

  • 内存管理单元 --> runtime.mspan
  • 线程缓存 --> runtime.mcache
  • 中心缓存 --> runtime.mcentral
  • 页堆 --> runtime.mheap

go-memory-layout

所有的 Go 语言程序都会在启动时初始化如上图所示的内存布局,每一个处理器都会分配一个线程缓存 runtime.mcache 用于处理微对象和小对象的分配,它们会持有内存管理单元 runtime.mspan

每个类型的内存管理单元都会管理特定大小的对象,当内存管理单元中不存在空闲对象时,它们会从 runtime.mheap 持有的 134 个中心缓存 runtime.mcentral

这张图很重要,建议先看一下留个印象,然后到时候把文章看完之后可以回来返回观看。

img

内存管理单元

runtime.mspan是Go语言内存管理的基本单元,该结构体中包含next和prev两个字段。

type mspan struct {
   next *mspan     // next span in list, or nil if none
   prev *mspan     // previous span in list, or nil if none
   list *mSpanList // For debugging. TODO: Remove.
	.......
}

mspan-and-linked-list

//go:notinheap
type mSpanList struct {
   first *mspan // first span in list, or nil if none
   last  *mspan // last span in list, or nil if none
}

页和内存

每一个runtime.mspan都管理npages个8KB的页,这个页不是操作系统的页,而是其整数倍。

type mspan struct {
	startAddr uintptr // 起始地址
	npages    uintptr // 页数
	freeindex uintptr // 扫描页中空闲对象的初始索引

	allocBits  *gcBits // 标记内存的占用情况
	gcmarkBits *gcBits // 标记内存的回收情况
	allocCache uint64 // allocBits的补码,可用于快速查找内存中未被使用的内存
	...
}

mspan-and-pages

如上图,当结构体管理的内存不足时,运行时会以页为单位向堆中申请内存。

mspan-and-objects

当用户程序或者线程向 runtime.mspan 申请内存时,它会使用 allocCache 字段以对象为单位在管理的内存中快速查找待分配的空间。如果我们能在内存中找到空闲的内存单元会直接返回,当内存中不包含空闲的内存时,上一级的组件 runtime.mcache 会为调用 runtime.mcache.refill 更新内存管理单元以满足为更多对象分配内存的需求。

状态

type mspan struct {
	...
	state       mSpanStateBox
	...
}

状态一共有4种:

var mSpanStateNames = []string{
   "mSpanDead",
   "mSpanInUse",
   "mSpanManual",
   "mSpanFree",
}

跨度类

type mspan struct {
	...
	spanclass   spanClass
	...
}

Go有68种不同大小的spanClass,用于小对象的分配

const _NumSizeClasses = 68
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

如果按照序号为1的spanClass(对象规格为8B)分配,每个span占用堆的字节数:8k,mspan可以保存1024个对象

如果按照序号为2的spanClass(对象规格为16B)分配,每个span占用堆的字节数:8k,mspan可以保存512个对象

如果按照序号为67的spanClass(对象规格为32K)分配,每个span占用堆的字节数:32k,mspan可以保存1个对象

img

大于32k的对象出现时,会直接从heap分配一个特殊的span,这个特殊的span的类型(class)是0, 只包含了一个大对象。

runtime.spanClass 是一个 uint8 类型的整数,它的前 7 位存储着跨度类的 ID,最后一位表示是否包含指针,该类型提供的两个方法能够帮我们快速获取对应的字段。

线程缓存

mcache-and-mspans

mcache管理线程在本地缓存的mspan,每个goroutine绑定的P都有一个mcache字段

type mcache struct {
   alloc [numSpanClasses]*mspan 
    ...
}

_NumSizeClasses = 68
numSpanClasses = _NumSizeClasses << 1

mcacheSpan Classes作为索引管理多个用于分配的mspan,它包含所有规格的mspan。它是_NumSizeClasses的2倍,也就是68*2=136,其中*2是将spanClass分成了有指针和没有指针两种,方便与垃圾回收。对于每种规格,有2个mspan,一个mspan不包含指针,另一个mspan则包含指针。对于无指针对象的mspan在进行垃圾回收的时候无需进一步扫描它是否引用了其他活跃的对象。

这也回应了前面spanClass前7位是跨度类的ID,最后一位是是否包含指针。

中心缓存

mcentral管理全局的mspan供所有线程使用,全局mheap变量包含central字段,每个 mcentral 结构都维护在mheap结构内

type mcentral struct {
    spanclass spanClass // 指当前规格大小

    partial [2]spanSet // 有空闲object的mspan列表
    full    [2]spanSet // 没有空闲object的mspan列表
}

每个mcentral管理一种spanClass的mspan,并将有空闲空间和没有空闲空间的mspan分开管理。partial和 full的数据类型为spanSet,表示 mspans集,可以通过pop、push来获得mspans

type spanSet struct {
    spineLock mutex
    spine     unsafe.Pointer // 指向[]span的指针
    spineLen  uintptr        // Spine array length, accessed atomically
    spineCap  uintptr        // Spine array cap, accessed under lock

    index headTailIndex  // 前32位是头指针,后32位是尾指针
}

简单说一下mcachemcentral获取和归还mspan的流程:

  • 获取; 加锁,从partial链表找到一个可用的mspan;并将其从partial链表删除;将取出的mspan加入到full链表;将mspan返回给工作线程,解锁。
  • 归还; 加锁,将mspanfull链表删除;将mspan加入到partial链表,解锁。

页堆

runtime.mheap 是内存分配的核心结构体,Go 语言程序会将其作为全局变量存储,而堆上初始化的所有对象都由该结构体统一管理,该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段。

var mheap_ mheap
type mheap struct {
    lock      mutex    // 全局锁
    pages     pageAlloc // 页面分配的数据结构
    allspans []*mspan // 所有通过 mheap_ 申请的mspans
        // 堆
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
    
        // 所有中心缓存mcentral
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
    }
    ...
}

mheap-and-memories

初始化

有几个初始化比较重要:

  • 空闲链表分配器
  • 中心缓存

内存管理单元

func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan {
	var s *mspan
	systemstack(func() {
		if h.sweepdone == 0 {
			h.reclaim(npages)
		}
		s = h.allocSpan(npages, false, spanclass, &memstats.heap_inuse)
	})
	...
	return s
}

为了阻止内存的大量占用和堆的增长,我们在分配对应页数的内存前需要先调用 runtime.mheap.reclaim 方法回收一部分内存,随后运行时通过 runtime.mheap.allocSpan 分配新的内存管理单元,我们会将该方法的执行过程拆分成两个部分:

如果申请内存大小充足,那么就直接申请成功。如果申请内存大小不充足,那么就会触发扩容。扩容的时候如果申请到内存就说明扩容成功,否则就是扩容失败,运行时会直接终止当前程序。

内存分配

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	mp := acquirem()
	mp.mallocing = 1

	c := gomcache()
	var x unsafe.Pointer
	noscan := typ == nil || typ.ptrdata == 0
	if size <= maxSmallSize {
		if noscan && size < maxTinySize {
			// 微对象分配
		} else {
			// 小对象分配
		}
	} else {
		// 大对象分配
	}

	publicationBarrier()
	mp.mallocing = 0
	releasem(mp)

	return x
}

上述代码使用 runtime.gomcache 获取线程缓存并判断申请内存的类型是否为指针。我们从这个代码片段可以看出 runtime.mallocgc 会根据对象的大小执行不同的分配逻辑,在前面的章节也曾经介绍过运行时根据对象大小将它们分成微对象、小对象和大对象,这里会根据大小选择不同的分配逻辑:

allocator-and-memory-size

  • 微对象 (0, 16B) — 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存;
  • 小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存;
  • 大对象 (32KB, +∞) — 直接在堆上分配内存;

下面依次来进行介绍:

微对象

Go 语言运行时将小于 16 字节的对象划分为微对象,它会使用线程缓存上的微分配器提高微对象分配的性能,我们主要使用它来分配较小的字符串以及逃逸的临时变量。微分配器可以将多个较小的内存分配请求合入同一个内存块中,只有当内存块中的所有对象都需要被回收时,整片内存才可能被回收。

微分配器管理的对象不可以是指针类型,管理多个对象的内存块大小 maxTinySize 是可以调整的,在默认情况下,内存块的大小为 16 字节。maxTinySize 的值越大,组合多个对象的可能性就越高,内存浪费也就越严重;maxTinySize 越小,内存浪费就会越少,不过无论如何调整,8 的倍数都是一个很好的选择。

tiny-allocator

图 7-20 微分配器的工作原理

如上图所示,微分配器已经在 16 字节的内存块中分配了 12 字节的对象,如果下一个待分配的对象小于 4 字节,它会直接使用上述内存块的剩余部分,减少内存碎片,不过该内存块只有所有对象都被标记为垃圾时才会回收。

线程缓存 runtime.mcache 中的 tiny 字段指向了 maxTinySize 大小的块,如果当前块中还包含大小合适的空闲内存,运行时会通过基地址和偏移量获取并返回这块内存:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	if size <= maxSmallSize {
		if noscan && size < maxTinySize {
			off := c.tinyoffset
			if off+size <= maxTinySize && c.tiny != 0 {
				x = unsafe.Pointer(c.tiny + off)
				c.tinyoffset = off + size
				c.local_tinyallocs++
				releasem(mp)
				return x
			}
			...
		}
		...
	}
	...
}

当内存块中不包含空闲的内存时,下面的这段代码会先从线程缓存找到跨度类对应的内存管理单元 runtime.mspan,调用 runtime.nextFreeFast 获取空闲的内存;当不存在空闲内存时,我们会调用 runtime.mcache.nextFree 从中心缓存或者页堆中获取可分配的内存块:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	if size <= maxSmallSize {
		if noscan && size < maxTinySize {
			...
			span := c.alloc[tinySpanClass]
			v := nextFreeFast(span)
			if v == 0 {
				v, _, _ = c.nextFree(tinySpanClass)
			}
			x = unsafe.Pointer(v)
			(*[2]uint64)(x)[0] = 0
			(*[2]uint64)(x)[1] = 0
			if size < c.tinyoffset || c.tiny == 0 {
				c.tiny = uintptr(x)
				c.tinyoffset = size
			}
			size = maxTinySize
		}
		...
	}
	...
	return x
}

获取新的空闲内存块之后,上述代码会清空空闲内存中的数据、更新构成微对象分配器的几个字段 tinytinyoffset 并返回新的空闲内存。

小对象

小对象是指大小为 16 字节到 32,768 字节的对象以及所有小于 16 字节的指针类型的对象,小对象的分配可以被分成以下的三个步骤:

  1. 确定分配对象的大小以及跨度类 runtime.spanClass
  2. 从线程缓存、中心缓存或者堆中获取内存管理单元并从内存管理单元找到空闲的内存空间;
  3. 调用 runtime.memclrNoHeapPointers 清空空闲内存中的所有数据;

大对象

运行时对于大于 32KB 的大对象会单独处理,我们不会从线程缓存或者中心缓存中获取内存管理单元,而是直接调用 runtime.mcache.allocLarge 分配大片内存:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	if size <= maxSmallSize {
		...
	} else {
		var s *mspan
		span = c.allocLarge(size, needzero, noscan)
		span.freeindex = 1
		span.allocCount = 1
		x = unsafe.Pointer(span.base())
		size = span.elemsize
	}

	publicationBarrier()
	mp.mallocing = 0
	releasem(mp)

	return x
}

runtime.mcache.allocLarge 会计算分配该对象所需要的页数,它按照 8KB 的倍数在堆上申请内存:

func (c *mcache) allocLarge(size uintptr, needzero bool, noscan bool) *mspan {
	npages := size >> _PageShift
	if size&_PageMask != 0 {
		npages++
	}
	...
	s := mheap_.alloc(npages, spc, needzero)
	mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)
	s.limit = s.base() + size
	heapBitsForAddr(s.base()).initSpan(s)
	return s
}

申请内存时会创建一个跨度类为 0 的 runtime.spanClass 并调用 runtime.mheap.alloc 分配一个管理对应内存的管理单元。

总结

  • 首先通过计算使用的大小规格
  • 然后使用mcache中对应大小规格的块分配。
  • 如果mcentral中没有可用的块,则向mheap申请,并根据算法找到最合适的mspan
  • 如果申请到的mspan 超出申请大小,将会根据需求进行切分,以返回用户所需的页数。剩余的页构成一个新的 mspan 放回 mheap 的空闲列表。
  • 如果 mheap 中没有可用 span,则向操作系统申请一系列新的页(最小 1MB)

img

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/146817.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第十八章Vue的学习

文章目录什么是VueVue.js的官网介绍环境配置基本语法声明式渲染绑定元素属性双向数据绑定条件渲染列表渲染事件驱动侦听属性Vue对象生命周期什么是Vue 对于Java程序来说&#xff0c;我们使用框架就是导入那些封装了**『固定解决方案』的jar包&#xff0c;然后通过『配置文件』…

CSS3 之选择器

文章目录1、关系性选择器&#xff1a;EFE~F2、属性选择器3、伪元素选择器4、伪类选择器(被选中的元素的一个种状态)calc1、关系性选择器&#xff1a;EFE~F 2、属性选择器 E[attr~“val”]E[attr|“val”]E[attr^“val”]E[attr$“val”]E[attr*“val”]3、伪元素选择器 E::pl…

CesiumLab对BIM模型的输入格式要求 CesiumaLab系列教程

BIM 模型和手工模型最大的区别在于几点&#xff1a; 1.建模目标不同&#xff0c;手工模型的目的是为了可视化&#xff0c;就是为了看的见&#xff0c;看不见的东西能省则省。BIM 完全是按照一些工程标准去创建的&#xff0c;比如路面可能有多个层代表了不同的物理层。手工模型…

windows编译Paraview源码

目录一. 环境准备二. 编译1. CMake2. Visual Studio一. 环境准备 下载基本所需&#xff1a; paraview官方给了编译文档&#xff1a;https://github.com/Kitware/ParaView/blob/master/Documentation/dev/build.md 所需要的基础有&#xff1a; 如图&#xff1a;&#xff08;进入…

2022我的年度总结-- AI遮天之路

我是一个普普通通的大学生&#xff0c;我的博客记录了我学习编程以来共计1年多的水平&#xff0c;我希望能把自己大学的经历、选择、困惑等与同样身处大学&#xff0c;选择AI方向不知如何发展的人进行分享&#xff0c;因此写了这篇年终总结。另外&#xff0c;对于一些刚刚开始写…

重磅!华为更新职业认证架构刷新和重认证规则

尊敬的各位朋友&#xff0c;感谢您一直以来对华为认证的支持&#xff01; 为匹配华为公司未来长期战略&#xff0c;紧随ICT技术演进趋势&#xff0c;自2023年1月1日起&#xff0c;华为职业认证将启用全新的架构体系和重认证规则&#xff0c;请您关注。 华为职业认证架构刷新 …

新年新希望--爱摸鱼的美工(12)

年近了&#xff0c;上班途中依然匆忙 看女孩子们渐渐开始倒腾 做了新发型&#xff0c;做了美美的指甲 换上了新衣服&#xff0c;买了新包 电话里讨论着 去哪里过年&#xff0c;买什么年货 好像以前的我也这样 今年挣得少了&#xff0c;不想添新衣&#xff08;不能&#xff09; …

【条理清晰】在 Windows 上安装 MySQL

下载 MySQL 安装程序安装 MySQL 数据库安装示例数据库下载 MySQL 安装程序 在本教程中&#xff0c;我们展示如何在 Windows 平台上下载和安装 MySQL 的详细步骤。 在 Windows 平台上安装 MySQL 很简单&#xff0c;并不需要太复杂的步骤。按照本文的步骤操练起来就可以了。 我…

一体化Ethercat通信伺服电机在汇川H5U PLC上的应用案例介绍(下)

内容介绍了一体化低压伺服Ethercat通信的电机在汇川H5UPLC上的使用&#xff0c;一体化Ethercat通信伺服电机在汇川H5U PLC上的应用案例介绍(上)主要讲解环境的搭建以及使用AutoShop软件的在线调试功能&#xff0c;简单控制电机位置、速度模式运行。那么本篇我们就来讲解下使用汇…

【Kotlin】空安全 ② ( 手动空安全管理 | 空安全调用操作符 ? | let 函数结合空安全调用操作符使用 )

文章目录一、手动空安全管理二、空安全调用操作符 ?三、let 函数结合空安全调用操作符使用一、手动空安全管理 Kotlin 语言中 , 变量类型 分为 可空类型 和 非空类型 , 默认状态 下 , 变量是 非空类型 的 , 如果使用 类型? 将变量声明为 可空类型 , 那么就需要使用 手动安…

【C语言进阶】动态内存管理

1. 为什么存在动态内存分配我们已经掌握的内存开辟方式有&#xff1a;int val 20;//在栈空间上开辟四个字节 char arr[10] {0};//在栈空间上开辟10个字节的连续空间但是上述的开辟空间的方式有两个特点&#xff1a;1. 空间开辟大小是固定的。2. 数组在申明的时候&#xff0c;…

JavaWeb复习

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目…

【迅为iMX6Q】开发板 u-boot 2022.04 SD卡 启动

相关参考 【迅为iMX6Q】开发板 u-boot 2015.04 SD卡 启动 【迅为iMX6Q】开发板 u-boot 2020.04 SD卡 启动 开发环境 win10 64位VMware Workstation Pro 16ubuntu 22.04【迅为imx6q】开发板&#xff0c; 2G DDR uboot-imx 下载 使用 NXP 官方提供的 uboot-imx&#xff0c;代…

TreeList-关闭默认显示的右击菜单

需要给控件添加自定义的右击菜单&#xff0c;所以就造成了冲突&#xff0c;导致右击时只弹出控件自带的菜单而没弹出我自定义的菜单&#xff0c;现在把关闭默认菜单的方法记录一下

数据库,计算机网络、操作系统刷题笔记27

数据库&#xff0c;计算机网络、操作系统刷题笔记27 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;oracle…

解决在Win10上安装VMware Workstation虚拟机不可用

一、说明 这是近几年安装虚拟机存在的问题&#xff0c;这里首先说明&#xff0c;以下信息纯粹来自VMware的参考文档&#xff0c;本人的实现不太成功&#xff0c;期望得到更好的WMware软件进行尝试。 二、错误提示 在 Windows 10 主机上&#xff0c;VMware Workstation 中显示“…

2023/1/7 Vue学习笔记-3-组件的理解

1 对组件的理解 模块与组件、模块化与组件化&#xff1a; 1.模块&#xff1a; &#xff08;1&#xff09;理解&#xff1a;向外提供特定功能的js程序&#xff0c;一般就是一个js文件 &#xff08;2&#xff09;为什么&#xff1a;js文件很多很复杂 &#xff08;3&#xff09;作…

【Linux工具】-vim介绍

Vim使用一&#xff0c;Vim的四种模式二&#xff0c;命令模式1,复制&#xff0c;剪切&#xff0c;粘贴2&#xff0c;撤销操作3&#xff0c;光标的移动4&#xff0c;替换&#xff0c;选中&#xff0c;删除5&#xff0c;h j k l键的使用6&#xff0c;多行注释&#xff0c;去多行注…

2023-01-07:hyper/docker-registry-web是registry的web界面工具之一。请问部署在k3s中,yaml如何写?

2023-01-07&#xff1a;hyper/docker-registry-web是registry的web界面工具之一。请问部署在k3s中&#xff0c;yaml如何写&#xff1f; 答案2023-01-07&#xff1a; yaml如下&#xff1a; apiVersion: apps/v1 kind: Deployment metadata:labels:app: docker-registry-webna…

实验二十五 VRRP配置

实验二十五 VRRP配置虚拟路由冗余协议VRRP(Virtual Router R edundancy Protocol)通过把几台路由设备联合组成一 台虚拟的路由设备&#xff0c;将虚拟路由设备的IP 地址作为用户的默认网关实现与外部网络通信。当网关设 备发生故障时&#xff0c;VRRP机制能够选举新的网关设备承…