📜 ⬆️ ⬇️

Three types of memory leaks

Hello colleagues.

Our long search for timeless bestsellers on optimizing the code so far only gives the first results, but we are ready to please you, that literally just finished the translation of the legendary book of Ben Watson " Writing High Performance .NET Code ". In stores - approximately in April, watch for advertising.

And today we offer you to read a purely practical article on the most pressing types of RAM leaks, which Nelson Elhage wrote from Stripe .

So, you have a program, the execution of which is spent the further - the more time. It is probably not difficult for you to understand that this is a sure sign of a leak in the memory.
However, what exactly do we mean by “memory leak”? In my experience, obvious leaks in memory are divided into three main categories, each of which is characterized by a particular behavior, and for debugging each category, special tools and techniques are needed. In this article I want to describe all three classes and suggest how to properly recognize, with
which class you are dealing with and how to find a leak.
')
Type (1): unreachable fragment of memory allocated

This is a classic memory leak in C / C ++. Someone allocated memory using new or malloc , and never called free or delete to free up memory at the end of working with it.

 void leak_memory() { char *leaked = malloc(4096); use_a_buffer(leaked); /* ,   free() */ } 

How to determine that a leak belongs to this category


How to find such a leak


Type (2): Unplanned Long-Lived Memory Allocations

Such situations are not “leaks” in the classical sense of the word, since a link from somewhere to this area of ​​memory is still preserved, so in the end it can be released (if the program has time to get there without spending all of its memory).
Situations in this category can arise for many specific reasons. The most common are:


How to determine that a leak belongs to this category


How to find such a leak

Use profilers or heap dump tools that are in your environment. I know if there is a guppy in Python or a memory_profiler in Ruby, and I myself wrote ObjectSpace directly in Ruby.

Type (3): free, but unused or unusable memory

Characterizing this category is the most difficult, but it is the most important to understand and take into account.

This type of leakage occurs in the gray area, between memory, which is considered “free” from the point of view of the allocator inside the VM or runtime environment, and memory, which is “free” from the point of view of the operating system. The most common (but not the only) reason for this is heap fragmentation . Some distributors simply take and do not return memory to the operating system after it has been allocated.

A case of this kind can be seen on the example of a short program written in Python:

 import sys from guppy import hpy hp = hpy() def rss(): return 4096 * int(open('/proc/self/stat').read().split(' ')[23]) def gcsize(): return hp.heap().size rss0, gc0 = (rss(), gcsize()) buf = [bytearray(1024) for i in range(200*1024)] print("start rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) buf = buf[::2] print("end rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) 

We allocate 200,000 1-kb buffers, and then save each subsequent one. We deduce every second the state of memory from the point of view of the operating system and from the point of view of our own Python garbage collector.

I get something like this on my laptop:

start rss=232222720 gcsize=11667592
end rss=232222720 gcsize=5769520


We can make sure that Python actually freed up half of the buffers, because the gcsize level dropped almost half the peak value, but could not return the operating system a single byte of this memory. The freed memory remains available to the same Python process, but to no other process on this machine.

Such free but unused portions of memory can be both problematic and harmless. If a Python program acts this way, and then allocates a handful of 1kb fragments, this space is simply reused, and everything is fine.

But, if we did this during the initial setup, and later allocated memory by the minimum, or if all the fragments subsequently allocated were at 1.5kb and did not fit into these previously left buffers, then all the memory allocated in this way would always stand idle. would be wasted.

Problems of this kind are especially relevant in a specific environment, namely, in multiprocess server systems for working with such languages ​​as Ruby or Python.

Suppose we set up a system in which:


Once a minute such a “cetacean” request arrives, the processing of which we assign to one of 10 employees, say, in a random fashion: ~random . Ideally, at the time of processing this request, the employee should allocate 1GB of RAM, and after finishing work, return this memory to the operating system so that it can be used again later. In order to process requests unlimitedly by this principle, the server will need only 10 * 500MB + 1GB = 6GB RAM.

However, let's assume that due to fragmentation or for some other reason, the virtual machine can never return this memory to the operating system. That is, the amount of RAM that it requires from the OS is equal to the largest amount of memory that ever has to be allocated at a time. In such a case, when a particular employee serves such a resource-intensive request, the area occupied by such a process in memory will swell forever by a whole gigabyte.

When you start the server, you will see that the amount of memory used is 10 * 500MB = 5GB. As soon as the first large request arrives, the first worker will grab 1GB of memory, and then will not give it back. The total memory used will jump to 6GB. The following incoming requests may from time to time be dropped by the process that has previously processed the "whale", and in this case, the amount of memory used will not change. But sometimes such a large request will be given to another employee, which will cause the memory to expand by another 1GB, and so on until each worker has had time to process such a large request at least once. In this case, you will use these operations up to 10 * (500MB + 1GB) = 15GB of RAM, which is much more than the ideal 6GB! Moreover, if we consider how the server fleet is used over time, then you can see how the amount of memory used gradually grows from 5GB to 15GB, which will very much resemble a “real” leak.

How to determine that a leak belongs to this category


How to find such a leak

As already mentioned, this category is a bit more insidious than the previous ones, since the problem often arises, even when all the components work “as intended”. However, there are a number of good practices that can help mitigate or reduce the impact of such “virtual leaks”:

Source: https://habr.com/ru/post/432072/


All Articles