Golang中内存使用量是下载内容三倍的原因是什么

Golang中内存使用量是下载内容三倍的原因是什么 我通过请求/REST下载一些数据,结果导致计算机内存被占满。我下载了大约12GB的数据,我的计算机有32GB内存,但内存还是满了。我对代码进行了pprof性能分析,似乎除了我存储数据的变量外,没有其他变量在累积数据。这可能与容量有关,但我没有预先确定容量,因为通常我事先不知道数据的大小。主要代码(运行了数百万次)如下:

func downloadStats(stor *Fields, field string, dateFromMs int64, dateToMs int64) {
	var cas int64
	var dateLast int64 = dateFromMs

loop2:
	for {
		//! --- 获取统计数据
		statsData, err := req.Get("URLtoEndpoint" + field + "xy"+dateLast)
		defer statsData.Response().Body.Close()
		if err == nil {
			break
		}
		status := gjson.Get(statsData.String(), "status").String()
		queryCount := gjson.Get(statsData.String(), "count").Int()
		if status == "OK" && err == nil && queryCount == 0 {
			fmt.Printf("\n%5s   RETURNED_Q 0-counv\n", field)
			return
		}

		//! --- JSON 数组:使用 gjson 库
		results := gjson.Get(statsData.String(), "results")
		for _, r := range results.Array() {
			cas = r.Get("time").Int()
			if cas >= dateToMs { // 跳出无限循环
				break loop2
			}

			var cnd = []int{}
			for _, a := range r.Get("conditions").Array() {
				cnd = append(cnd, int(a.Int()))
			}
			//! --- 存储统计字段 !!!!! 这里是数据存储,实际情况下有三行类似的代码
			stor.Prop1 = append(stor.Prop1, Property1{Ev: "turbine1", Volt: r.Get("voltage").Float(), Rot: r.Get("rotation").Float(), Temp: r.Get("temperature").Float(), Pres: r.Get("presure").Float(), T: cas, C: cnd, Axis: int(r.Get("axNb").Int()), tSort: cas})
		}
		
		time.Sleep(250 * time.Millisecond)
	}
}

store.Prop1 = append(store.Prop1,…)

是收集数据的行,实际情况中有3行这样的代码,所以这可能与内存使用量是内容大小的三倍有关。在这种情况下,我如何将内存使用量保持在接近下载数据大小(12GB)的水平?另外,我想了解内存管理的行为,我是新手,任何解释都将不胜感激。感谢您的想法。


更多关于Golang中内存使用量是下载内容三倍的原因是什么的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

感谢您的回复。但变量“results”在每次迭代中都会被覆盖,所以我不明白这怎么会成为问题?

我进一步测试的是,我注释掉了 stor.Prop1_ = stor.Prop1),它实际上是存储数据的部分,然后计算机的RAM在处理过程中保持恒定且较低,这意味着正是这一部分累积了大量的存储,但为什么比数据本身多出2-3倍?!是的,这部分是以字符串形式存储的,所以可能存储占用1倍,字节占用2倍……有没有办法改进存储方式?

更多关于Golang中内存使用量是下载内容三倍的原因是什么的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你好 @Rok_Petka

试试这个,看看能有多大帮助:

			var cnd = []int{}
			for _, a := range r.Get("conditions").Array() {
				cnd = append(cnd, int(a.Int()))
			}

你已经知道这个切片的容量上限,可以预先分配它。 同样地,你也知道 result.Array() 的大小,但对于其中的每个元素,你都执行了以下操作:

			stor.Prop1 = append(stor.Prop1, Property1{Ev: "turbine1", Volt: r.Get("voltage").Float(), Rot: r.Get("rotation").Float(), Temp: r.Get("temperature").Float(), Pres: r.Get("presure").Float(), T: cas, C: cnd, Axis: int(r.Get("axNb").Int()), tSort: cas})

这同样是浪费的,因为存在切片重新分配的问题。我不认为这本身足以导致你需要三倍的内存。但是,如果你先为这两个切片预分配空间,你将需要更少的内存。

请尝试一下并发布结果,我真的很想知道这会导致内存使用量减少多少。

results := gjson.Get(statsData.String(), "results")

你好 @Rok_Petka

我认为你接收到的是原始数据(字节),然后它被转换为字符串,并且这个字符串值也被存储了:

// Result 表示从 Get() 返回的 JSON 值。
type Result struct {
    // Type 是 JSON 类型
    Type Type
    // Raw 是原始的 JSON
    Raw string
    // Str 是 JSON 字符串
    Str string
    // Num 是 JSON 数字
    Num float64
    // Index 是原始 JSON 中原始值的索引,零表示索引未知
    Index int
    // Indexes 是包含 '#' 查询字符的路径上所有匹配元素的索引。
    Indexes []int
}

可能是这个包存储了你接收数据大小的两倍。

然后你在结果数组上进行迭代,并为其创建了一个额外的“副本”,总共达到了原始大小的 3 倍:

for _, r := range results.Array() {
    cnd = append(cnd, int(a.Int()))
    stor.Prop1 = append(stor.Prop1, Property1

所以你的包分配了 2 倍(实际读取数据)的大小,此外你还分配了 1 倍(实际数据)的大小。这只是一个理论,不过可能值得测试一下。

根据你的代码描述,内存使用量是下载内容三倍的主要原因很可能是切片(slice)的扩容机制和内存碎片导致的。以下是具体分析和解决方案:

主要问题分析

1. 切片扩容机制

Go语言的切片在append时,如果容量不足会触发扩容。扩容策略通常是:

  • 容量小于1024时,每次翻倍
  • 容量大于等于1024时,每次增加25%
// 示例:展示切片扩容导致的内存浪费
package main

import "fmt"

func main() {
    var data []int
    var totalAllocated int
    
    for i := 0; i < 1000000; i++ {
        before := cap(data)
        data = append(data, i)
        after := cap(data)
        
        if after > before {
            totalAllocated += (after - before) * 8 // 假设int是8字节
        }
    }
    
    fmt.Printf("实际使用: %d bytes\n", len(data)*8)
    fmt.Printf("总分配: %d bytes\n", totalAllocated)
}

2. 你的代码中的具体问题

// 这三行代码都会各自独立扩容
stor.Prop1 = append(stor.Prop1, Property1{...})
stor.Prop2 = append(stor.Prop2, Property2{...})  // 假设有
stor.Prop3 = append(stor.Prop3, Property3{...})  // 假设有

每个切片都在独立增长,每个都可能分配比实际需要更多的内存。

解决方案

方案1:预分配切片容量(推荐)

func downloadStats(stor *Fields, field string, dateFromMs int64, dateToMs int64) {
    // 先获取总数据量
    totalCount := getTotalCount(field, dateFromMs, dateToMs)
    
    // 预分配切片容量
    if totalCount > 0 {
        stor.Prop1 = make([]Property1, 0, totalCount)
        stor.Prop2 = make([]Property2, 0, totalCount)  // 如果有
        stor.Prop3 = make([]Property3, 0, totalCount)  // 如果有
    }
    
    // ... 其余代码不变
}

方案2:分批处理数据

func downloadStats(stor *Fields, field string, dateFromMs int64, dateToMs int64) {
    const batchSize = 10000
    var batchBuffer1 []Property1
    var batchBuffer2 []Property2
    var batchBuffer3 []Property3
    
    // 预分配批次缓冲区
    batchBuffer1 = make([]Property1, 0, batchSize)
    batchBuffer2 = make([]Property2, 0, batchSize)
    batchBuffer3 = make([]Property3, 0, batchSize)
    
    for {
        // ... 获取数据
        
        for _, r := range results.Array() {
            // ... 处理数据
            
            // 添加到批次缓冲区
            batchBuffer1 = append(batchBuffer1, Property1{...})
            batchBuffer2 = append(batchBuffer2, Property2{...})
            batchBuffer3 = append(batchBuffer3, Property3{...})
            
            // 批次满时,合并到主切片
            if len(batchBuffer1) >= batchSize {
                stor.Prop1 = append(stor.Prop1, batchBuffer1...)
                stor.Prop2 = append(stor.Prop2, batchBuffer2...)
                stor.Prop3 = append(stor.Prop3, batchBuffer3...)
                
                // 清空批次缓冲区
                batchBuffer1 = batchBuffer1[:0]
                batchBuffer2 = batchBuffer2[:0]
                batchBuffer3 = batchBuffer3[:0]
            }
        }
        
        // 处理剩余批次数据
        if len(batchBuffer1) > 0 {
            stor.Prop1 = append(stor.Prop1, batchBuffer1...)
            stor.Prop2 = append(stor.Prop2, batchBuffer2...)
            stor.Prop3 = append(stor.Prop3, batchBuffer3...)
        }
    }
}

方案3:使用sync.Pool减少内存分配

var propertyPool = sync.Pool{
    New: func() interface{} {
        return make([]Property1, 0, 1000)
    },
}

func downloadStats(stor *Fields, field string, dateFromMs int64, dateToMs int64) {
    // 从池中获取切片
    tempSlice := propertyPool.Get().([]Property1)
    
    for {
        // ... 获取数据
        
        for _, r := range results.Array() {
            // 使用临时切片
            tempSlice = append(tempSlice, Property1{...})
        }
        
        // 定期将数据转移到主切片并重置临时切片
        if len(tempSlice) >= 10000 {
            stor.Prop1 = append(stor.Prop1, tempSlice...)
            tempSlice = tempSlice[:0]
        }
    }
    
    // 处理剩余数据
    if len(tempSlice) > 0 {
        stor.Prop1 = append(stor.Prop1, tempSlice...)
    }
    
    // 将切片放回池中
    propertyPool.Put(tempSlice[:0])
}

内存管理说明

Go语言的内存管理:

  1. 切片是引用类型,底层是数组
  2. append操作可能触发重新分配,导致旧数组成为垃圾等待GC回收
  3. GC有延迟,不会立即回收内存
  4. 内存碎片可能导致实际使用内存大于数据大小

验证内存使用

使用runtime包监控内存:

import "runtime"

func printMemoryUsage() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
    fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc))
    fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
    fmt.Printf("\tNumGC = %v\n", m.NumGC)
}

func bToMb(b uint64) uint64 {
    return b / 1024 / 1024
}

在你的循环中定期调用printMemoryUsage()来监控内存变化。

最有效的解决方案是方案1的预分配,如果无法预先知道数据总量,则使用方案2的分批处理。这样可以显著减少内存碎片和过度分配。

回到顶部