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.
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:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With