Blog

转移(Evacuation)

转移(Evacuation) #

G1 的垃圾清理是通过把活跃的对象,从一个 Region 拷贝到另一个空白 Region,这个空白 Region 隶属于 Survivor 空间。这个过程在 G1 GC 中被命名为转移(Evacuation)。它和基于 copy 的 GC 的最大区别是:它可以充分利用 concurrent mark 的结果快速定位到哪些对象需要被拷贝。

接下来让我们通过一个例子,来看看 G1 Evacuation 的具体过程吧。 在上图中,Region2 是一个待回收的 Region,隶属于 CSet。在它的专属 RSet 中记录了 Region1 的第二个 card 和 Region3 的第一个 card,说明 Region1 和 Region3 有对 Region2 的对象引用,Region4 是一个被选为 Survivor 的空白 Region。

假如 Region1 和 Region3 都经过了并发标记,识别出 A 对象是垃圾对象,而 E 对象是活跃对象。那么,我们就可以从活跃对象 E 开始进行遍历。注意,这一次遍历的目标是把 Region2 中的对象搬移到 Region4。

Region1 中的 A 是垃圾对象,这在并发标记阶段就已经发现了,所以在转移阶段就不会再起作用了。进而,Region2 中的 B、C 也不会被标记到,最终只有对象 D 被拷贝到了 Region4,与此同时,原始 Region2 的 RSet 也会被维护到 Region4。

...

G1的并发标记过程

G1的并发标记过程 #

因为 Evacuation 发生的时机是不确定的,在并发标记阶段也可能发生。所以并发标记要使用一个 BitMap 来记录活跃对象,而 Evacuation 也需要使用一个 BitMap 来将活跃的对象进行搬移。这就产生了读和写的冲突:并发标记需要写 BitMap,而 Evacuation 需要读 BitMap。

为了解决这个问题,G1 维护了两个 BitMap,一个名为 nextBitMap,一个名为 prevBitMap。其中,prevBitMap 是用于搬移活跃对象,而 nextBitMap 则用于并发标记记录活跃对象。

当并发标记开始以后,新的对象仍然有可能会被继续分配。内存管理器把这些对象全部认为是活跃对象。TAMS 指针,是 Top At Mark Start 的缩写。

这是初始时状态,prevTAMS,nextTAMS 和 top 指针都指向一个分区的开始位置。 随着业务线程的执行,top 指针不断向后移动。并发标记开始时,nextTAMS 记录下当前的 top 指针,并且针对 nextTAMS 之前的对象进行活跃性扫描,扫描的结果就存放在 nextBitMap 中。

当并发标记结束以后,nextTAMS 的值就记录在 prevTAMS 中,并且 nextBitMap 也赋值给 prevBitMap。 如果此时发生了 Evacuation,则 prevBitMap 已经可用了。如果没有发生 Evacuation,那么 nextBitMap 就会清空,为下一轮并发标记做准备。这样就可以保证,在任意时刻开启 Evacuation 的话,prevBitMap总是可用的。

在并发标记开始以后,再创建的对象,其实就是 nextTAMS 指针到 top 指针之间的对象,这些对象全部认为是活跃的(注意观察图中紫色部分)。

我们再从对象活跃性的角度理解两个 TAMS 指针和 top 的关系。当并发标记开始时,nextTAMS 就固定了,但是 top 还是可能继续向后移,所以 nextTAMS 和 top 之间的对象在这次标记过程中都被认为是活跃对象。当 Evacuation 开始时,它只使用 prevBitMap 的信息,显然 prevBitMap 中的信息只能覆盖到 prevTAMS 处,所以从 prevTAMS 到 top 的对象就都认为是活跃的。

...

RSet的存放形式

RSet的存放形式 #

考虑某个 Region 的 RSet,它可能会因为引用关系比较多,而变得很大。根据另一个 Region 对这个 Region 的引用数量,可以分为少、中、多三种情况。针对这三种情况,RSet 准备了三种不同的数据结构来应对,分别是稀疏表、细粒度表和粗粒度表。三种表之间的关系是不断粗化的,如下图所示:

  1. 稀疏表是一个哈希表,当 Region A 对 Region B 的引用很少时,就可以将相关的 card 放到稀疏表里;
  2. 细粒度表则是一个真正的 card table,当 Region 之间的引用比较多时,就可以直接使用位图来代替哈希表,因为这能加快查找的速度(使用位操作代替哈希表的查找);
  3. 粗粒度表则是一个区的位图,因为相对来说,区是比较少的,所以粗粒度表的大小也很小。当 Region A 对 Region B 的引用非常多时,就不用再使用 card table 来进行管理了,在回收 Region B 时,直接将 Region A 的全部对象都遍历一次就可以了。

总之,随着其他 Region 对本 Region 的引用关系越多,RSet 存放引用关系使用的表粒度就越粗,这样做主要是为了减少 RSet 记录数,提高定位效率。

Viewpoint #

From #

22 | G1 GC:分区回收算法说的是什么?

学习金字塔

学习金字塔 #

1946 年,美国学者埃德加·戴尔(Edgar Dale)提出了「学习金字塔」(Cone of Learning)的理论。之后,美国缅因州国家训练实验室也做了相同的实验,并发布了「学习金字塔」报告。 人的学习分为「被动学习」和「主动学习」两个层次。

  1. 被动学习:如听讲、阅读、视听、演示,学习内容的平均留存率为 5%、10%、 20% 和 30%。
  2. 主动学习:如通过讨论、实践、教授给他人,会将原来被动学习的内容留存率从 5% 提升到 50%、75% 和 90%。

这个模型很好地展示了不同学习深度和层次之间的对比。

Viewpoint #

From #

96 | 高效学习:端正学习态度

defer表达式的执行次序

defer表达式的执行次序 #

defer 关键字后面的表达式,是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。我们用一个典型的例子来说明一下 defer 后表达式的求值时机:

func foo1() {
    for i := 0; i <= 3; i++ {
        defer fmt.Println(i)
    }
}
func foo2() {
    for i := 0; i <= 3; i++ {
        defer func(n int) {
            fmt.Println(n)
        }(i)
    }
}
func foo3() {
    for i := 0; i <= 3; i++ {
        defer func() {
            fmt.Println(i)
        }()
    }
}
func main() {
    fmt.Println("foo1 result:")
    foo1()
    fmt.Println("\nfoo2 result:")
    foo2()
    fmt.Println("\nfoo3 result:")
    foo3()
}

foo1 中 defer 后面直接用的是 fmt.Println 函数,每当 defer 将 fmt.Println 注册到 deferred 函数栈的时候,都会对 Println 后面的参数进行求值。根据上述代码逻辑,依次压入 deferred 函数栈的函数是:

...

defer的运作机制

defer的运作机制 #

defer 是 Go 语言提供的一种延迟调用机制,defer 的运作离不开函数。

  1. 在 Go 中,只有在函数(和方法)内部才能使用 defer;
  2. defer 关键字后面只能接函数(或方法),这些函数被称为 deferred 函数。

defer 将它们注册到其所在 Goroutine 中,用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前,按后进先出(LIFO)的顺序被程序调度执行(如下图所示)。

而且,无论是执行到函数体尾部返回,还是在某个错误处理分支显式 return,又或是出现 panic,已经存储到 deferred 函数栈中的函数,都会被调度执行。所以说,deferred 函数是一个可以在任何情况下为函数进行收尾工作的好“伙伴”。

Viewpoint #

From #

23|函数:怎么让函数更简洁健壮?

程序计数器电路设计

程序计数器电路设计 #

有了时钟信号,我们可以提供定时的输入;有了 D 型触发器,我们可以在时钟信号控制的时间点写入数据。我们把这两个功能组合起来,就可以实现一个自动的计数器了。

加法器的两个输入,一个始终设置成 1,另外一个来自于一个 D 型触发器 A。我们把加法器的输出结果,写到这个 D 型触发器 A 里面。于是,D 型触发器里面的数据就会在固定的时钟信号为 1 的时候更新一次。

这样,我们就有了一个每过一个时钟周期,就能固定自增 1 的自动计数器了。这个自动计数器,可以拿来当我们的 PC 寄存器。事实上,PC 寄存器的这个 PC,英文就是 Program Counter,也就是程序计数器的意思。

每次自增之后,我们可以去对应的 D 型触发器里面取值,这也是我们下一条需要运行指令的地址。另外,同一个程序的指令应该要顺序地存放在内存里面,其目的就是为了让我们通过程序计数器就能定时地不断执行新指令。

加法计数、内存取值,乃至后面的命令执行,最终其实都是依靠时钟信号,来控制执行时间点和先后顺序的,这也是我们需要时序电路最核心的原因。

Viewpoint #

From #

19 | 建立数据通路(下):指令+运算=CPU

recover的过程

recover的过程 #

可以通过 recover 函数来实现捕捉 panic 并恢复程序正常执行秩序。

Panicking过程,在触发 panic 的 bar 函数中,对 panic 进行捕捉并恢复,我们直接来看恢复后,整个程序的执行情况是什么样的。

func bar() {
    defer func() {
        if e := recover(); e != nil {
            fmt.Println("recover the panic:", e)
        }
    }()
    println("call bar")
    panic("panic occurs in bar")
    zoo()
    println("exit bar")
}

我们在一个 defer 匿名函数中调用 recover 函数对 panic 进行了捕捉。 recover 是 Go 内置的专门用于恢复 panic 的函数,它必须被放在一个 defer 函数中才能生效。如果 recover 捕捉到 panic,它就会返回以 panic 的具体内容为错误上下文信息的错误值。如果没有 panic 发生,那么 recover 将返回 nil。而且,如果 panic 被 recover 捕捉到,panic 引发的 panicking 过程就会停止。

...

Panicking过程

Panicking过程 #

Go 官方文档以手工调用 panic 函数触发 panic 为例,对 panicking 这个过程进行了诠释:当函数 F 调用 panic 函数时,函数 F 的执行将停止。不过,函数 F 中已进行求值的 deferred 函数都会得到正常执行,执行完这些 deferred 函数后,函数 F 才会把控制权返还给其调用者。

对于函数 F 的调用者而言,函数 F 之后的行为就如同调用者调用的函数是 panic 一样,该panicking过程将继续在栈上进行下去,直到当前 Goroutine 中的所有函数都返回为止,然后 Go 程序将崩溃退出。

我们用一个例子来更直观地解释一下 panicking 这个过程:

func foo() {
    println("call foo")
    bar()
    println("exit foo")
}
func bar() {
    println("call bar")
    panic("panic occurs in bar")
    zoo()
    println("exit bar")
}
func zoo() {
    println("call zoo")
    println("exit zoo")
}
func main() {
    println("call main")
    foo()
    println("exit main")
}

上面这个例子中,从 Go 应用入口开始,函数的调用次序依次为main -> foo -> bar -> zoo。在 bar 函数中,我们调用 panic 函数手动触发了 panic。

...

股本与资本公积

资本公积与股本 #

在中国,股本必须等于注册资本,或者更准确地说,是实缴到位的注册资本。如果股东实际投入的资金比注册资本多,那多出来的这块,就是所谓的资本公积。

不是每一家公司都一定有资本公积,但有一种公司却一定有,这就是上市公司。比如说,小明的公司是一家上市公司,它发行了1亿股的股票。假设每股的面值是1块钱,那就意味着这1亿股对应着1亿元的股本,如果每股售价为20块,那就相当于筹集了20亿元资金。在这20亿元资金里,只有1亿元是股本,剩下的19亿元就是资本公积,这就是资本公积的概念。

资本公积 #

企业通过​​非经营利润​​形成的所有者权益,主要来源于股东投入但​​不构成股本​​的部分,或资产重估增值等。​​特点​​:

  • ​​不可用于分红​​(需符合法定条件才能转增股本)。

​​- 非利润来源​​,与企业经营活动无直接关系。

股本 #

公司通过发行股票从股东处获得的​​基础资本​​,对应股票的​​面值总额​​。​​特点​​:

  • ​​代表股东的基本出资​​,是公司注册资本的组成部分。

​​- 不可随意减少​​(需经法定程序,如减资公告)。

举例说明​​ #

​​1. 股票发行溢价

​​公司A​​发行1000万股,面值1元/股,发行价10元/股。

​​股本​​ = 1000万股 × 1元 = ​​1000万元​​(计入“股本”科目)。

​​资本公积​​ = (10元 - 1元) × 1000万股 = ​​9000万元​​(计入“资本公积-股本溢价”)。

​2. 资产重估增值​​​​公司B​​有一块土地,账面价值500万元,经评估增值至800万元。

​​资本公积增加​​ = 800万 - 500万 = ​​300万元​​(计入“资本公积-其他资本公积”)。

  1. 资本公积转增股本

​​公司C​​有资本公积5000万元,决定每10股转增5股(面值1元):

转增股本 = 5000万元 ÷ 1元 = ​​5000万股​​。

​​股本增加5000万元​​,​​资本公积减少5000万元​​(所有者权益总额不变)。

From #