首页 > 系统相关 >【Go 实践学习】内存泄漏情景及pprof工具使用(上半篇)

【Go 实践学习】内存泄漏情景及pprof工具使用(上半篇)

时间:2024-09-03 16:20:55浏览次数:7  
标签:Node 泄漏 pprof res 切片 内存 func Go 半篇

目录


什么是内存泄漏?

golang本身是拥有GC机制的,GC机制帮我们去处理掉那些我们程序之后不在使用的内存,以缓解程序在运行期间里,内存占用率不断上升,最终达到临界值而出现内存溢出(OOM)的问题。
所谓内存泄漏直白来讲即:程序主观上不再想去使用的内存,但是客观上却又持续占有,而是GC系统无法通过三色算法去回收的问题。
所以我们在开发中也并能完全的信任GC,而不去管程序中内存的使用。因此,保持一个良好的开发习惯是非常重要的。

夫人,你也不想你的代码周末的时候突然在线上挂掉吧

两类内存泄漏

如果非要把内存泄漏在进行详细的归类的话,那么还能再分成两类:暂时性内存泄漏与永久性内存泄漏

暂时性内存泄漏

所谓暂时性内存泄漏,即该释放的内存资源没有释放掉,但是这个资源并不是无法释放,而是在之后的更晚的时候才能释放掉。这种情况一般发生在stringslice或者一些存在浅拷贝的引用下出现的资源共享,或者是defer导致的资源没法及时释放。
比如接下来会提到的一个经典slice切片取值导致的暂时性内存泄漏

package main

import (
	"fmt"
)

func main() {
	var sliceG []int
	sliceG = f1()

	fmt.Println(len(sliceG), cap(sliceG)) // 1 99999
}

func f1() []int {
	slice1 := make([]int, 100000)
	slice1[6] = 1
	return slice1[3:10]
}

可以看到,虽然slice1不再使用了,但是切片的浅拷贝的资源共享,导致slice1的底层资源并没有及时得到释放,需要等到sliceG释放才能随着GC被释放。

永久性内存泄漏

永久性内存泄漏即在进程后续生命周期内,泄露的内存永远不会被回收,比如创建的 goroutine 出现死循环或者select case无法得到退出信号,导致的无法退出的情况,从而使协程栈及引用内存永久泄露问题。

比如在Web服务中,如果在某些服务中存在这样永久性内存泄漏,那将是致命的,很有可能在某个安然入睡的夜晚,线上的项目就突然挂掉了。比如下面这个代码,没有正确的设置关闭任务,而出现协程泄漏的情况

package main

import (
	"fmt"
	"net/http"
	"time"
)

var i int = 0

// 处理请求的函数
func handler(w http.ResponseWriter, r *http.Request) {
	// 创建一个停止信号通道
	stop := make(chan struct{})
	// 启动一个新的 Goroutine
	go workFunc(stop)
	// 向客户端响应
	fmt.Fprintf(w, "Task started.")

	// 注意:这里的 close(stop) 被注释掉了,Goroutine 将永远不会停止
	// defer close(stop)
}

func workFunc(stop chan struct{}) {
	// 模拟持续运行的任务
	groutineId := i
	i++
	fmt.Println("Working...")

	time.Sleep(1 * time.Second)

	fmt.Println("Task finished.")
	for {
		select {
		case <-stop:
			// 收到停止信号,退出 Goroutine
			fmt.Println("Goroutine stopped.")
			return
		default:
			// 模拟一些工作
			time.Sleep(1 * time.Second)
			fmt.Printf("goroutine %d Leaking...\n", groutineId)
		}
	}
}

func main() {
	http.HandleFunc("/", handler)

	port := ":8080"
	fmt.Printf("Starting server on http://localhost%s\n", port)
	err := http.ListenAndServe(port, nil)
	if err != nil {
		fmt.Println("Error starting server:", err)
	}
}

每当有用户访问页面时就会创建一个新的协程,但是这个协程的设计了一个for - loop select case监听机制,却永远监听不到停止指令,出现协程泄漏。而协程泄漏又会导致协程申请的资源也无法释放掉,从而使整个系统的内存占用不断升高,最终出现OOM错误。


常见的内存泄漏及解决办法

go101内存泄漏论坛上看了看了一些内存泄漏的场景,在此总结一些,并举例方便大家去理解这些案例

浅拷贝共享底层资源,导致无关内存无法释放

子切片导致的内存泄漏

func main() {
	// 场景1:浅拷贝 共享底层 导致大内存无法释放
	s := scene1()
	fmt.Println(len(s), cap(s)) // 2 1000
}
func scene1() []int {
	sliceBase := make([]int, 1000)
	sliceSmall := sliceBase[0:2]
	return sliceSmall
}

scene1sliceBasesliceSmall引用了底层数据,并且sliceSmall被传给main函数中,使得sliceBase申请的资源没法被立即释放,从而增大程序对空间的不必要消耗。

解决方案:
在确定要选择的大小,可以提前创建好对应的空间,使用深拷贝的策略来避免这种空间共享问题。

func main() {
	// 场景1:子切片导致的内存泄漏
	s = solveScene1()
	fmt.Println(len(s), cap(s))  // 2 2
}

func solveScene1() []int {
	sliceBase := make([]int, 1000)
	sliceSmall := make([]int, 2)
	copy(sliceSmall, sliceBase[0:2])
	return sliceSmall
}

子字符串导致的内存泄漏

golang中,子字符串会与原始字符串进行底层内存的共享,这样的设计虽然可以很好的节省CPU和内存的资源,但是使用不当就会造成内存泄漏的问题。

func main() {
	// 场景2:子字符串导致的内存泄漏
	str := scene2()
	fmt.Println(str)
}

func scene2() string {
	str := "123456789"
	res := str[:3]
	return res
}

上述代码中,虽然将str[:3]这个字串作为返回结果,但是它实际上还会影响str这个包含9个字节的字符串无法被回收。


即使scene2str已经不再指向字符串,但是mainstr指向了共享的空间地址,使得一些空间无法被GC回收,同时也无法再被系统使用了。这种操作积攒多了,就会影响golang的内存性能。

解决方案1:
通过标准 Go 编译器进行优化,以避免不必要的重复, 使用一字节内存的小额外成本,避免不确定性的内存损失。

func solve1Scene2() string {
	str := "123456789"
	res := (" " + str[:3])[1:]
	return res
}

编译器优化可能会使这个方法变得无效

解决方案2:
string 转换到 []byte 转换到 string

func solve2Scene2() string {
	str := "123456789"
	res := string([]byte(str[:3]))
	return res
}

通过转换byte切片,在转换到string的额外步骤,构建一个具有新空间的字符串,从而避免了共享内存带来的内存泄漏。

解决方案3:
学过Java的同学可能会比较亲切下面这种方式,通过strings.Builder构建一个新的字符串,有点类似与方案2,但速度和内存占用相对更好。

func solve3Scene2() string {
	str := "123456789"

	// 使用strings.Builder
	var b strings.Builder
	b.Grow(3)
	b.WriteString(str[:3])

	res := b.String()
	return res
}

这种方案虽然可以很好的解决上述问题,但是它的代码量实在不敢恭维。因此方案4,由strings包封装的Repeat方法,可能是最佳选择了
解决方案4:

func solve4Scene2() string {
	str := "123456789"
	
	res := strings.Repeat(str[:3], 1)
	return res
}

代码量只需一行,基本无痛。
所以以后大家如果要从一个大字符串取其中相对少量的子字符串可以根据自己程序需要的情况,优先使用上面这些方法来避免临时性内存泄漏。

子切片未重置指针索引

这种情况和子切片共享内存空间两者产生的机制类似,但是不同点在于这种情况会导致更严重的内存泄漏问题。

type Node struct {
	Next *Node
	Val  int
}



func scene3() []*Node {
	s := []*Node{new(Node), new(Node), new(Node), new(Node)}
	//  s[1:3:3]  low:high:max
	res := s[1:3:3]
	return res
}

假设这个指针节点指向是一些链表,当返回的子切片共享空间占用整个大切片时,将会导致切片中的其余节点在程序的后期,即使不使用这些节点,也不会回收到这些指针节点,直到子切片被回收。

上述代码中s[1:3:3]是切片的另一中取子切片的方法,s[low:high:max] 。使用 s[low:high:max] 语法时,可以同时指定切片的长度和容量。切片的长度由 high - low 决定,而容量则由 max - low 决定。这种语法允许限制切片的最大容量,从而避免切片意外增长到不期望的大小。

这种子切片的共享在go101给出的解决方案如下
解决方案1:

func solve1Scene3() []*Node {
	s := []*Node{new(Node), new(Node), new(Node), new(Node)}
	
	s[0], s[3] = nil, nil
	res := s[1:3:3]
	return res
}

将指针置为nil,但是感觉不是太理解,这样的操作会把程序变得复杂化,我觉得按照自己的理解,应该使用类似前面子切片的操作解决最好

解决方案2:

func solve2Scene3() []*Node {
	s := []*Node{new(Node), new(Node), new(Node), new(Node)}

	s[0], s[3] = nil, nil
	res := make([]*Node, 2)
	copy(res, s[1:3:3])
	return res
}

挂起的 goroutines 导致的内存泄漏

Goroutine内存泄漏通常发生在协程启动后未能正确退出或被适当清理,导致其持续占用内存。以下是几个常见的例子:

死循环导致的内存泄漏

当一个goroutine陷入死循环,且没有任何退出条件时,就会导致内存泄漏。

package main

import (
    "time"
)

func leakyGoroutine() {
    for {
        // 模拟一些工作
        time.Sleep(1 * time.Second)
    }
}

func main() {
    for i := 0; i < 10; i++ {
        go leakyGoroutine()
    }
    // 主程序运行10秒后退出
    time.Sleep(10 * time.Second)
}

此时for循环设计为死循环,开启的leakyGoroutine将会因为死循环而永远无法停止

阻塞的通道读取

如果一个goroutine正在等待从通道中读取数据,而这个通道永远不会发送数据,goroutine就会被永远阻塞,导致内存泄漏。

package main

func leakyGoroutine(ch chan int) {
    <-ch // 永远阻塞
}

func main() {
    ch := make(chan int)
    for i := 0; i < 10; i++ {
        go leakyGoroutine(ch)
    }

    // 主程序运行10秒后退出
    time.Sleep(10 * time.Second)
}

未关闭的通道

如果一个goroutine正在从一个通道中读取数据,并且该通道在发送数据后未能正确关闭,这可能导致读取操作永远不会结束,进而导致内存泄漏。

package main

import (
    "fmt"
    "time"
)

func leakyGoroutine(ch chan int) {
    for range ch {
        // 处理接收到的数据
    }
    fmt.Println("Goroutine exited")
}

func main() {
    ch := make(chan int)
    for i := 0; i < 10; i++ {
        go leakyGoroutine(ch)
    }

    // 向通道发送数据但未关闭
    for i := 0; i < 5; i++ {
        ch <- i
    }

    // 主程序运行10秒后退出
    time.Sleep(10 * time.Second)
}

leakyGoroutine中的for range循环将永远等待新的数据,因为通道没有关闭,goroutine无法退出。

总结

以上就是今天探讨的内容,本文详细介绍了Golang中内存泄漏的概念及其两种主要类型:暂时性内存泄漏和永久性内存泄漏,并通过多个实例说明了这些内存泄漏的常见原因及解决方法。虽然Golang拥有GC机制,但作为开发者仍需保持良好的编码习惯,避免内存泄漏对程序性能的影响。
在下一篇中,我们将详细的介绍pprof内存分析工具,是如何排查一些常见的内存泄漏问题。

本文是经过个人查阅相关资料后理解的提炼,可能存在理论上理解偏差的问题,如果您在阅读过程中发现任何问题或有任何疑问,请不吝指出,我将非常感激并乐意与您讨论。谢谢您的阅读!

标签:Node,泄漏,pprof,res,切片,内存,func,Go,半篇
From: https://blog.csdn.net/qq_45171525/article/details/140722501

相关文章

  • Vite2.0+ElementPlus+Koa2+Mongo全栈开发通用后台系统Vue3
    Vite2.0+ElementPlus+Koa2+Mongo全栈开发通用后台系统Vue3前言当前基于NodeJs框架的全栈工程实践非常之火,作为一个很长时间未接触代码的前程序猿。一直有点手痒痒,想尝试一下这种全新的编程体验,于是就重新开始了填坑的不归之路。这一套框架是基于现在的前后台分离的指导原则来......
  • goleveldb的原理简述(基于golang的goleveldb库)
    简介goleveldb是基于LSM-Tree实现的针对处理写多读少场景的解决方案,通常用于构建写多读少的存储引擎整体架构图如下基于用户接口层简述原理吧Get,按key查询数据,首先区内存中的数据,如果内存中没有则依次从硬盘中的ldb文件中取得数据。Put,按key更新数据,首先写内存数据,如......
  • https 服务示例 go-gin框架 支持ssl/tls,
    本文为演示采用自签名证书一.生成证书通过openssl工具生成证书1.1安装opensslmacos通过brew安装brewinstallopenssl1.2生成跟证书私钥opensslgenrsa-outca.key40961.3准备配置文件vimca.conf内容如下   [req]   default_bits      =4096   distin......
  • 基于django+vue药店销售管理系统【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着医疗行业的快速发展和人们对健康意识的日益增强,药店作为药品流通的重要环节,其管理效率与服务质量直接关系到民众的健康保障与用药安全......
  • 基于django+vue药店管理系统【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着医药行业的快速发展与消费者健康意识的日益增强,药店作为药品销售与服务的重要窗口,其管理效率与服务质量直接影响到顾客的满意度及企业......
  • 基于django+vue药店管理系统【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着医疗行业的快速发展和人们对健康需求的日益增长,药店作为药品销售与服务的重要窗口,其管理效率和服务质量直接影响到消费者的满意度及行......
  • go——GC垃圾回收机制
    堆和栈的区别存储方式栈是线性数据结构、采用先进后出的方式存储数据。栈通常用于存储局部变量、函数的参数,栈上的存储空间是有限且固定的,由编译器和操作系统控制。堆是树型数据结构,用于动态分配和管理内存,堆内存的大小根据需要动态调整,通常比栈大,用于存储复杂的数据结构......
  • Go语言中的交互式CLI开发:survey库简介
    在构建命令行工具时,良好的用户交互体验至关重要。尤其是在需要与用户进行复杂输入的场景下,传统的命令行参数和标志可能显得笨拙。github.com/AlecAivazis/survey/v2是一个为Go语言设计的库,专门用于构建交互式的命令行界面。它提供了多种用户输入方式,让你的CLI工具变得更加易......
  • 计算机毕业设计django+vue高校二手书买卖系统的设计与实现【开题+论文+程序】
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着高等教育的普及和高校学生数量的增加,二手书市场在高校内逐渐兴起。然而,传统的二手书交易方式往往存在信息不对称、交易效率低下等问题......
  • 使用 niljson 处理 Go 语言中 JSON 的空值类型
    使用niljson处理Go语言中JSON的空值类型原创 源自开发者 源自开发者  2024年09月03日11:43 广东 听全文源自开发者专注于提供关于Go语言的实用教程、案例分析、最新趋势,以及云原生技术的深度解析和实践经验分享。321篇原创内容公众号在使用G......