golang的垃圾回收机制如何?与Python和Java的垃圾回收有何异同?

Java语言是基于分代的,在新生代主要使用了标记-复制,在新生代中分为eden,s0区和s1区。
其中,绝大多数对象都是直接在eden区进行分配,然后s0区和s1区空闲的区域进行复制工作。而老年代主要使用标记-整理算法,将标记后的对象统一移动到内存区域的前部分,然后对后面的区域进行清理。
而go当中主要使用了标记-清除算法,其主要原理是对对象进行标记,然后回收未被标记过的对象。具体的算法就不在此详述,本文想要探索的是java和go当中垃圾回收的不同点。

要想找到不同点,首先要找到相同点。即标记,三种算法均是基于标记算法产生的,不同地方在于对于标记后的行为的处理,但这其实并不是关键,因为不管是复制、整理还是清除,均是基于内存区域特点或语言特点决定的,如新生代常常大批量死亡,所以只需要复制存活对象,这样效率高,而老年代主要是大对象以及存活时间较长的对象,变化不大,因此进行整理,而go语言追求简单,因此使用清除。

三色标记法垃圾回收

一开始所有的引用都是白色。
从栈和全局变量出发进行遍历,先把栈和全局变量标为灰色,标灰是入队的过程。
对于队列中的结点,遍历它所指向的引用,把它的邻居放进队列。
当队列中节点出队时,把结点标记为黑色,表示已经处理结束。
遍历完成之后,最终为白色的就是垃圾。

应该让白色垃圾尽量少,因为白色垃圾一旦错误回收,就会产生致命错误;而白色垃圾延迟回收,在下一轮垃圾回收中依旧能够回收掉。
如果是灰色结点引用了白色结点,那不需要做任何处理,只需要等处理灰色结点的时候就能把这个白色结点染成灰色。
如果是黑色结点引用了白色结点,这种需要特殊处理,不然这个白色结点明明在使用,结果被回收了。
Java的CMS垃圾回收器就是写屏障+增量更新模式。当黑色结点引用白色结点的时候,把白色结点放入队列。

垃圾回收中的写屏障write barrier是什么?

写屏障是一种编译技术,可以用于优化垃圾回收。
把Java、Go编译的时候,涉及到引用变化的语句,插入一些写屏障。其实就是跟垃圾回收器做一些通信。

协程为什么比线程轻量?轻量在何处?

线程属于操作系统的概念,线程切换涉及到系统调用。
协程属于语音内部运行机制的概念。
协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
内核空间:操作系统空间;用户空间:程序空间。

协程切换比线程切换快主要有两点: (1)协程切换完全在用户空间进行,线程切换涉及特权模式切换,需要在内核空间完成;
(2)协程切换相比线程切换做的事情更少。

协程切换只涉及基本的CPU上下文切换,所谓的 CPU 上下文,就是一堆寄存器,里面保存了 CPU运行任务所需要的信息:从哪里开始运行(%rip:指令指针寄存器,标识 CPU 运行的下一条指令),栈顶的位置(%rsp: 是堆栈指针寄存器,通常会指向栈顶位置),当前栈帧在哪(%rbp 是栈帧指针,用于标识当前栈帧的起始位置)以及其它的CPU的中间状态或者结果(%rbx,%r12,%r13,%14,%15 等等)。协程切换非常简单,就是把当前协程的 CPU 寄存器状态保存起来,然后将需要切换进来的协程的 CPU 寄存器状态加载的 CPU 寄存器上就 ok 了。而且完全在用户态进行,一般来说一次协程上下文切换最多就是几十ns 这个量级。下面给出 libco 的协程切换的汇编代码,也就是二十来条汇编指令,完成当前协程 CPU 寄存器的保存,并恢复调度进来的 CPU 寄存器状态,类似的也可以参考 boost context 里面的切换汇编代码,大同小异。

线程切换系统内核调度的对象是线程,因为线程是调度的基本单元(进程是资源拥有的基本单元,进程的切换需要做的事情更多,这里占时不讨论进程切换),而线程的调度只有拥有最高权限的内核空间才可以完成,所以线程的切换涉及到用户空间和内核空间的切换,也就是特权模式切换,然后需要操作系统调度模块完成线程调度(taskstruct),而且除了和协程相同基本的 CPU 上下文,还有线程私有的栈和寄存器等,说白了就是上下文比协程多一些,其实简单比较下 task_strcut 和 任何一个协程库的 coroutine 的 struct 结构体大小就能明显区分出来。而且特权模式切换

golang中make和new的区别?

  1. make返回引用,new返回指针
  2. 变量类型不同:new用于string、int和数组等,make用于切片、map、channel。

数组和切片的区别?

  1. 数组是定长,访问和复制不能超过数组定义的长度。切片的长度和容量可以自动扩容。
  2. 数组是值类型,切片是引用类型,切片一旦扩容,指向一个新的底层数组,内存地址随之改变。

数组的定义

var a1 [3]int
var a2 [...]int{1,2,3}

切片的定义

var a1 []int
var a2 :=make([]int,3,5)

数组的初始化

a1 := [...]int{1,2,3}
a2 := [5]int{1,2,3}

切片的初始化

b:= make([]int,3,5)

讲讲 Go 的 select 底层数据结构和一些特性?(难点,没有项目经常可能说不清,面试一般会问你项目中怎么使用select)

答:go 的 select 为 golang 提供了多路 IO 复用机制,和其他 IO 复用一样,用于检测是否有读写事件是否 ready。linux 的系统 IO 模型有 select,poll,epoll,go 的 select 和 linux 系统 select 非常相似。

select 结构组成主要是由 case 语句和执行的函数组成 select 实现的多路复用是:每个线程或者进程都先到注册和接受的 channel(装置)注册,然后阻塞,然后只有一个线程在运输,当注册的线程和进程准备好数据后,装置会根据注册的信息得到相应的数据。

channel知识点

1.传入channel的值是原来的备份,从channel中取出来的值也是通道中值的备份

2.如果想通过channel传输同一个值,那么可以传递这个值的指针

3.如果关闭channel要从发送端关闭,如果从接收端关闭会引发恐慌

4.发送端关闭通道不会影响接收端接收

5.带缓冲区和不带缓冲区的channel区别就是长度是否为0,不带缓冲区的channel的长度就是0

6.操作未被初始化的通道会造成永久阻塞

什么是channel?

chan是Go中的一种特殊类型,不同的协程可以通过channel来进行数据交互。

channel分为有缓冲区与无缓冲区两种

channel底层?

channel是基于环形队列实现的。

gmp模型

go map 底层 扩容机制

有缓存和无缓存channel的区别

2个协程交替打印奇数偶数

package main
 
import (
	"fmt"
	"sync"
)
 
//打印奇数
func PrintOddNumber(wg *sync.WaitGroup, ch chan int, num int) {
	defer wg.Done()
	for i := 0; i <= num; i++ {
		ch <- i
		if i%2 != 0 {
			fmt.Println("奇数:", i)
		}
	}
}
 
//打印偶数
func PrintEvenNumber(wg *sync.WaitGroup, ch chan int, num int) {
	defer wg.Done()
	for i := 1; i <= num; i++ {
		<-ch
		if i%2 == 0 {
			fmt.Println("偶数:", i)
		}
	}
}
func main() {
	wg := sync.WaitGroup{}
	ch := make(chan int)
	wg.Add(1)
	go PrintOddNumber(&wg, ch, 10)
	go PrintEvenNumber(&wg, ch, 10)
	wg.Wait()
}

逃逸分析说下?为什么要逃逸分析?如何避免逃逸

Golang的GMP调度模型(Golang面试必问)