map-的内存走漏-了解和预防-Go-言语中 (map的内存结构)
Map在内存中总是会增长;它不会收缩。因此,假设map造成了一些内存疑问,你可以尝试不同的选项,比如强迫Go从新创立map或经常使用指针。
在Go中经常使用map时,咱们须要了解map增长和收缩的一些关键个性。让咱们深化讨论这一点,以防止或许造成内存走漏的疑问。
首先,为了检查这个疑问的一个详细例子,让咱们设计一个场景,在这个场景中咱们将经常使用以下map:
m:=make(map[int][128]byte)
每个m的值都是一个蕴含128字节的数组。咱们将口头以下操作:
在每个步骤之后,咱们宿愿打印堆的大小(经常使用一个printAlloc适用函数)。这将展现这个示例在内存方面的行为方式:
funcmn(){n:=1_000_000m:=make(map[int][128]byte)printAlloc()fori:=0;i<n;i++{//Adds1millionelementsm[i]=[128]byte{}}printAlloc()fori:=0;i<n;i++{//Deletes1millionelementsdelete(m,i)}runtime.GC()//TriggersamanualGCprintAlloc()runtime.KeepAlive(m)//Keepsareferencetomsothatthemapisn’tcollected}funcprintAlloc(){varmruntime.MemStatsruntime.ReadMemStats(&m)fmt.Printf("%dKBn",m.Alloc/1024)}
咱们调配一个空的map,参与100万个元素,删除100万个元素,而后运转渣滓回收。咱们还确保经常使用runtime.KeepAlive坚持对map的援用,以防止map被搜集。让咱们运转这个示例:
0MB<--Aftermisallocated461MB<--Afterweadd1millionelements293MB<--Afterweremove1millionelements
咱们观察到了什么?后来,堆大小很小。而后,在将100万个元素参与到map后,它清楚增长了。但是,假设咱们希冀在删除一切元素后堆大小会减小,这并不是Go中map的上班方式。最后,虽然GC曾经搜集了一切元素,但堆大小依然是293MB。因此,内存增加了,但并非咱们或许预期的方式。这其中的原理是什么?咱们须要深化了解一下Go中map的上班原理。
map提供了一个无序的键值对汇合,其中一切的键都是惟一的。在Go中,map基于哈希表数据结构:一个数组,其中每个元素都是指向键值对存储桶的指针,如图1所示。
图1—哈希示意例,重点关注存储桶0。
每个存储桶都是一个固定大小的数组,蕴含八个元素。假设要将元素拔出曾经满了的存储桶(即存储桶溢出),Go会创立另一个蕴含八个元素的存储桶,并将前一个存储桶链接到它上。图2显示了一个例子:
图2—假设存储桶溢出,Go会调配一个新的存储桶,并将前一个存储桶链接到它上。
在底层,Go中的map是指向runtime.hmap结构体的指针。该结构体蕴含多个字段,其中包括一个B字段,示意map中存储桶的数量:
typehmapstruct{Buint8//log_2of#ofbuckets//(canholduptoloadFactor*2^Bitems)//...}
在参与了100万个元素之后,B的值等于18,这象征着有2¹⁸=262,144个存储桶。当咱们删除了100万个元素后,B的值是多少呢?依然是18。因此,map依然蕴含相反数量的存储桶。
要素在于map中存储桶的数量是无法缩减的。因此,从map中删除元素不会影响现有存储桶的数量;它只是将存储桶中的槽清零。map只能增长并领有更多的存储桶;它永远不会增加。
在先前的示例中,咱们从461MB增加到了293MB,由于元素被搜集,但运转渣滓回收并没有影响map自身。即使额外存储桶的数量(由于溢出而创立的存储桶)也坚持不变。
让咱们退一步,讨论map无法增加的状况何时或许成为疑问。构想一下经常使用map[int][128]byte来构建缓存。这个map以每个客户ID(int)为键,保管一个长度为128字节的序列。如今,假定咱们想保管最近的1000位客户。map的大小将坚持不变,所以咱们不用担忧map无法增加的疑问。
但是,假定咱们想要存储一小时的数据。同时,咱们的公司选择在彩色星期五启动大促销:在一个小时内,咱们或许会有数百万的客户衔接到咱们的系统。但是在彩色星期五之后的几天,咱们的map将蕴含与高峰期相反数量的存储桶。这就解释了为什么在这种状况下咱们或许会遇到内存消耗高却不会清楚增加的状况。
假设咱们不想手动重启服务来清算map消耗的内存量,有哪些处置打算?一种处置打算可以是活期从新创立以后map的正本。例如,每小时咱们可以构建一个新map,复制一切元素,并监禁先前的map。这种选用的关键缺陷是,在复制后直到下一次性渣滓回收之前,咱们或许会在短期间内讧费两倍于以后内存。
另一种处置打算是将map类型更改为存储数组指针:map[int]*[128]byte。这并没有处置咱们会有少量存储桶的疑问;但是,每个存储桶条目将为值保管指针的大小,而不是128字节(64位系统上为8字节,32位系统上为4字节)。
回到原始场景,让咱们比拟每种map类型在每个步骤后的内存消耗。以下表格显示了比拟。
|
|
调配一个空的map |
|
参与100万个元素 |
|
删除一切元素并运转GC |
正如咱们所看到的,在删除一切元素后,经常使用map[int]*[128]byte类型所需的内存量清楚较少。此外,在这种状况下,由于一些优化措施以增加内存消耗,高峰期间所需的内存量也较少清楚。
留意:假设键或值超越128字节,Go将不会间接将其存储在map存储桶中。相反,Go将存储用于援用键或值的指针。
论断
正如咱们所见,向map参与n个元素,而后删除一切元素象征着在内存中坚持相反数量的存储桶。因此,咱们必定记住,由于Gomap只能增长,因此其内存消耗也会随之参与。它没有智能化的战略来增加。假设这造成内存消耗过高,咱们可以尝试不同的选项,比如强迫Go从新创立map或经常使用指针来审核能否可以启动优化。
golang map的元素遍历为什么是随机的
Map是随机存储的,好像是按内存块的大小放数据。 这样存储效率高。 但检索效率低。 List是会重新划分存储空间,保证连续存储,存的效率低,检索效率高。 大概是这个意思,具体的,准确、详细的自己google下。 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。 得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。 设置了首尾倒置函数,也会出现这种类似情况。 还有,你要注意:map中不允许存在重复的键名,你也可以使用其他的方式来实现,比如List,排序的话还得靠你自己来实现了。
你遇到过哪些高质量的 Go 语言面试题?
在探索Go语言的面试之旅中,你曾遇到哪些既深入又实用的考察点?
在众多的面试经历中,我发现高票答案往往聚焦于表面的Cgo技术,却忽略了Go语言核心精髓的考察。真正考验Go程序员功底的问题,如channel的巧妙使用、goroutine的调度与管理,实则常常被忽视。
Channel与goroutine的实践
面试官们常常会测试你对channel的理解,是否能透彻地运用它来实现高效的并发通信,避免常见的死锁问题。同时,他们可能会询问你关于goroutine的生命周期管理,是否明白何时需要手动关闭,以及如何确保资源释放,防止CPU资源的浪费。
数据结构的微妙之处
> 当你面对slice的操作,切记要理解它的底层实现。创建新slice时,原slice的影响不容忽视,尤其是在处理大型数据时,如何合理设计以降低内存压力至关重要。此外,对map的工作原理和内存管理也有深入理解,将有助于你避免常见的编程陷阱。并发与数据结构的深度理解
面试时,理解进程、线程与协程的区别是基础。要知道,Go的channel是如何通过注册goroutine的标识实现通信通知的。同时,掌握互斥锁、读写锁等并发控制手段,以及理解内存模型如何影响程序性能,对于调试和优化至关重要。
深入文档,实用为王
尽管上述内容看似抽象,但它们都源于Go官方文档的权威讲解。真正的高质量面试题不仅关注技术细节,也考察你是否能将理论知识转化为实际应用,这正是Go语言面试的精髓所在。
总之,面试不仅是测试技术能力,更是对编程理念和实践深度的检验。希望你在面对Go语言面试时,能够自信地展示你对这些核心概念的掌握,让你的面试之旅更加精彩。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。