深度剖析Golang如何实现GC扫描对象

目录
  • 扫描的目的
  • 扫描的实现
    • 运行期内存分配
    • 运行扫描阶段
  • 总结

之前阐述了 golang 垃圾回收通过保证三色不变式来保证回收的正确性,通过写屏障来实现业务赋值器和 gc 回收器正确的并发的逻辑。其中高概率的提到了“扫描队列”和“扫描对象”。队列这个逻辑非常容易理解,那么”扫描对象“ 这个你理解了吗?有直观的感受吗?这篇文章就是要把这个扫描的过程深入剖析下。

  • 扫描的东西是啥?形象化描述下
  • 怎么去做的扫描?形象化描述下

我们就是要把这两个抽象的概念搞懂,不能停留在语言级别浅层面,要知其然知其所以然。

扫描的目的

扫描到底是为了什么?

之前的文章我们深入剖析了垃圾回收的理论和实现,可以总结这么节点:

  • 垃圾回收的根本目的是:“回收那些业务永远都不会再使用的内存块”;
  • 扫描的目的则是:“把这些不再使用的内存块找出来”;

我们通过地毯式的扫描,从一些 root 起点开始,不断推进搜索,最终形成了一张有向可达的网,那些不在网里的就是没有被引用到的,也就是可回收的内存。

扫描的实现

扫描对象代码逻辑其实不简单,但主体线索很清晰,可以分为三部分:

  • 编译阶段:编译期是非常重要的一环,针对静态类型做好标记准备(旁白:原则上编译期能做的绝对不留到运行期);
  • 运行阶段:赋值器分配内存的时候,根据编译阶段的 type 标示,会为分配的对象内存设置好一个对应的指针标示的 bitmap;
  • 扫描阶段:根据指针的 bitmap 标示,地毯式扫描;

编译阶段

结构体对齐

要理解编译阶段做的事情,那么首先要理解结构体对齐的基础知识。这个和 C 语言类似,golang 的结构体是有对齐规则的,也就是说,必要的时候可能会填充一些内存空间来满足对齐的要求。总结来说两条规则:

  • 长度要对齐
  • 地址要对齐

“长度要对齐”怎么理解?

结构体的长度要至少是内部最长的基础字段的整数倍。

举例:

type TestStruct struct {
	ptr uintptr     // 8
	f1  uint32      // 4
	f2  uint8       // 1
}

这个结构体内存占用 size 多大?

答案是:16个字节,因为字段 ptr 是 uintptr 类型,占 8 字节,是内部字段最大的,TestStruct 整体长度要和 8 字节对齐。那么就是 16 字节了,而不是有些人想的 13 字节(8+4+1)。

dlv 调试如下:

(dlv) p typ
*runtime._type {
	size: 16,
    ...

字节示意图:

|--8 Byte--|--4 Byte--|--4 Byte--|

“地址要对齐”怎么理解?

字段的地址偏移要是自身长度的整数倍。

举例:

type TestStruct struct {
	ptr uintptr   // 8
	f1  uint8     // 1
	f2  uint32    // 4
}

假设 new 一个 TestStruct 结构体 a 的地址是 0xc00008a010 ,那么 &a.ptr 是 0xc00008a010 (= a + 0),&a.f1 是 0xc00008a018 (= a + 8) ,&a.f2 是 0xc00008a01c (= a + 8 + 4) 。

dlv 调试如下:

(dlv) p &a.ptr
(*uintptr)(0xc00008a010)
(dlv) p &a.f1
(*uint8)(0xc00008a018)
(dlv) p &a.f2
(*uint32)(0xc00008a01c)

假设 TestStruct 分配对象 a 的地址是 0xc00008a010 ,解释如下:

  • ptr 是第一个字段,当然和结构体本身地址一样,相对偏移是 0,所以地址是 0xc00008a010 == 0xc00008a010 + 0 ;
  • f1 是第二个字段,由于前一个字段 ptr 是 uintptr 类型(8字节),并且由于 f1 本身是 uint8 类型(1字节),所以 f1 从 8 偏移开始没毛病,所以 f1 的偏移地址从 0xc00008a018 == 0xc00008a010 + 8
  • f2 是第三个字段,由于前一个字段 f1 是 uint8(1字节),所以表面上看好像 f2 要接着 0xc00008a019 (= 0xc00008a018 +1) 这个地址才对,但是 f2 本身是 uint32 (4字节的类型),所以 f2 地址偏移至少要是 4 的倍数,所以 f2 的地址要从 0xc00008a01c (0xc00008a018 + 4)这个地址开始才对。也就是说,f1 到 f2 之间填充了一些不用的空间,为了地址对齐。

所以这样算下来,整个 TestStruct 的占用空间长度是 16字节 (8+1+3+4)。

指针位标记

golang 的所有类型都对应一个 _type 结构,可以在 runtime/type.go 里面找到,定义如下:

type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldalign uint8
	kind       uint8
	alg        *typeAlg
	// gcdata stores the GC type data for the garbage collector.
	// If the KindGCProg bit is set in kind, gcdata is a GC program.
	// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
	gcdata    *byte
	str       nameOff
	ptrToThis typeOff
}

比如我们定义了一个 Struct 如下:

type TestStruct struct {
	ptr uintptr
	f1  uint8
	f2  *uint8
	f3  uint32
	f4  *uint64
	f5  uint64
}

该结构 dlv 调试如下:

(dlv) p typ
*runtime._type {
	size: 48,
	ptrdata: 40,
	hash: 4075663022,
	tflag: tflagUncommon|tflagExtraStar|tflagNamed (7),
	align: 8,
	fieldalign: 8,
	kind: 25,
	alg: *runtime.typeAlg {hash: type..hash.main.TestStruct, equal: type..eq.main.TestStruct},
	gcdata: *20,
	str: 28887,
	ptrToThis: 49504,}

在编译期间,编译器就会在内部生成一个 _type 结构体与之对应。_type 里面重点解释几个和本次扫描主题相关的字段:

1.size:类型长度,我们上面这个类型长度应该是 32 字节;

这里理解要应用上上面讲的结构体字节对齐的知识,这里就不再复述;

2.ptrdata:指针截止的长度位置,我们 f4 是指针,所以包含指针的字段最多也就到 40 字节的位置,ptrdata==40;

要理解字节对齐哈;

3.kind:表明类型,我们是自定义struct类型,所以 kind == 25

kind 枚举定义在 runtime/typekind.go 文件里;

4.gcdata:这个就重要了,这个就是指针的 bitmap,因为编译器他在编译分析的时候,肯定就知道了所有的类型结构,那么自然知道所有的指针位置。gcdata 是 *byte 类型(byte 数组),当前值是 20,20 转换成二进制数据就是 00010100 ,这个眼熟不?这个你要从右往左看就是 00101000(从低 bit 往高 bit 看),这个不就是刚好是 TestStruct 的指针 bitmap 嘛,每个 bit 表示一个指针大小(8 字节)的内存,00101000 第 3 个 bit 和第 5 个 bit 是 1,表示 第 3 个字段(第 3 个 8 字节的位置)和第 5 个字段(第 5 个 8 字节的位置)是存储的是指针类型,这里刚好就和 TestStruct.f2 和 TestStruct.f4 对应起来。

划重点:这里重点回顾一下 uintptr 类型的问题,这里注意到,第一个字段 ptr(uintptr 类型)在指针的 bitmap 上是没有标记成指针类型的,这里一定要注意了,uintptr 是数值类型,非指针类型,用这个存储指针是无法保护对象的(扫描的时候 uintptr 指向的对象不会被扫描),这里就是实锤了。

小结

编译阶段给每个类型生成 _type 类型,内部对类型字段生成指针的 bitmap,这个是后面扫描行为的基础依据。

思考题:是否可以不用 bitmap,其实有个最简单最笨拙的扫描方式,我们可以不搞这个指针的 bitmap,我上来就直接扫描,每 8 字节的读取内存,然后去看这个内存块存储的值是否指向了一个对象?如果是我就保护起来。

这个实现理论上可以满足,但是有两个不能接受的缺陷:

  • 精度太低,你编译期间不做准备,那运行期间就要来偿还这部分损耗,你无法判断是不是指针,所以只要指向了一个有效内存地址,就得无脑保护,这样就保护了很多不需要保护的内存块;
  • 扫描太低效,必须全地址扫描,因为你没有 bitmap,无法识别是否有指针。也无法做优化,比如我们程序里面可能 一半以上的类型内是不包含指针的,这种根本就不需要扫描;

运行期内存分配

下一步就是赋值器的做的事情,也就是业务运行的过程中分配内存。分配内存的时候肯定要指定类型,调用 runtime.newobject 函数进行分配,本质上调用 mallocgc 函数来操作。mallocgc 函数做几件事情:

  • 分配内存
  • 内存采样
  • gc 标记准备

我们这里重点分析给 gc 做扫描做的准备。在分配完堆内存之后,会调用一个函数 heapBitsSetType ,这个函数逻辑非常复杂,但是做的事情其实一句话能概括:“给 gc 扫描做准备,对分配的内存块做好标记,这小块内存中,哪些位置是指针,我们用一个 bitmap 对应记录下来”。这就是 heapBitsSetType 500 多行代码做的所有事情,之所以这么复杂是因为要判断各种情况。

heapBitsSetType 主要逻辑解析:

func heapBitsSetType(x, size, dataSize uintptr, typ *_type) {
    // ...

    // 最重要的两个步骤:
    // 通过分配地址反查获取到 heap 的 heapBits 结构(回忆下 golang 的内存地址管理)
    h := heapBitsForAddr(x)
    // 获取到类型的指针 bitmap;
    ptrmask := typ.gcdata // start of 1-bit pointer mask (or GC program, handled below)

    var (
        // ...
    )

    // 把 h.bitp 这个堆上的 bitmap 取出来;
    hbitp = h.bitp

    // 该类型的指针 bitmap
    p = ptrmask

    // ...
    if p != nil {
        // 把 bitmap 第一个字节保存起来
        b = uintptr(*p)
        // p 指向下一个字节
        p = add1(p)
        //
        nb = 8
    }

    // 我们的是简单的 Struct 结构(48==48)
    if typ.size == dataSize {
        // nw == 5 == 40/8,说明扫描到第 5 个字段为止即可。
        // ptrdata 指明有指针的范围在[0, 40]以内,再往外确定就没有指针字段了;
        nw = typ.ptrdata / sys.PtrSize
    } else {
        nw = ((dataSize/typ.size-1)*typ.size + typ.ptrdata) / sys.PtrSize
    }

    switch {
    default:
        throw("heapBitsSetType: unexpected shift")

    case h.shift == 0:
        // b 是类型的   ptr bitmap  =>  00010100
        //              bitPointerAll   =>  00001111
        // hb => 0000 0100
        hb = b & bitPointerAll
        // bitScan => 0001 0000
        // 0001 0000 | 0100 0000 | 1000 0000
        // hb => 1101 0100
        hb |= bitScan | bitScan<<(2*heapBitsShift) | bitScan<<(3*heapBitsShift)
        // 赋值 hbitp => 1101 0100
        *hbitp = uint8(hb)
        // 指针往后一个字节(递进一个字节)
        hbitp = add1(hbitp)
        // b => 0000 0001
        b >>= 4
        // nb => 4
        nb -= 4

    case sys.PtrSize == 8 && h.shift == 2:
        // ...
    }

    // ...
    // 处理完了前 4 bit,接下来处理后 4 bit
    nb -= 4
    for {
        // b => 0000 0001
        // hb => 0000 0001
        hb = b & bitPointerAll
        // hb => 1111 0001
        hb |= bitScanAll
        if w += 4; w >= nw {
            // 处理完了,有指针的字段都包含在已经处理的 ptrmask 范围内了
            break
        }
        // ...
    }

Phase3:
    // Phase 3: Write last byte or partial byte and zero the rest of the bitmap entries.
    // 8 > 5
    if w > nw {
        // mask => 1
        mask := uintptr(1)<<(4-(w-nw)) - 1
        // hb => 0001 0001
        hb &= mask | mask<<4 // apply mask to both pointer bits and scan bits
    }

    // nw => 6
    nw = size / sys.PtrSize

    // ...

    if w == nw+2 {
        // 赋值 hbitp => 0001 0001
        *hbitp = *hbitp&^(bitPointer|bitScan|(bitPointer|bitScan)<<heapBitsShift) | uint8(hb)
    }

Phase4:
    // Phase 4: Copy unrolled bitmap to per-arena bitmaps, if necessary.
    // ...
}

所以,上面函数调用完,h.bitp 就给设置上了:

低字节 -> 高字节 [ 1101 0100 ], [ 0001 0001 ] |–前4*8字节–|–后4*8字节–|

这个就是 mallocgc 内存的时候做的事情。

总结就一句话:根据编译期间针对每个 struct 生成的 type 结构,来设置 gc 需要扫描的位图,也就是指针 bitmap。(旁白:每分配一块内存出去,我都会有一个 bitmap 对应到这个内存块,指明哪些地方有指针)。

运行扫描阶段

1.扫描以 markroot 开始,从栈,全局变量,寄存器等根对象开始扫描,创建一个有向引用图,把根对象投入到队列中,重点的一个函数就是 scanstack 。

2.另外异步的 goroutine 运行 gcDrain 函数,从队列里消费对象,并且扫描这个对象;

扫描调用的就是 scanobject 函数

下面重点介绍:scanstackscanobject 这个函数怎么扫描对象。

scanstack

这个函数是起点函数( 起始最原始的还是 markroot,但是我们这里梳理主线 ),该扫描栈上所有可达对象,因为栈是一个根,因为你做事情总要有个开始的地方,那么“栈”就是 golang 的起点。

func scanstack(gp *g, gcw *gcWork) {
    // ...
    // 扫描栈上所有的可达的对象
    state.buildIndex()
    for {
        p := state.getPtr()
        if p == 0 {
            break
        }
        // 获取一个到栈上对象
        obj := state.findObject(p)
        if obj == nil {
            continue
        }
        // 获取到这个对象的类型
        t := obj.typ
        // ...
        // 获取到这个类型内存块的 ptr 的 bitmap(编译期间编译器设置好)
        gcdata := t.gcdata
        var s *mspan
        if t.kind&kindGCProg != 0 {
            s = materializeGCProg(t.ptrdata, gcdata)
            gcdata = (*byte)(unsafe.Pointer(s.startAddr))
        }

        // 扫描这个对象
        // 起点:对象起始地址 => state.stack.lo + obj.off
        // 终点:t.ptrdata (还记得这个吧,这个指明了指针所在内的边界)
        // 指针 bitmap:t.gcdata
        scanblock(state.stack.lo+uintptr(obj.off), t.ptrdata, gcdata, gcw, &state)

        if s != nil {
            dematerializeGCProg(s)
        }
    }
    // ...
}

小结:

  • 找到这个 goroutine 栈上的内存对象(一个个找,一个个处理);
  • 找到对象之后,获取到这个对象的 type 结构,然后取出 type.ptrdata, type.gcdata ,从而我们就知道扫描的内存范围,和内存块上指针的所在位置;
  • 调用 scanblock 扫描这个内存块;

scanblock

scanblock 这个函数不说你应该知道,这是一个非常底层且通用的函数,他的一切参数都是传入的,这个函数作为一个基础函数被很多地方调用:

/*
b0: 扫描开始的位置
n0: 扫描结束的长度
ptrmask: 指针的 bitmap
*/
func scanblock(b0, n0 uintptr, ptrmask *uint8, gcw *gcWork, stk *stackScanState) {
    b := b0
    n := n0
    // 扫描到长度 n 为止;
    for i := uintptr(0); i < n; {
        // 每个 bit 标识一个 8 字节,8个 bit (1个字节)标识 64 个字节;
        // 这里计算到合适的 bits
        bits := uint32(*addb(ptrmask, i/(sys.PtrSize*8)))
        // 如果整个 bits == 0,那么说明这 8 个 8 字节都没有指针引用,可以直接跳到下一轮
        if bits == 0 {
            i += sys.PtrSize * 8
            continue
        }
        // bits 非0,说明内部有指针引用,就必须一个个扫描查看;
        for j := 0; j < 8 && i < n; j++ {
            // 指针类型?只有标识了指针类型的,才有可能走到下面的逻辑去;
            if bits&1 != 0 {
                p := *(*uintptr)(unsafe.Pointer(b + i))
                if p != 0 {
                    if obj, span, objIndex := findObject(p, b, i); obj != 0 {
                        // 如果这 8 字节指向的是可达的内存对象,那么就投入扫描队列(置灰)保护起来;
                        greyobject(obj, b, i, span, gcw, objIndex)
                    } else if stk != nil && p >= stk.stack.lo && p < stk.stack.hi {
                        stk.putPtr(p)
                    }
                }
            }
            bits >>= 1
            i += sys.PtrSize
        }
    }
}

如果以上面的 TestStruct 结构举例的话,假设在栈上分配了对象 TestStruct{},地址是 0xc00007cf20,那么会从这个地址扫描 scanblock ( 0xc00007cf20, 40, 20, xxx)

type TestStruct struct {
	ptr uintptr
	f1  uint8
	f2  *uint8
	f3  uint32
	f4  *uint64
	f5  uint64
}

示意图如下:

最外层 for 循环一次就够了,里面 for 循环 5 次,扫描到 f4 字段就完了(还记得 type.ptrdata == 40 吧 )。只有 f2 ,f4 字段才会作为指针去扫描。如果 f2, f4 字段存储的是有效的指针,那么指向的对象会被保护起来(greyobject)。

小结:

  • scanblock 这个函数非常简单,只扫描给定的一段内存块;
  • 大循环每次递进 64 个字节,小循环每次递进 8 字节;
  • 是否作为指针扫描是由 ptrmask 指定的;
  • 只要长度和地址是对齐的,指针类型按 8 字节对齐,那么我们按照 8 字节递进扫描一定是全方位覆盖,不会漏掉一个对象的;
  • 再次提醒下,uintptr 是数值类型,编译器不会标识成指针类型,所以不受扫描保护;

scanobject

gcDrain 这个函数就是从队列里不断获取,处理这些对象,最重要的一个就是调用 scanobject 继续扫描对象。

markroot 从根(栈)扫描,把扫描到的对象投入扫描队列。gcDrain 等函数从里面不断获取,不断处理,并且扫描这些对象,进一步挖掘引用关系,当扫描结束之后,那些没有扫描到的就是垃圾了。

还是 TestStruct 举例:

type TestStruct struct {
	ptr uintptr
	f1  uint8
	f2  *uint8
	f3  uint32
	f4  *uint64
	f5  uint64
}

如果一个创建在堆上的 TestStruct 对象被投入到扫描队列,对应的 type.gcdata 是 0001 0100 ,TestStruct 对应编译器创建的 type 类型如下:

(dlv) p typ
*runtime._type {
	size: 48,
	ptrdata: 40,
    ...
	gcdata: *20,
	... }

scanobject 逻辑如下:

/*
b   : 是对象的内存地址
gcw : 是扫描队列的封装
*/
func scanobject(b uintptr, gcw *gcWork) {
    // 通过对象地址 b 获取到这块内存地址对应的 hbits
    hbits := heapBitsForAddr(b)
    // 通过对象地址 b 获取到这块内存地址所在的 span
    s := spanOfUnchecked(b)
    // span 的元素大小
    n := s.elemsize
    if n == 0 {
        throw("scanobject n == 0")
    }
    // ...
    var i uintptr
    // 每 8 个字节处理递进处理(因为堆上对象分配都是 span,每个 span 的内存块都是定长的,所以扫描边界就是 span.elemsize )
    for i = 0; i < n; i += sys.PtrSize {
        if i != 0 {
            hbits = hbits.next()
        }
        // 获取到内存块的 bitmap
        bits := hbits.bits()

        // 确认该整个内存块没有指针,直接跳出,节约时间;
        if i != 1*sys.PtrSize && bits&bitScan == 0 {
            break // no more pointers in this object
        }
        // 确认 bits 对应的小块内存没有指针,所以可以直接到下一轮
        // 如果是指针,那么就往下看看这 8 字节啥情况
        if bits&bitPointer == 0 {
            continue // not a pointer
        }

        // 把这 8 字节里面存的值取出来;
        obj := *(*uintptr)(unsafe.Pointer(b + i))
        // 如果 obj 有值,并且合法(不在一个 span 的内存块里)
        if obj != 0 && obj-b >= n {
            // 如果 obj 指向一个有效的对象,那么把这个对象置灰色,投入扫描队列,等待处理
            if obj, span, objIndex := findObject(obj, b, i); obj != 0 {
                greyobject(obj, b, i, span, gcw, objIndex)
            }
        }
    }
    // ...
}

小结:

  • scanobject 的目的其实很简单:就是进一步发现引用关系,尽可能的把可达对象全覆盖;
  • 这个地方就没有直接使用到 type ,而是使用到 mallocgc 时候的准备成果( heapBitsSetType 设置),每个内存块都对应了一个指针的 bitmap;

总结

  • 要达到“正确并且高效的扫描”需要 编译期间运行分配期间扫描期间 三者配合处理;
  • 内存对齐是非常重要的一个前提条件;
  • 编译期间生成 type 类型,对用户定义的类型全方位分析,标记出所有的指针类型字段;
  • 运行期间,赋值器分配内存的时候,根据 type 结构,设置和对象内存一一对应的 bitmap,标明指针所在位置,以便后续 gc 扫描;
  • 回收器扫描期间,从根部开始扫描,遇到对象,则置灰,投入队列,并且不断的扫描这些对象指向的对象,直到结束。扫描的依据,就根据编译期间生成的 bitmap,分配期间设置的 bitmap 来识别哪些地方有指针,然后进一步处理;
  • 扫描只需要给个开始地址,然后每 8 字节推进就可以扫描了,为了加快效率我们才有了指针的 bitmap (所以这个是个优化项);
  • 再次强调下,定义的非指针类型不受保护,比如 uintptr 里面就算存储的是一个地址的值,也是不会被扫描到的;

到此这篇关于深度剖析Golang如何实现GC扫描对象的文章就介绍到这了,更多相关Golang GC扫描对象内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • go:垃圾回收GC触发条件详解

    版本: go version go1.13 darwin/amd64 在go源码runtime目录中找到gcTrigger结构体,就能看出大致调用的位置 GC调用方式 所在位置 代码 定时调用 runtime/proc.go:forcegchelper() gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()}) 分配内存时调用 runtime/malloc.go:mallocgc() gcTrigger{kind: gcTriggerHe

  • 谈论Go 什么时候会触发 GC问题

    目录 1.什么是 GC 2.为什么要 GC 3.GC 触发场景 3.1系统触发 3.2手动触发 3.3 基本流程 3.4 在哪触发 4.监控线程 5.堆内存申请 在早期经常遭到唾弃的就是在垃圾回收(下称:GC)机制中 STW(Stop-The-World)的时间过长.那么这个时候,我们又会好奇一点,作为 STW 的起始,Go 语言中什么时候才会触发 GC 呢? 1.什么是 GC 在计算机科学中,垃圾回收(GC)是一种自动管理内存的机制,垃圾回收器会去尝试回收程序不再使用的对象及其占用的内存. 最

  • Go语言七篇入门教程七GC垃圾回收三色标记

    目录 GC 如何判断一个对象是否可达 三色标记法 原理如下 GC GC全称Garbage Collection 目前主流的垃圾回收算法有两类,分别是追踪式垃圾回收算法(Tracing garbage collection)和引用计数法( Reference counting ). 而三色标记法是属于追踪式垃圾回收算法的一种. 追踪式算法的核心思想是判断一个对象是否可达,因为一旦这个对象不可达就可以立刻被 GC 回收了. 如何判断一个对象是否可达 分为两步: 第一步找出所有的全局变量和当前函数栈里

  • 图解Golang的GC垃圾回收算法

    虽然Golang的GC自打一开始,就被人所诟病,但是经过这么多年的发展,Golang的GC已经改善了非常多,变得非常优秀了. 以下是Golang GC算法的里程碑: v1.1 STW v1.3 Mark STW, Sweep 并行 v1.5 三色标记法 v1.8 hybrid write barrier 经典的GC算法有三种: 引用计数(reference counting) . 标记-清扫(mark & sweep) . 复制收集(Copy and Collection) . Golang的G

  • 深度剖析Golang如何实现GC扫描对象

    目录 扫描的目的 扫描的实现 运行期内存分配 运行扫描阶段 总结 之前阐述了 golang 垃圾回收通过保证三色不变式来保证回收的正确性,通过写屏障来实现业务赋值器和 gc 回收器正确的并发的逻辑.其中高概率的提到了“扫描队列”和“扫描对象”.队列这个逻辑非常容易理解,那么”扫描对象“ 这个你理解了吗?有直观的感受吗?这篇文章就是要把这个扫描的过程深入剖析下. 扫描的东西是啥?形象化描述下 怎么去做的扫描?形象化描述下 我们就是要把这两个抽象的概念搞懂,不能停留在语言级别浅层面,要知其然知其所以

  • 深度剖析Golang中的数组,字符串和切片

    目录 1. 数组 1.1 定义数组 1.2 访问数组 1.3 修改数组 1.4 数组长度 1.5 遍历数组 1.6 多维数组 2. 切片 2.1 定义切片 2.2 访问切片元素 2.3 修改切片元素 2.4 切片长度和容量 2.5 向切片中添加元素 2.6 切片的切片 2.7 切片排序 3. 字符串 3.1 访问字符串中的字符 3.2 字符串切片 3.3 字符串操作 3.4 关于字符串的常见问题 4. 总结 1. 数组 数组是 Golang 中的一种基本数据类型,用于存储固定数量的同类型元素.在

  • js对象实例详解(JavaScript对象深度剖析,深度理解js对象)

    这算是酝酿很久的一篇文章了. JavaScript作为一个基于对象(没有类的概念)的语言,从入门到精通到放弃一直会被对象这个问题围绕. 平时发的文章基本都是开发中遇到的问题和对最佳解决方案的探讨,终于忍不住要写一篇基础概念类的文章了. 本文探讨以下问题,在座的朋友各取所需,欢迎批评指正: 1.创建对象 2.__proto__与prototype 3.继承与原型链 4.对象的深度克隆 5.一些Object的方法与需要注意的点 6.ES6新增特性 下面反复提到实例对象和原型对象,通过构造函数 new

  • PHP序列化和反序列化深度剖析实例讲解

    序列化 序列化格式 在PHP中,序列化用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构. 序列化函数原型如下: string serialize ( mixed $value ) 先看下面的例子: class CC { public $data; private $pass; public function __construct($data, $pass) { $this->data = $data; $this->pass = $pass; } } $number = 34;

  • MyBatis核心源码深度剖析SQL语句执行过程

    目录 1 SQL语句的执行过程介绍 2 SQL执行的入口分析 2.1 为Mapper接口创建代理对象 2.2 执行代理逻辑 3 查询语句的执行过程分析 3.1 selectOne方法分析 3.2 sql获取 3.3 参数设置 3.4 SQL执行和结果集的封装 4 更新语句的执行过程分析 4.1 sqlsession增删改方法分析 4.2 sql获取 4.3 参数设置 4.4 SQL执行 5 小结 1 SQL语句的执行过程介绍 MyBatis核心执行组件: 2 SQL执行的入口分析 2.1 为Ma

  • 你知道JVM中GC Root对象有哪些吗

    目录 JVM中GC Root对象有哪些 (一)虚拟机栈中引用的对象 (二)方法区中类静态属性引用的对象 (三)方法区中常量引用的对象 (四)本地方法栈中引用的对象 JVM 中的 GC Roots 和可达链 什么是GC Root 对象? 常用的GC算法 GC Root 对象有哪些? 总结 JVM中GC Root对象有哪些 众所周知,我们目前最常用的虚拟机hotspot使用可达性分析来进行垃圾回收,而可达性分析需要依赖GC Root. 下面我就来介绍下可以作为GC Root的对象. (一)虚拟机栈中

  • CSS的margin边界叠加深度剖析图文演示

    边界叠加是一个相当简单的概念.但是,在实践中对网页进行布局时,它会造成许多混淆.简单地说,当两个垂直边界相遇时,它们将形成一个边界.这个边界的高度等于两个发生叠加的边界的高度中的较大者. 当一个元素出现在另一个元素上面时,第一个元素的底边界与第二个元素的顶边界发生叠加,见图: 元素的顶边界与前面元素的底边界发生叠加 当一个元素包含在另一个元素中时(假设没有填充或边框将边界分隔开),它们的顶和/或底边界也发生叠加,见图: 元素的顶边界与父元素的顶边界发生叠加 尽管初看上去有点儿奇怪,但是边界甚至可

  • C语言可变参数列表的用法与深度剖析

    目录 前言 一.可变参数列表是什么? 二.怎么用可变参数列表 三.对于宏的深度剖析 隐式类型转换 对两个函数的重新认知 总结 前言 可变参数列表,使用起来像是数组,学习过函数栈帧的话可以发现实际上他也就是在栈区定义的一块空间当中连续访问,不过他不支持直接在中间部分访问. 声明: 以下所有测试都是在x86,vs2013下完成的. 一.可变参数列表是什么? 在我们初始C语言的第一节课的时候我们就已经接触了可变参数列表,在printf的过程当中我们通常可以传递大量要打印的参数,但是我们却不知道他是如何

  • 示例剖析golang中的CSP并发模型

    目录 1. 相关概念: 2. CSP (通信顺序进程) 3. channel:同步&传递消息 4. goroutine:实际并发执行的实体 5. golang调度器 1. 相关概念: 用户态:当一个进程在执行用户自己的代码时处于用户运行态(用户态) 内核态:当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),引入内核态防止用户态的程序随意的操作内核地址空间,具有一定的安全保护作用.这种保护模式是通过内存页表操作等机制,保证进程间的地址空间不会相互冲突,一个进程的操作不会修改另一个

  • 你真的理解C语言qsort函数吗 带你深度剖析qsort函数

    目录 一.前言 二.简单冒泡排序法 三.qsort函数的使用 1.qsort函数的介绍 2.qsort函数的运用 2.1.qsort函数排序整型数组 2.2.qsort函数排序结构体 四.利用冒泡排序模拟实现qsort函数 五.总结 一.前言 我们初识C语言时,会做过让一个整型数组按照从小到大来排序的问题,我们使用的是冒泡排序法,但是如果我们想要比较其他类型怎么办呢,显然我们当时的代码只适用于简单的整形排序,对于结构体等没办法排序,本篇将引入一个库函数来实现我们希望的顺序. 二.简单冒泡排序法

随机推荐