6
package main

import (
    "fmt"
    "time"
)

func main() {
    storage := []string{}

    for i := 0; i < 50000000; i++ {
        storage = append(storage, "string string string string string string string string string string string string")
    }

    fmt.Println("done allocating, emptying")

    storage = storage[:0]
    storage = nil

    for {
        time.Sleep(1 * time.Second)
    }
}

The code above will allocate about ~30mb of memory, and then won't release it. Why is that? How can I force go to release memory used by this slice? I sliced that slice and then nilled it.

The program I'm debugging is a simple HTTP input buffer: it appends all requests into large chunks, and sends these chunks over a channel to goroutine for processing. But problem is illustrated above - I can't get storage to release the memory and then eventually run out of memory.

Edit: as some people pointed out to similar question, no, it first doesn't work, second isn't what I'm asking for. The slice gets emptied, the memory does not.

Grzegorz Żur
  • 47,257
  • 14
  • 109
  • 105
Aleksandr Makov
  • 2,820
  • 3
  • 37
  • 62
  • Nope, this isn't what I'm looking for. I can get the slice emptied, but I need it to release the memory. – Aleksandr Makov Feb 05 '18 at 08:20
  • @Filmzy, thank you. I've updated my question. – Aleksandr Makov Feb 05 '18 at 08:36
  • What do you mean by "release the memory"? If you mean hand the memory back to the operating system that's usually not done. If you mean make the memory available to other allocations in your program, then that's done. – Art Feb 05 '18 at 08:59
  • I ran your program for like 5..10 minutes, and memory was released back to OS. When I inserted a `debug.FreeOSMemory()` call before the last `for` loop, memory again was released back to OS without waiting. So this is a duplicate of [Cannot free memory once occupied by bytes.Buffer](https://stackoverflow.com/questions/37382600/cannot-free-memory-once-occupied-by-bytes-buffer/37383604#37383604). – icza Feb 05 '18 at 09:48

2 Answers2

13

There are several things going on here.

The first one which is needed to be absorbed is that Go is a garbage-collected language; the actual algorithm of its GC is mostly irrelevant but one aspect of it is crucial to understand: it does not use reference counting, and hence there's no way to somehow make the GC immediately reclaim the memory of any given value whose storage is allocated on the heap. To recap it in more simple words, it's futile to do

s := make([]string, 10*100*100)
s = nil

as the second statement will indeed remove the sole reference to the slice's underlying memory but won't make the GC go and "mark" that memory as available for reuse.

This means two things:

  • You should know how the GC works. This explains how it works since v1.5 and up until now (v1.10 these days).
  • You should structure those of your algorythms which are memory-intensive in a way that reduces memory pressure.

The latter can be done in several ways:

  • Preallocate, when you have a sensible idea about how much to.

    In your example, you start with a slice of length 0, and then append to it a lot. Now, almost all library code which deals with growing memory buffers—the Go runtime included—deals with these allocations by 1) allocating twice the memory requested—hoping to prevent several future allocations, and 2) copies the "old" contents over, when it had to reallocate. This one is important: when reallocation happens, it means there's two memory regions now: the old one and the new one.

    If you can estimate that you may need to hold N elements on average, preallocate for them using make([]T, 0, N)— more info here and here. If you'll need to hold less than N elements, the tail of that buffer will be unused, and if you'll need to hold more than N, you'll need to reallocate, but on average, you won't need any reallocations.

  • Re-use your slice(s). Say, in your case, you could "reset" the slice by reslicing it to the zero length and then use it again for the next request. This is called "pooling", and in the case of mass-parallel access to such a pool, you could use sync.Pool to hold your buffers.

  • Limit the load on your system to make the GC be able to cope with the sustained load. A good overview of the two approaches to such limiting is this.

kostix
  • 51,517
  • 14
  • 93
  • 176
2

In the program you wrote, it makes no sense to release memory because no part of code is requesting it any more.

To make a valid case, you have to request a new memory and release it inside the loop. Then you will observe that the memory consumption will stabilize at some point.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Grzegorz Żur
  • 47,257
  • 14
  • 109
  • 105
  • Well, yes, It isn't actual program of course. This is part of actual code: `func(ctx *fasthttp.RequestCtx) { uris = append(uris, string(ctx.RequestURI()[:])) }` and then in go routine `go func() { for { time.Sleep(10 * time.Second) if len(uris) > 0 { metrics <- uris uris = uris[:0] } } }()` And under benchmarking tests I always run out of memory quite fast. – Aleksandr Makov Feb 05 '18 at 08:29
  • I've updated my question, to make it more reality-based. – Aleksandr Makov Feb 05 '18 at 08:36
  • 1
    @AleksandrMakov And now the answer makes no sense. Please add another question. – Grzegorz Żur Feb 05 '18 at 08:38