内存分析
能栈则栈,逃逸才堆
Go 编译器会在编译时进行逃逸分析,以此决定对象应该分配在栈上还是堆上:
- 分配在栈上(高效):如果编译器能确定函数返回后,该变量不再被引用,就会把它分配在栈上。栈的分配和回收只是移动两个指针,开销极低,且无需垃圾回收介入。
- 分配在堆上(代价高):如果变量在函数返回后仍有其他部分引用,就必须分配在堆上。堆分配需要寻找空闲内存,且最终依赖垃圾回收器来清理,开销相对较大。
栈随着线程的创建而创建,jvm一启动就创建堆内存,方法一调用就放进栈里面,对象一new就放到堆里面 真的吗
“栈随着线程的创建而创建”
无论是Java还是Go中的 goroutine都有自己的栈。线程启动时,系统会分配一块连续的内存作为栈,用来存放该方法调用链上的局部变量和方法参数。
Java 栈:通常大小固定(可通过
-Xss设置)。Go 栈:设计上更灵活,初始时很小(如 2KB),可按需动态增长,因此 Go 可以轻松创建成千上万个 goroutine。
“JVM 一启动就创建堆内存”
- java中JVM 启动时向操作系统申请并初始化堆内存。 这个堆是所有线程共享的内存区域,用于存放类的实例对象和数组。
“方法一调用就放进栈里面”
- 这叫“栈帧”。 每当调用一个方法,JVM 就会在当前线程的栈上创建一个栈帧。方法执行完毕后,对应的栈帧被弹出并销毁,这就是为什么局部变量能自动清理。Go 语言的工作方式也类似。
“对象一 new 就放到堆里面”
在 Java 中:成立。通常认为
new出来的对象都在堆中。这使得垃圾回收器必须频繁扫描堆,是 Java 早期性能开销的一个来源。在 Go 中:不成立。Go 并非所有
new或make出来的对象都放在堆里。Go 编译器会通过逃逸分析决定,如果对象没有逃逸出函数作用域,也是直接在栈上分配,从而减轻垃圾回收器的负担。
逃逸场景
1. 向外共享:返回局部变量指针
1
2
3
4
5
6
7
8
type User struct {
Name string
}
func CreateUser(name string) *User {
u := User{Name: name} // 这个u会被共享给调用方
return &u // 发生逃逸,u分配到堆上
}
2. 向内共享:闭包捕获变量
1
2
3
4
5
6
7
8
func Counter() func() int {
count := 0 // 这个count被下面的匿名函数共享了
return func() int { // 匿名函数引用了外部变量count
count++
return count
}
// 即使Counter函数返回,匿名函数依然持有count,所以count在堆上
}
3. 接口多态:存储在接口中的值
在把具体类型的值存入接口类型变量时,Go 编译器有时无法确定它的具体类型信息,为了安全起见,也会让它逃逸到堆上。
1
2
3
4
5
6
7
8
func PrintValue(v interface{}) { // 函数接收接口类型
fmt.Println(v)
}
func main() {
num := 100
PrintValue(num) // num被传入接口参数,很可能逃逸到堆上
}
4. 全局共享:全局变量
如果一个变量被定义为包级别的全局变量,它的作用域是全局的,任何函数都能访问它。它无法被分配在某个函数的栈上,必须分配在堆上。
5. 大小共享:栈空间不足
这是一种特殊的“共享”是跟编译器相关的。如果一个对象非常大(例如一个 10MB 的数组),即使它完全在函数内部使用,没有返回指针,编译器也可能因为当前 goroutine 的栈空间可能不够,而把这个大对象分配到堆上。