Go语言学习
  • README
  • Go 基础
    • go语言介绍
    • go语言主要特性
    • go内置类型和函数
    • init函数和main函数
    • 下划线
    • iota
    • 字符串
    • 数据类型:数组与切片
    • 数据类型:byte、rune与字符串
    • 变量的5种创建方式
    • 数据类型:字典
    • 指针
    • 数据类型:指针
    • 类型断言
    • 流程控制:defer延迟执行
    • 异常机制:panic和recover
    • 函数
    • go依赖管理
    • go中值传递、引用传递、指针传递区别
  • 标准库
    • Go net/http包
  • 数据结构
    • 哈希表
      • 为什么对 map 的 key 或 value 进行取址操作是不允许的
  • Gin
    • gin 快速开始
    • gin-swagger用法
  • Go 进阶
    • Go 指针
    • Go 中的 GC 演变是怎样的?
    • Go 的堆和栈
  • 面向对象
    • make 和 new 的区别
    • new(T) 和 &T{} 有什么区别?
  • 并发编程
    • Channel
    • Go语言 CSP 并发模型
    • GMP 模型原理
      • GMP 模型为什么要有 P ?
    • Go 协程池(goroutine pool)
    • Go语言常见的并发模式
    • Go并发实践:主动停止goroutine
  • 最佳实践
    • 发布Go语言模块
  • 软件包
    • 常用的GoLang包工具
    • Go的UUID生成
    • 现代化的命令行框架Cobra
    • 配置解析神器Viper
    • Go发送邮件gomail
    • Go反射框架Fx
    • NSQ消息队列的使用
    • Go爬虫框架colly
    • grpc-go 的安装和使用
Powered by GitBook
On this page
  • 三个结论:
  • 下面做实验来证明三个结论:
  • 实验一:
  • 实验二:
  • 中间说明
  • 实验三:

Was this helpful?

  1. Go 进阶

Go 的堆和栈

三个结论:

  1. golang的垃圾回收是针对堆的(垃圾回收都是针对堆的,这里只是做一个简单的证明)

  2. 引用类型的全局变量内存分配在堆上,值类型的全局变量分配在栈上

  3. 局部变量内存分配可能在栈上也可能在堆上

堆和栈的简单说明: 1.栈(操作系统):由操作系统自动分配释放 2.堆(操作系统): 一般由程序员分配释放,例如在c/c++中,在golang,java,python有自动的垃圾回收机制

我们都知道变量占有内存,内存在底层分配上有堆和栈。

  • 值类型变量的内存通常是在栈中分配

  • 引用类型变量的内存通常在堆中分配

注意这里说的是"通常",因为变量又分为局部变量和全局变量。

  • 当变量是全局变量时,符合上面所说的分配规则

  • 但当变量是局部变量时,分配在堆和栈就变的不确定了

以下是摘录go圣经的一部分:

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。

var global *int

func f() {
    var x int
    x = 1
    global = &x
}

func g() {
    y := new(int)
    *y = 1
}

函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量*y将是不可达的,也就是说可以马上被回收的。因此,y并没有从函数g中逃逸,编译器可以选择在栈上分配y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

下面做实验来证明三个结论:

实验一:

说明的问题:引用类型的全局变量的内存可以被垃圾回收

package main

import (
	"log"
	"runtime"
)

var lastTotalFreed uint64 // size of last memory has been freed

func InitMap(a map[int]int) {
	for i := 0; i < 100000; i++ {
		a[i] = i
	}
}
func ClearMap(a map[int]int) {
	for i := 0; i < 100000; i++ {
		delete(a, i)
	}
}

var a map[int]int

func main() {
	a = make(map[int]int)
	printMemStats()

	InitMap(a)
	runtime.GC()
	printMemStats()

	ClearMap(a)
	runtime.GC()
	printMemStats()

	a = nil
	runtime.GC()
	printMemStats()

}
func printMemStats() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	log.Printf("Alloc = %v TotalAlloc = %v  Just Freed = %v Sys = %v NumGC = %v\n",
		m.Alloc/1024, m.TotalAlloc/1024, ((m.TotalAlloc-m.Alloc)-lastTotalFreed)/1024, m.Sys/1024, m.NumGC)

	lastTotalFreed = m.TotalAlloc - m.Alloc
}

实验结果:

2019/12/19 17:03:59 Alloc = 88 TotalAlloc = 88  Just Freed = 0 Sys = 1700 NumGC = 0
2019/12/19 17:03:59 Alloc = 2798 TotalAlloc = 5739  Just Freed = 2940 Sys = 9502 NumGC = 2
2019/12/19 17:03:59 Alloc = 2799 TotalAlloc = 5740  Just Freed = 1 Sys = 9502 NumGC = 3
2019/12/19 17:03:59 Alloc = 91 TotalAlloc = 5741  Just Freed = 2708 Sys = 9502 NumGC = 4

实验结论:

引用类型的全局变量的内存被垃圾回收,先不下结论引用类型的全局变量的内存分配在栈上还是堆上

实验二:

说明的问题:引用类型的局部变量的内存没被垃圾回收

将代码中的a改为局部变量,其他地方完全不懂。为了简洁,就不上代码了

直接看实验结果:

2019/12/19 17:11:13 Alloc = 89 TotalAlloc = 89  Just Freed = 0 Sys = 1700 NumGC = 0
2019/12/19 17:11:13 Alloc = 2797 TotalAlloc = 5737  Just Freed = 2940 Sys = 9566 NumGC = 2
2019/12/19 17:11:13 Alloc = 2797 TotalAlloc = 5738  Just Freed = 1 Sys = 9566 NumGC = 3
2019/12/19 17:11:13 Alloc = 2797 TotalAlloc = 5740  Just Freed = 1 Sys = 9566 NumGC = 4

实验结论:

引用类型的局局变量的内存没被垃圾回收,先不下结论引用类型的局局变量的内存分配在栈上还是堆上

中间说明

做了以上两个实验。只能知道内存是否被垃圾回收,至于回收的是栈上内存还是堆上内存不得而知。原因在于不知道变量内存在栈上还是堆上。

为了解决这一问题,我们按照go圣经所说,将局部变量做一步文中的处理,是它必须在推上分配内存

实验三:

说明的问题:

  • 垃圾回收的就是堆上的内存空间

  • 局部变量内存可能在栈上,也可能在堆上分配内存,

实验代码:

package main

import (
	"log"
	"runtime"
)

var lastTotalFreed uint64 // size of last memory has been freed

func InitMap(a map[int]int) {
	for i := 0; i < 100000; i++ {
		a[i] = i
	}
}
func ClearMap(a map[int]int) {
	for i := 0; i < 100000; i++ {
		delete(a, i)
	}
}

var global *map[int]int

func main() {
	a := make(map[int]int)
	printMemStats()
	global = &a
	InitMap(a)
	runtime.GC()
	printMemStats()

	ClearMap(a)
	runtime.GC()
	printMemStats()

	a = nil
	runtime.GC()
	printMemStats()

}
func printMemStats() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	log.Printf("Alloc = %v TotalAlloc = %v  Just Freed = %v Sys = %v NumGC = %v\n",
		m.Alloc/1024, m.TotalAlloc/1024, ((m.TotalAlloc-m.Alloc)-lastTotalFreed)/1024, m.Sys/1024, m.NumGC)

	lastTotalFreed = m.TotalAlloc - m.Alloc
}

实验结果:

2019/12/19 17:22:42 Alloc = 89 TotalAlloc = 89  Just Freed = 0 Sys = 1700 NumGC = 0
2019/12/19 17:22:42 Alloc = 2794 TotalAlloc = 5731  Just Freed = 2937 Sys = 9566 NumGC = 2
2019/12/19 17:22:42 Alloc = 2794 TotalAlloc = 5732  Just Freed = 1 Sys = 9566 NumGC = 3
2019/12/19 17:22:42 Alloc = 91 TotalAlloc = 5733  Just Freed = 2704 Sys = 9566 NumGC = 4

实验结论:

由于对局部变量做了处理,使其确定在堆上分配内存,就被垃圾回收了

由此证明,Go语言的GC回收只回收堆上的内存

PreviousGo 中的 GC 演变是怎样的?Nextmake 和 new 的区别

Last updated 1 year ago

Was this helpful?

个人理解补充说明:go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。 可参考

(以下实验方法说明请参照)

Golang中的逃逸分析
Go的原生map中删除元素,内存会自动释放吗?