武安做网站,高端网站建设一般多少钱,学校部门网站的建设,怎样局域网站建设文章目录 概要一、数据结构二、初始化2.1、字面量2.2、下标截取2.2.1、截取原理 2.3、make关键字2.3.1、编译时 三、复制3.1、copy源码 四、扩容4.1、append源码 五#xff1a;切片的GC六#xff1a;切片使用注意事项七#xff1a;参考 概要
Go语言的切片#xff08;slice… 文章目录 概要一、数据结构二、初始化2.1、字面量2.2、下标截取2.2.1、截取原理 2.3、make关键字2.3.1、编译时 三、复制3.1、copy源码 四、扩容4.1、append源码 五切片的GC六切片使用注意事项七参考 概要
Go语言的切片slice是对数组的扩展类似C语言常见的简单动态字符串典型应用如Redis的string类型动态扩容是其相对数组的最大优势。 本人在工作过程中对slice的使用与底层原理有了较为全面的理解特在这里针对其初始化、扩容、复制等机制进行源码分析。
PS: go V1.20.6
一、数据结构
slice的数据结构非常简单其提供了和数组一样的下标访问任意元素方式。在运行时其结构由一个数组字段一个长度字段一个容量字段组成。 最初是在runtime/slice.go文件中
type slice struct {array unsafe.Pointerlen intcap int
}但是2018年10月份的一次优化cmd/compile: move slice construction to callers of makeslice如下
本次优化运行时结构迁移到reflect/value.go文件中
// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
//
// In new code, use unsafe.Slice or unsafe.SliceData instead.
type SliceHeader struct {Data uintptrLen intCap int
}至今未改其中Data字段是指向底层数组的指针Len是当前底层数组使用的长度Cap是当前底层数组的总长度。
二、初始化
切片有三种初始化方式
使用字面量初始化新的切片通过下标的方式获得数组或截取切片的一部分使用关键字 make 创建切片。
2.1、字面量
示例如下
a : []int64{4, 8, 9, 6, 4}2.2、下标截取
数组转切片
a : [5]int64{4, 8, 9, 6, 4}
b : a[:]从切片截取截取是遵循左闭右开原则
a : []int64{4, 8, 9, 6, 4}
//删除第一个元素
b1 : a[1:] //[8,9,6,4]此时a的值不变
//删除最后一个元素
b2 : a[:len(a)-1]//[4,8,9,6]此时a的值不变
//删除中间一个元素
n : len(a)/2
b3 : append(a[:n],a[n1:]...)//[4,8,6,4]此时a的值是[4,8,6,4,4]这种操作非常高效不会申请内存相比b1和b2,b3还会涉及到元素的移动进而改变了a的内容。
2.2.1、截取原理
a : []int64{4, 8, 9, 6, 4}
b1 : a[1:4]//仅指定长度
b2 : a[1:2:3]//指定长度为12-1容量为23-1。【1标识索引下标1、2标识索引下标2决定长度、3表示索引下标3决定容量】长度和容量变化如下 如图所示虽说a、b1、b2的值不是同一个但底层数组还共用同一段连续的内存块所以在编码中要注意这一点我们可以从Go SSA过程中一窥究竟 【在go的源码和汇编码之间其实编译器做了很多事情而ssa是一种中间代码的表示形式就是查看编译器优化行为的利器】 先设置下环境变量。
# windows
$env:GOSSAFUNCmain
# linux
export GOSSAFUNCmain再运行以下代码
package mainimport fmtfunc main() {a : []int64{4, 8, 9, 6, 4}b1 : a[2:4]fmt.Println(b1, len(b1), cap(b1))b2 : a[1:2:3]fmt.Println(b2, len(b2), cap(b2))
}执行go build main.go就可以得到ssa.html文件,读者可自行试验下内容太多我们是看start和opt阶段的就可以 我们只探究b1 : a[2:4]即可关键两处如下 start阶段 opt阶段 对比可以看到从start阶段到opt阶段中间码已经简化很多。 从中可以看到
变量v39表示源码中的变量a变量v56表示源码中的变量b1
那么b1 : a[2:4]如何变化的呢 在opt阶段可以看到v40v39,
v49表示源码中的变量b1的长度已经计算出来真实数值 2在start阶段还不是呢v50表示源码中的变量b1的容量已经计算出来真实数值 3。v55 通过对变量v40进行OffPtr操作得到一个地址就是一个指针运算我们知道a,b1元素是int64的一个元素8字节。b1相对a是右移了两个元素就是16字节了。即对a的底层数组指针加16字节就是b1的底层数组的指针了。v56 就是整合v49,v50,v55这几个变量到一起了。通过SliceMake 操作会接受四个参数创建新的切片依次元素类型[]int64、底层数组指针(v55)、长度v49和容量(v50)这也是我们在数据结构一节中提到的切片的几个字段 。
可以看到整个过程并没有重新申请新的内存段是基于a的底层数组进行指针运算调整切片长度和容量的值等操作得到b1 所以需要注意的是修改新切片b1的数据也会改变原切片a的数据。
所以说b2 : a[:2:3]操作只是改小了切片容量并不会释放a申请的内存段这种缩容是伪缩容
2.3、make关键字
提到make的源码我们第一时间想到的就是Go SDK下的src/runtime/slice.go文件中的makeslice函数但该函数目前只是申请了一块连续内存见第一章节2018年10月份的一次优化相关那么什么地方调用了该函数呢这就要去看一下Go编译器的源码了。
2.3.1、编译时
Go编译器的执行流程有多个阶段
经过词法分析和语法分析得到抽象语法树AST;类型检查,包含检查常量、类型和函数名等类型变量捕获与赋值函数内联、逃逸分析、闭包重写、遍历函数(有些会导入内建的运行时函数如runtime.makeslice,runtime.makechan等)SSA生成机器码生成。
分析何处调用runtime.makeslice函数我们只要分析类型检查阶段。
编译器入口文件src/cmd/compile/main.go,代码如下
func main() {// disable timestamps for reproducible outputlog.SetFlags(0)log.SetPrefix(compile: )buildcfg.Check()archInit, ok : archInits[buildcfg.GOARCH]if !ok {fmt.Fprintf(os.Stderr, compile: unknown architecture %q\n, buildcfg.GOARCH)os.Exit(2)}gc.Main(archInit)//注意此处gc是go compiler的缩写与垃圾回收的GCgarbage collection区分开base.Exit(0)
}进入gc.Main函数
func Main(archInit func(*ssagen.ArchInfo)) {//此处省略若干代码...// Prepare for backend processing. This must happen before pkginit,// because it generates itabs for initializing global variables.ssagen.InitConfig()//ssa初始化// 词法解析、语法解析、类型检查工作noder.LoadPackage(flag.Args())//此处省略若干代码...// 逃逸分析escape.Funcs(typecheck.Target.Decls)//遍历函数工作base.Timer.Start(be, compilefuncs)fcount : int64(0)for i : 0; i len(typecheck.Target.Decls); i {if fn, ok : typecheck.Target.Decls[i].(*ir.Func); ok {// Dont try compiling dead hidden closure.if fn.IsDeadcodeClosure() {continue}enqueueFunc(fn)fcount}}base.Timer.AddEvent(fcount, funcs)//ssa生成、机器码生成工作compileFunctions()// Write object data to disk.base.Timer.Start(be, dumpobj)dumpdata()base.Ctxt.NumberSyms()dumpobj()if base.Flag.AsmHdr ! {dumpasmhdr()}
}进入noder.LoadPackage函数 该函数位于src/cmd/compile/internal/noder/目录下
func LoadPackage(filenames []string) {//只摘抄了部分关键代码// Limit the number of simultaneously open files.sem : make(chan struct{}, runtime.GOMAXPROCS(0)10)noders : make([]*noder, len(filenames))//...// 词法解析、语法解析工作p.file, _ syntax.Parse(fbase, f, p.error, p.pragma, syntax.CheckBranches) // 类型检查相关check2(noders)
}check2函数会在某个节点调用typecheck.Expr,typecheck.Stmt,typecheck.Call等函数进行类型检查即转入typecheck.typecheck函数。
func typecheck(n ir.Node, top int) (res ir.Node) {//省略...n.SetTypecheck(2)n typecheck1(n, top)n.SetTypecheck(1)//省略...
}// typecheck1 should ONLY be called from typecheck.
func typecheck1(n ir.Node, top int) ir.Node {switch n.Op() {case ir.OMAKE://make操作n : n.(*ir.CallExpr)return tcMake(n)}
}// tcMake typechecks an OMAKE node.
func tcMake(n *ir.CallExpr) ir.Node {args : n.Argsl : args[0]l typecheck(l, ctxType)t : l.Type()var nn ir.Nodeswitch t.Kind() {case types.TSLICE://...,设置为ir.OMAKESLICE操作nn ir.NewMakeExpr(n.Pos(), ir.OMAKESLICE, l, r)}//省略...return nn
}func NewMakeExpr(pos src.XPos, op Op, len, cap Node) *MakeExpr {n : MakeExpr{Len: len, Cap: cap}n.pos posn.SetOp(op)return n
}至此获取了make([]int,0,10)之类操作的类型稍后进入遍历函数操作即gc.Main函数中的enqueueFunc。
func enqueueFunc(fn *ir.Func) {//...todo : []*ir.Func{fn}for len(todo) 0 {next : todo[len(todo)-1]todo todo[:len(todo)-1]prepareFunc(next)todo append(todo, next.Closures...)}//...
}
// prepareFunc handles any remaining frontend compilation tasks that
// arent yet safe to perform concurrently.
func prepareFunc(fn *ir.Func) {walk.Walk(fn)//进入遍历函数核心逻辑
}调用链Walk-walkStmtList-walkStmt-walkExpr-walkExpr1
func walkExpr1(n ir.Node, init *ir.Nodes) ir.Node {switch n.Op() {case ir.OMAKESLICE:n : n.(*ir.MakeExpr)return walkMakeSlice(n, init)case ir.OSLICEHEADER:n : n.(*ir.SliceHeaderExpr)return walkSliceHeader(n, init)}
}
// walkMakeSlice walks an OMAKESLICE node.
func walkMakeSlice(n *ir.MakeExpr, init *ir.Nodes) ir.Node {l : n.Lenr : n.Capif n.Esc() ir.EscNone {//不发生逃逸分配栈内内存注意这里由gc.Main函数中的escape.Funcs函数分析得到t types.NewArray(t.Elem(), i) // [r]Tvar_ : typecheck.Temp(t)appendWalkStmt(init, ir.NewAssignStmt(base.Pos, var_, nil)) // zero tempr : ir.NewSliceExpr(base.Pos, ir.OSLICE, var_, nil, l, nil) // arr[:l]// The conv is necessary in case n.Type is named.return walkExpr(typecheck.Expr(typecheck.Conv(r, n.Type())), init)}len, cap : l, rfnname : makeslice64//声明要调用runtime.makeslice64函数argtype : types.Types[types.TINT64]if (len.Type().IsKind(types.TIDEAL) || len.Type().Size() types.Types[types.TUINT].Size()) (cap.Type().IsKind(types.TIDEAL) || cap.Type().Size() types.Types[types.TUINT].Size()) {fnname makeslice//声明要调用runtime.makeslice函数argtype types.Types[types.TINT]}fn : typecheck.LookupRuntime(fnname)//调用得到一块连续内存的头指针ptr : mkcall1(fn, types.Types[types.TUNSAFEPTR], init, reflectdata.MakeSliceElemRType(base.Pos, n), typecheck.Conv(len, argtype), typecheck.Conv(cap, argtype))ptr.MarkNonNil()//修正slice长度和容量len typecheck.Conv(len, types.Types[types.TINT])cap typecheck.Conv(cap, types.Types[types.TINT])//这里转化为ir.OSLICEHEADER操作sh : ir.NewSliceHeaderExpr(base.Pos, t, ptr, len, cap)//执行ir.OSLICEHEADER操作return walkExpr(typecheck.Expr(sh), init)
}
// 转化为ir.SliceHeaderExpr在程序启动后就会变成反射库中的SliceHeader 结构体
func walkSliceHeader(n *ir.SliceHeaderExpr, init *ir.Nodes) ir.Node {n.Ptr walkExpr(n.Ptr, init)n.Len walkExpr(n.Len, init)n.Cap walkExpr(n.Cap, init)return n
}至此把编译阶段如何调用makeslice基本解释清楚了也顺便了解了Go编译相关的知识
至于makeslice函数就很简单了
func makeslice(et *_type, len, cap int) unsafe.Pointer {mem, overflow : math.MulUintptr(et.size, uintptr(cap))if overflow || mem maxAlloc || len 0 || len cap {//参数自动修正mem, overflow : math.MulUintptr(et.size, uintptr(len))if overflow || mem maxAlloc || len 0 {panicmakeslicelen()}panicmakeslicecap()}return mallocgc(mem, et, true)//申请一块连续的内存
}
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {//...if size 0 {//这时mallocgc函数有意思的地方此时会返回一个固定指针我们常用的struct{}{}就是因此而来return unsafe.Pointer(zerobase)}//...
}PSir.SliceHeaderExpr是如何在程序启动后转化为reflect.SliceHeader 的呢有兴趣的大佬可在评论区解释下
三、复制
func main() {s1 : []string{aaa, sss, ddd}s2 : make([]string, 2, 6)copy(s2, s1)s2 append(s2, yyy)printSlice(s2)//output: len3 cap6 slice[aaa sss yyy]s3 : make([]string, 4, 6)copy(s3, s1)s3 append(s3, xxx)printSlice(s3)//output: len5 cap6 slice[aaa sss ddd xxx]
}
func printSlice(x []string) {fmt.Printf(len%d cap%d slice%v\n, len(x), cap(x), x)
}根据s2,s3的打印结果可知若想将源slice的内容全部复制到目的slice,那么目的slice的长度必须大于等于源slice的长度。
3.1、copy源码
编译时源码可见2.3.1小节关键词是src/cmd/compile/internal/ir/node.go中的OCOPY ,搜索可知其遍历函数是walkCopy。
// Lower copy(a, b) to a memmove call or a runtime call.
// Also works if b is a string.
func walkCopy(n *ir.BinaryExpr, init *ir.Nodes, runtimecall bool) ir.Node {if n.X.Type().Elem().HasPointers() {//slice在堆上的话调用runtime.typedslicecopyfn : writebarrierfn(typedslicecopy, n.X.Type().Elem(), n.Y.Type().Elem())return mkcall1(fn, n.Type(), init, reflectdata.CopyElemRType(base.Pos, n), ptrL, lenL, ptrR, lenR)}if runtimecall {//某些特殊情况比如编译时开启竞态检查(-race)调用runtime.slicecopyfn : typecheck.LookupRuntime(slicecopy)fn typecheck.SubstArgTypes(fn, ptrL.Type().Elem(), ptrR.Type().Elem())return mkcall1(fn, n.Type(), init, ptrL, lenL, ptrR, lenR, ir.NewInt(n.X.Type().Elem().Size()))}//排除以上两种情况都走runtime.memmovenlen : typecheck.Temp(types.Types[types.TINT])// n len(to)l append(l, ir.NewAssignStmt(base.Pos, nlen, ir.NewUnaryExpr(base.Pos, ir.OLEN, nl)))fn : typecheck.LookupRuntime(memmove)fn typecheck.SubstArgTypes(fn, nl.Type().Elem(), nl.Type().Elem())call : mkcall1(fn, nil, init, nto, nfrm, nwid)ne.Body.Append(call)return nlen
}进入runtime.typedslicecopy和runtime.slicecopy函数其最后也是调用的runtime.memmove函数。 该函数与C语言的memmove作用是一样的时间复杂度是O(N)所以面对较多元素的切片时使用copy操作应当慎重。
四、扩容
func main() {r : make([]int, 0, 3)fmt.Printf(len%d cap%d slice%v,r addr:%p,addr:%p\n, len(r), cap(r), r, r, r) //初始化但可以看出r本质为*SliceHeader的指针类型所以在传参时就是指针传递r append(r, 5, 6)fmt.Printf(len%d cap%d slice%v,r addr:%p,addr:%p,r[0] addr:%p\n, len(r), cap(r), r, r, r, r[0]) //第一个元素地址没变r append(r, 11)fmt.Printf(len%d cap%d slice%v,r addr:%p,addr:%p,r[0] addr:%p\n, len(r), cap(r), r, r, r, r[0]) //第一个元素地址没变r append(r, 22)fmt.Printf(扩容len%d cap%d slice%v,r addr:%p,addr:%p,r[0] addr:%p\n, len(r), cap(r), r, r, r, r[0]) //扩容后地址发生变化即底层数组发生变化但变量的地址不变fmt.Printf(r addr:%p,addr:%p,r[0] addr:%p,r[1] addr:%p,\n, r, r, r[0], r[1]) //r值的地址也变为扩容后第一个元素的地址r append(r, []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110}...)fmt.Printf(扩容 len%d cap%d slice%v,r addr:%p,r[0] addr:%p\n, len(r), cap(r), r, r, r[0]) //扩容后地址发生变化即底层数组发生变化但变量的地址不变。扩容后newCap本应该是15但实际是16因为做了内存对齐
}
运行代码输出如下
len0 cap3 slice[],r addr:0xc000008570,addr:0xc000017698
len2 cap3 slice[5 6],r addr:0xc000008570,addr:0xc000017698,r[0] addr:0xc000017698
len3 cap3 slice[5 6 11],r addr:0xc000008570,addr:0xc000017698,r[0] addr:0xc000017698
扩容len4 cap6 slice[5 6 11 22],r addr:0xc000008570,addr:0xc00000eba0,r[0] addr:0xc00000eba0
r addr:0xc000008570,addr:0xc00000eba0,r[0] addr:0xc00000eba0,r[1] addr:0xc00000eba8,
扩容 len15 cap16 slice[5 6 11 22 10 20 30 40 50 60 70 80 90 100 110],r addr:0xc000008570,r[0] addr:0xc000078f00
其扩容流程图如下 假设有一切片其长度为oldLen,容量为oldCap现要增加num个元素。则有newLenoldLennum,doublecapoldCapoldCap。
4.1、append源码
编译时源码可见2.3.1小节关键词是src/cmd/compile/internal/ir/node.go中的OAPPEND ,注意walkExpr1函数源码中OAPPEND已废弃而是走OAS搜索可知其遍历函数是walkAssign。
func walkAssign(init *ir.Nodes, n ir.Node) ir.Node {//...as : n.(*ir.AssignStmt)switch as.Y.Op() {case ir.OAPPEND:var r ir.Nodeswitch {case isAppendOfMake(call):// x append(y, make([]T, y)...)r extendSlice(call, init)case call.IsDDD:r appendSlice(call, init) // also works for append(slice, string).default:r walkAppend(call, init, as)}}
}
func walkAppend(n *ir.CallExpr, init *ir.Nodes, dst ir.Node) ir.Node {var l []ir.Node// s slice to append tos : typecheck.Temp(nsrc.Type())l append(l, ir.NewAssignStmt(base.Pos, s, nsrc))// num number of things to appendnum : ir.NewInt(int64(argc))// newLen : s.len numnewLen : typecheck.Temp(types.Types[types.TINT])l append(l, ir.NewAssignStmt(base.Pos, newLen, ir.NewBinaryExpr(base.Pos, ir.OADD, ir.NewUnaryExpr(base.Pos, ir.OLEN, s), num)))//调用runtime.growslice函数fn : typecheck.LookupRuntime(growslice) // growslice(ptr *T, newLen, oldCap, num int, type) (ret []T)fn typecheck.SubstArgTypes(fn, s.Type().Elem(), s.Type().Elem())nif.Else []ir.Node{ir.NewAssignStmt(base.Pos, s, mkcall1(fn, s.Type(), nif.PtrInit(),ir.NewUnaryExpr(base.Pos, ir.OSPTR, s),//要扩容切片的地址newLen,//新切片元素个数ir.NewUnaryExpr(base.Pos, ir.OCAP, s),//要扩容切片的容量num,//要追加的元素个数reflectdata.TypePtr(s.Type().Elem()))),//要扩容切片的类型}
}再看看runtime.growslice函数
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {oldLen : newLen - numif et.size 0 {return slice{unsafe.Pointer(zerobase), newLen, newLen}//扩容的运行时竟然用的是runtime.slice结构体}//扩容逻辑newcap : oldCapdoublecap : newcap newcapif newLen doublecap {newcap newLen} else {const threshold 256if oldCap threshold {newcap doublecap} else {// Check 0 newcap to detect overflow// and prevent an infinite loop.for 0 newcap newcap newLen {// Transition from growing 2x for small slices// to growing 1.25x for large slices. This formula// gives a smooth-ish transition between the two.newcap (newcap 3*threshold) / 4}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap 0 {newcap newLen}}}var overflow boolvar lenmem, newlenmem, capmem uintptr//进行内存对齐switch {case et.size 1:lenmem uintptr(oldLen)newlenmem uintptr(newLen)capmem roundupsize(uintptr(newcap))overflow uintptr(newcap) maxAllocnewcap int(capmem)case et.size goarch.PtrSize: //goarch.PtrSize is 4 on 32-bit systems, 8 on 64-bit systems。lenmem uintptr(oldLen) * goarch.PtrSizenewlenmem uintptr(newLen) * goarch.PtrSizecapmem roundupsize(uintptr(newcap) * goarch.PtrSize)//内存对齐overflow uintptr(newcap) maxAlloc/goarch.PtrSizenewcap int(capmem / goarch.PtrSize)case isPowerOfTwo(et.size):var shift uintptrif goarch.PtrSize 8 {// Mask shift for better code generation.shift uintptr(sys.TrailingZeros64(uint64(et.size))) 63} else {shift uintptr(sys.TrailingZeros32(uint32(et.size))) 31}lenmem uintptr(oldLen) shiftnewlenmem uintptr(newLen) shiftcapmem roundupsize(uintptr(newcap) shift)overflow uintptr(newcap) (maxAlloc shift)newcap int(capmem shift)capmem uintptr(newcap) shiftdefault:lenmem uintptr(oldLen) * et.sizenewlenmem uintptr(newLen) * et.sizecapmem, overflow math.MulUintptr(et.size, uintptr(newcap))capmem roundupsize(capmem)newcap int(capmem / et.size)capmem uintptr(newcap) * et.size}if overflow || capmem maxAlloc {panic(errorString(growslice: len out of range))}//申请新切片所需的内存var p unsafe.Pointerif et.ptrdata 0 {p mallocgc(capmem, nil, false)memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)} else {p mallocgc(capmem, et, true)if lenmem 0 writeBarrier.enabled {bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.sizeet.ptrdata)}}//旧切片中的内容复制到新切片中memmove(p, oldPtr, lenmem)return slice{p, newLen, newcap}
}接着一起看下内存对齐函数roundupsize,窥视下Go的内存管理我们以案例中的r : make([]int, 0, 3)第二次发生扩容为例 已知测试环境是64位的那么et.size8goarch.PtrSize8在内存对齐前的newcap15 则有capmem roundupsize(uintptr(newcap) * goarch.PtrSize)转化为capmem roundupsize(120)
const (_MaxSmallSize 32768smallSizeDiv 8smallSizeMax 1024largeSizeDiv 128_NumSizeClasses 68_PageShift 13
)
// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {if size _MaxSmallSize {if size smallSizeMax-8 {return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])} else {return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])}}if size_PageSize size {return size}return alignUp(size, _PageSize)
}
// divRoundUp returns ceil(n / a).
func divRoundUp(n, a uintptr) uintptr {return (n a - 1) / a
}按照代码逻辑计算:
divRoundUp(size, smallSizeDiv)得到15size_to_class8[15]得到10class_to_size[10]得到128newcap int(capmem / goarch.PtrSize)得到16。 所以经过内存对齐后的容量为16所以本次扩容申请了16B的内存块。
我们观察到两个全局变量class_to_size和size_to_class8 。Go为了方便对内存进行管理将内存划分成了68个级别的span,最小为8B最大为32KB大于32KB的类型都为0。 class_to_size通过 spanClass获取 span划分的 object大小而 size_to_class8 表示通过 size 获取它的 spanClass。
五切片的GC
Go 的GC算法是三色黑、灰、白标记算法白色的是要被回收的。Go在GC时从根对象开始扫描根对象包含全局变量、goroutine上的栈对象span中的finalizer对象所以runtime.SetFinalizer要慎用可能会引起内存泄漏。
那么我们要想slice被GC就要slice不被全局变量栈对象一直使用即可(finalizer对象编码时一般不涉及)。
1切片下标截取引起的内存泄漏
func PrintMemory() {bToMb : func(b uint64) uint64 {return b}var m runtime.MemStatsruntime.ReadMemStats(m)// For info on each, see: https://golang.org/pkg/runtime/#MemStatsfmt.Printf(Alloc %vB, bToMb(m.Alloc))fmt.Printf(\tTotalAlloc %vB, bToMb(m.TotalAlloc))fmt.Printf(\tSys %vB, bToMb(m.Sys))fmt.Printf(\tHeapSys %vB, bToMb(m.HeapSys))fmt.Printf(\tHeapReleased %vB, bToMb(m.HeapReleased))fmt.Printf(\tHeapInuse %vB, bToMb(m.HeapInuse))fmt.Printf(\tHeapAlloc %vB, bToMb(m.HeapAlloc))fmt.Printf(\tHeapIdle %vB, bToMb(m.HeapIdle))fmt.Printf(\tNumGC %v\n, m.NumGC)
}
func main() {PrintMemory()a : make([]int64, 1024*1024) //FIXME:申请内存块 APrintMemory()time.Sleep(time.Second)for i : 0; i 1024*1024; i {a[i] rand.Int63()}time.Sleep(time.Second)PrintMemory()runtime.GC()time.Sleep(time.Second)PrintMemory()b : a[:100:100] //FIXME:所以说这样操作并不会释放已申请的内存块 Aruntime.GC()time.Sleep(time.Second)PrintMemory()b append(b, []int64{5, 5, 6, 6}...) //FIXME:此时发生了扩容已经与内存块 A无关了后续发生GC就会回收内存块 Aruntime.GC()time.Sleep(time.Second)PrintMemory() //这里从HeapIdle可以看出来Go中及时某些对象被释放了但并不会将内存立即归还给OS,而是标记为free,后续可能会被重新利用提高性能fmt.Println(b[100])
}2指针切片引起的内存泄漏
func main() {PrintMemory()s : make([]*string, 5)s[0] strPtr(世纪东方收款方就开始)s[1] strPtr(速度进房撒克服恐惧的)s[2] strPtr(畜牧场辛苦费几十块大飞机刷卡)s[3] strPtr(摩卡壶亚体育前三个哈哈的)runtime.GC()time.Sleep(time.Second)PrintMemory()s1 : strings.Builder{}for i : 0; i 1024*1024; i { //变量s1申请内存s1.WriteString(看见对方收款方几十块)}s[4] strPtr(s1.String())runtime.GC()time.Sleep(time.Second)PrintMemory()p : s[:3] //并不会释放变量s1申请的内存runtime.GC()time.Sleep(time.Second)PrintMemory()s[4] nil //会释放变量s1申请的内存runtime.GC()time.Sleep(time.Second)PrintMemory()fmt.Println(*p[0])
}六切片使用注意事项
切片初始化时尽量确定容量避免频繁扩容,因为要重新申请内存并copy旧切片内容大切片尽量少copy,而是复用copy的时间复杂度是O(N)切片采用下标截取时不会申请新内存块所以修改截取后的切片内容会改变源切片的内容切片变量本身是个结构体指针切片发生扩容时会改变指针值指向一个新的地址
func main() {s : []int64{4, 8, 9}fmt.Printf(len%d cap%d slice%v,s addr:%p,s:%p,s[0] addr:%p\n, len(s), cap(s), s, s, s, s[0])//len3 cap3 slice[4 8 9],s addr:0xc000008078,s:0xc000016180,s[0] addr:0xc000016180s append(s, 6)fmt.Printf(len%d cap%d slice%v,s addr:%p,s:%p,s[0] addr:%p\n, len(s), cap(s), s, s, s, s[0])//len4 cap6 slice[4 8 9 6],s addr:0xc000008078,s:0xc00000e3f0,s[0] addr:0xc00000e3f0
}
可以看到切片变量s的值在扩容前是0xc000016180扩容后是0xc00000e3f0切片传参是值传递当然了go里面只有值传递但一般会把指针传递本质也是值传递指针本身也是一个值分出来
func testSlice(s []int64) {fmt.Printf(s addr:%p,s:%p,s[0] addr:%p\n, s, s, s[0])//s addr:0xc0000080a8,s:0xc000016180,s[0] addr:0xc000016180s[1] 80s append(s, 66)fmt.Printf(s addr:%p,s:%p,s[0] addr:%p\n, s, s, s[0])//s addr:0xc0000080a8,s:0xc00000e420,s[0] addr:0xc00000e420fmt.Println(s)//[4 80 9 66]
}
func main() {s : []int64{4, 8, 9}fmt.Printf(s addr:%p,s:%p,s[0] addr:%p\n, s, s, s[0])//s addr:0xc000008078,s:0xc000016180,s[0] addr:0xc000016180testSlice(s)fmt.Println(s)//[4 80 9]
}切片下标截取可能会引起的内存泄漏如果切片内的其他元素不会再使用最好申请一个新的切片copy需要的元素切片下标截取的方式是伪缩容要想真缩容就要申请一个新的切片进行copy操作这样旧的切片是会被GC
func main() {s : []int{1, 2, 3, 4, 5, 6, 7, 8, 9}s1 : make([]int,3)copy(s1,s)//之后的代码与切片s无关那么由于没有地方再使用切片s就会被GCfmt.Println(s1) //[1,2,3]
}切片删除不符合要求的元素
func main() {//该算法时间复杂度是O(N)s : []int{1, 2, 3, 4, 5, 6, 7, 8, 9}k : 0for _, n : range slice {if n%3 ! 0 { // 指定过滤条件s2[k] nk}}s s[:k]fmt.Println(s) //[1 2 4 5 7 8]
}七参考
1]:深入学习go语言-前置知识-编译过程 2]:Go 中切片使用不当会造成内存泄漏的那些场景