Golang解析GIF图片后内存无法释放的原因是什么?

Golang解析GIF图片后内存无法释放的原因是什么? 我尝试使用 image/gif 库,但这导致我的项目内存占用过高且无法释放,最终我发现这是该库的一个问题?

你可以看看下面这段简单的代码,它占用了过多的物理内存,其中大部分是共享内存,即使是手动执行垃圾回收也无法释放。我使用了一个 Prometheus 工具来显示实际的物理内存占用,这非常不正常。

go version go1.20.11 linux/amd64 22.04.3-Ubuntu

package main

import (
	"bytes"
	"fmt"
	"github.com/prometheus/procfs"
	"image/gif"
	"ntest/res"
	"os"
	"runtime"
	"time"
)

func init() {
	gif.DecodeAll(bytes.NewReader(res.GIF))
}

func main() {
	proc, _ := procfs.NewProc(os.Getpid())
	for {
		time.Sleep(1 * time.Second)
		runtime.GC()
		stat, _ := proc.Stat()
		fmt.Println(float64(stat.ResidentMemory()) / 1024 / 1024)
	}
}

更多关于Golang解析GIF图片后内存无法释放的原因是什么?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

有趣,但我仍然不明白为什么随着我解析更多的图像帧,这个共享内存会变得越来越大。而且当我启动第二个进程并重复此操作时,共享内存也是从零开始并逐渐增大,这看起来像是进程独占了共享内存,使其变成了无法释放的专有内存。

更多关于Golang解析GIF图片后内存无法释放的原因是什么?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


无论是主函数还是初始化函数,这种情况都会发生。起初我修改了源代码,以流式方式读取图片帧,然后发现随着我持续读取图片帧,共享内存会逐渐变大。而gif库只是在for循环中读取一次,结果也是一样的。

我无法重现这个问题,因为我没有 ntest/res,但这里有几个问题:

  • res.GIF 的大小是多少?
  • 如果将解码操作移到 main() 函数中会怎样?
  • 进程使用了多少内存,而系统又有多少可用内存?

另外,也许你在探索过程中会发现 《Go 垃圾回收器指南》 会有所帮助。

祝好

这个结果。根据我的测试,这个gif只有20MB,它无关紧要,问题并没有在我的macOS上出现,对象内存被释放了。而在我的Ubuntu上,对象内存也被释放了,但共享内存却没有被释放(在macOS上这部分也被释放了)。这让我感到困惑,显然,示例代码非常简单,对象也被垃圾回收了,但实际的物理内存在Ubuntu和macOS上的表现却不一致。

有趣的是你提到了macOS和Linux之间的差异。这可能实际上是“按设计工作”的。我找到了这个StackOverflow回答,可能与你的情况相关。

我想到这一点是因为苹果如何宣传8GB就足够了。laughing 他们可能只是为了效率而将一些POSIX标准抛之脑后。

这是一个已知的 image/gif 包的内存管理问题。当解码 GIF 时,库会缓存全局的调色板数据,这些缓存不会被垃圾回收器释放。问题主要出现在 decode 函数内部使用的全局 decodeOnly 结构体缓存。

具体来说,image/gif 包在解码时会将调色板数据缓存在全局的 decodeOnly 变量中,这个缓存的生命周期是程序运行期间,不会被 GC 回收。即使你不再使用解码后的 GIF 对象,这些缓存数据仍然会占用内存。

以下是演示问题的示例代码:

package main

import (
	"bytes"
	"image/gif"
	"runtime"
	"time"
)

func main() {
	// 模拟多次解码 GIF
	for i := 0; i < 100; i++ {
		data := make([]byte, 1024*1024) // 1MB 模拟 GIF 数据
		_, err := gif.DecodeAll(bytes.NewReader(data))
		if err != nil {
			// 忽略解码错误,重点在内存行为
		}
		
		// 强制 GC
		runtime.GC()
		time.Sleep(100 * time.Millisecond)
	}
	
	// 此时内存不会完全释放
	select {}
}

问题的根源在 image/gif 包的实现中。在 reader.go 文件中,有一个全局的 decodeOnly 变量:

var decodeOnly struct {
	sync.Mutex
	m map[string]*decoder
}

这个缓存用于存储已解码的 GIF 调色板信息,但缓存策略存在问题,会导致内存持续增长。

目前有两种解决方案:

  1. 使用替代库:使用第三方 GIF 解码库,如 github.com/disintegration/imaging
package main

import (
	"bytes"
	"fmt"
	"image"
	_ "image/gif"
	"io"
	"os"
)

func decodeGIFWithoutCache(r io.Reader) (image.Image, error) {
	// 使用通用的 image.Decode 而不是 gif.DecodeAll
	img, _, err := image.Decode(r)
	return img, err
}

func main() {
	data, _ := os.ReadFile("test.gif")
	img, err := decodeGIFWithoutCache(bytes.NewReader(data))
	if err != nil {
		panic(err)
	}
	fmt.Printf("Decoded image: %v\n", img.Bounds())
}
  1. 限制解码次数并重启进程:对于需要大量处理 GIF 的长期运行服务,可以考虑定期重启 worker 进程
package main

import (
	"bytes"
	"image/gif"
	"os"
)

// 在独立函数中解码,函数返回后局部变量可能被回收
func decodeGIFInIsolation(data []byte) (*gif.GIF, error) {
	return gif.DecodeAll(bytes.NewReader(data))
}

func main() {
	data, _ := os.ReadFile("large.gif")
	
	// 每次解码都在新的函数作用域中
	for i := 0; i < 10; i++ {
		g, err := decodeGIFInIsolation(data)
		if err != nil {
			panic(err)
		}
		_ = g // 使用解码后的 GIF
	}
}

这个问题在 Go 的 issue 跟踪器中已经被报告过(如 issue #50146)。虽然有一些缓解措施,但核心的缓存问题在标准库中仍然存在。对于内存敏感的应用,建议使用替代方案或等待官方修复。

回到顶部