Why this matters
A leak is memory that a program has taken and never given back. One leaked block of 40 bytes does nothing noticeable. A server that leaks 40 bytes for every request, and runs for a week, can use gigabytes. The program does not crash at first. It slows down, the computer starts to swap memory to disk, and then something fails with an out of memory error. Such bugs are hard to find by reading the code, because each single allocation looks correct.
The previous lessons showed how to avoid leaks by matching every new with a delete, and by using smart pointers that do the delete for you. Real code still leaks, because a function can return early, a pointer can be overwritten, or a container can keep growing. This lesson shows how to find those leaks with a tool, and how the same kind of problem looks in Java, where there is no delete at all.
Two words are used throughout:
- A heap block is one piece of memory that the program asked for with
newin C++, or withnewfor an object in Java. - Reachable means that some pointer or reference in the running program can still lead to the block. The program could use the block, even if it does not.
What counts as a leak
A leak has two common shapes. The difference matters, because the tools treat them differently.
The first shape is lost memory. The program no longer has any pointer that holds the address of the block. The block cannot be reached, and nothing can ever free it. This is the classic leak, such as an early return that skips the delete, or a pointer that is overwritten before it is freed.
The second shape is kept memory. The block is still reachable from a global variable or a long-lived list. The program could in theory use it, and it is still held. The memory keeps growing because the list keeps growing. In C++ this is often called a "still reachable" block, and most leak checkers do not report it as a leak. In Java, it is the usual way a leak happens, because the garbage collector never frees an object that is reachable.
The distinction is the key to this lesson. A tool can prove that lost memory is lost. It cannot easily decide whether kept memory is needed, so it usually stays silent about it. You need to know the difference to read the reports.
Tools in C++: the address sanitizer
A sanitizer is a feature built into the compiler that checks the program while it runs and stops it when it finds a memory error. The most common one is the address sanitizer, which is turned on with the compiler flag -fsanitize=address. On Linux, this sanitizer also includes a leak check, called LeakSanitizer. The leak check runs when the program ends, and it reports every heap block that cannot be reached at that point.
Two other flags matter for this lesson:
-gadds debug information to the program, so each report can name the source file and line number.-fsanitize=leakturns on only the leak check, without the other memory checks.
The sanitizer slows the program down and uses more memory. It is meant for testing and debugging, not for the final version that users run.
Worked example 1: an early return
The first program has a function that allocates an array, and then returns early on one path without freeing it. This is a common real bug. The early return is added later, after the delete[] line was already written.
Compile and run it with the address sanitizer and debug information.
g++ -std=c++20 -Wall -g -fsanitize=address -o leak1 leak1.cpp
./leak1Output from one run:
sum: 60
negative: -1
=================================================================
==752198==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12 byte(s) in 1 object(s) allocated from:
#0 0x781a3372c8cf in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:111
#1 0x6545a52c44e2 in sum_scaled(std::vector<int, std::allocator<int> > const&, int) /home/chiranjeet/dev/anisha/scratch-offtmp/claude-ptr/leak1.cpp:7
#2 0x6545a52c488a in main /home/chiranjeet/dev/anisha/scratch-offtmp/claude-ptr/leak1.cpp:22
#3 0x781a32e2a600 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a600) (BuildId: 066527e430a32768d82741e00b81eebb1a872294)
#4 0x781a32e2a717 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a717) (BuildId: 066527e430a32768d82741e00b81eebb1a872294)
#5 0x6545a52c43e4 in _start (/home/chiranjeet/dev/anisha/scratch-offtmp/claude-ptr/leak1+0x23e4) (BuildId: d7ef380ac9413696234dcfba6b649d75d43deb04)
SUMMARY: AddressSanitizer: 12 byte(s) leaked in 1 allocation(s).The program's own output comes first. Then the report starts with a line of equal signs, followed by the name of the tool and the process number. The process number, the addresses and the build ID change on every run, so your numbers will differ.
Read the report from the top. The line Direct leak of 12 byte(s) in 1 object(s) says that 12 bytes were leaked in one block. Twelve bytes is three int values of 4 bytes each, which matches the vector of three values. The lines below it are the stack trace, which is the list of functions that were running when the block was allocated. Frame #0 is inside the C++ library's operator new[], which is the function that new int[...] calls. Frame #1 is the first line of your own code that asked for the block: leak1.cpp:7, which is the new line. Frame #2 shows who called that function. Here it is line 22 of main, the call with a factor of -1.
The report points to the place the memory was allocated, not to the place where it should have been freed. To fix the leak, find the path that returns without calling delete[]. In this program the fix is to use a vector instead of a raw array, because a vector frees itself on every path. The fixed program is shown next.
Output:
sum: 60
negative: -1The sanitizer prints nothing when there is no leak, and the program exits with code 0. A useful habit is to check the exit code with echo $? after each run. When the leak check finds a leak, the program exits with code 1 under the address sanitizer, and with code 23 when it is built with -fsanitize=leak alone.
Worked example 2: a pointer that is overwritten
The second kind of lost memory happens when a pointer is given a new address before the old block is freed. The old block still exists, but no pointer holds its address.
Output from the same kind of build (g++ -std=c++20 -Wall -g -fsanitize=address):
first: 5
second: 6
=================================================================
==752239==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 0x74080a92c73f in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:109
#1 0x59b6d318d29e in main /home/chiranjeet/dev/anisha/scratch-offtmp/claude-ptr/leak_lost.cpp:5
#2 0x74080a02a600 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a600) (BuildId: 066527e430a32768d82741e00b81eebb1a872294)
#3 0x74080a02a717 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a717) (BuildId: 066527e430a32768d82741e00b81eebb1a872294)
#4 0x59b6d318d1c4 in _start (/home/chiranjeet/dev/anisha/scratch-offtmp/claude-ptr/leak_lost+0x11c4) (BuildId: fb4ee52e02b34e450960da2bccce1d3b7ee6ed23)
SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).The report points at line 5, where the first new happened. The leaked block is the first int, which holds 5. Line 7 is where the pointer was overwritten. The sanitizer cannot say which line caused the loss, only where the block was allocated. Reading the code between the allocation and the report tells you the rest. The fix is to free the first block before the pointer changes, or to use a unique_ptr, which frees its block when it is overwritten.
Worked example 3: memory that is kept, not lost
The third example stores pointers in a global list, which lives for the whole run of the program. The list never frees its entries. Here is the program, with a comment that explains what it does.
Output:
recorded 5 valuesThe sanitizer reports nothing, and the exit code is 0. The five ints are still leaking in the sense that the program never frees them. But the global history still holds the address of the list, and the list still holds the addresses of the ints. The leak checker sees that a path from a global variable reaches every block, so it treats them as reachable. It does not know that the program will never read them again.
This is the reason a clean report does not mean there is no growth problem. A structure that only grows, such as a global list of results, a cache without a limit or a list of listeners that never unsubscribe, can use more and more memory without any leak report. For that kind of problem you need to read the code and ask when each entry is removed. The same thing is true in Java, which is covered next.
Java: objects kept alive by a static list
Java has no delete and no leak check built into the compiler. The garbage collector frees an object when no reference reaches it. A Java leak is therefore always a kept object: an object that is still reachable from a long-lived place, such as a static field, and that the program no longer needs. A static field belongs to the class itself, not to one object, and it lives as long as the program runs.
The next program simulates a server that handles requests. Each request makes a 1 MB response, and the response is added to a static list that is never cleared. The list is a leak in the sense of this lesson, because every entry is kept but never used again.
import java.util.ArrayList;
import java.util.List;
public class StaticLeak {
static final List<byte[]> CACHE = new ArrayList<>();
static void handleRequest(int id) {
byte[] response = new byte[1_000_000];
CACHE.add(response);
}
public static void main(String[] args) {
for (int id = 1; id <= 1000; id++) {
handleRequest(id);
if (id % 10 == 0) {
System.out.println("requests: " + id + ", cached: " + CACHE.size());
}
}
System.out.println("done");
}
}Compile it, then run it with a maximum heap of 64 MB. The -Xmx64m option sets the largest amount of memory the JVM may use for objects.
javac StaticLeak.java
java -Xmx64m StaticLeakOutput from one run:
requests: 10, cached: 10
requests: 20, cached: 20
requests: 30, cached: 30
requests: 40, cached: 40
requests: 50, cached: 50
requests: 60, cached: 60
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"The program stops after about 60 requests, long before the 1000 it was asked to handle. The exit code is 1. The first run used 64 MB, and the program needs about 1 MB for each request that is kept, so the heap fills up. The error message is odd. The words thrown from the UncaughtExceptionHandler mean that a second out of memory error happened while the JVM was trying to report the first one. The heap was most likely still full at that point, so the report could not be written in the usual way. The important part is the OutOfMemoryError, which tells you the heap is full.
The fix is to remove entries that are no longer needed. The next version keeps at most eight responses. A LinkedHashMap with a method that drops the oldest entry is one simple way to do this.
import java.util.LinkedHashMap;
import java.util.Map;
public class StaticLeakFixed {
// Keeps at most 8 entries. The eldest entry is dropped when a ninth is added.
static final Map<Integer, byte[]> CACHE = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, byte[]> eldest) {
return size() > 8;
}
};
static void handleRequest(int id) {
byte[] response = new byte[1_000_000];
CACHE.put(id, response);
}
public static void main(String[] args) {
for (int id = 1; id <= 1000; id++) {
handleRequest(id);
if (id % 100 == 0) {
System.out.println("requests: " + id + ", cached: " + CACHE.size());
}
}
System.out.println("done");
}
}Output with the same heap limit:
requests: 100, cached: 8
requests: 200, cached: 8
requests: 300, cached: 8
requests: 400, cached: 8
requests: 500, cached: 8
requests: 600, cached: 8
requests: 700, cached: 8
requests: 800, cached: 8
requests: 900, cached: 8
requests: 1000, cached: 8
doneThe program now handles all 1000 requests with the same 64 MB limit, because only eight responses are kept at any time.
Finding the kept objects in Java
When you do not know what is filling the heap, two tools help. The first is the GC log, which you have seen in the weak reference lesson. The second is a heap dump, which is a file that holds a copy of every object in the heap at one moment. The JVM can write one automatically when it runs out of memory.
java -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=static_leak.hprof StaticLeakOutput from the run used to check this lesson (the byte count and time differ on each run):
java.lang.OutOfMemoryError: Java heap space
Dumping heap to static_leak.hprof ...
Heap dump file created [68934953 bytes in 0.066 secs]
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"The file is large, about 69 MB here, because it holds every object. Tools such as Eclipse MAT open heap dumps and show which objects use the most memory, and which chain of references keeps each one alive. In this program, the chain would lead from the static field CACHE to the list and then to each byte array. That chain is the answer to the question "why is this still here?"
Common mistakes
Mistake 1: reading a report that has no line numbers
If you compile without -g, the sanitizer still prints the functions, but it cannot name the source line. The frame then looks like this:
#1 0x5dd4aa0904e2 in sum_scaled(std::vector<int, std::allocator<int> > const&, int) (/home/chiranjeet/dev/anisha/scratch-offtmp/claude-ptr/leak1_nog+0x24e2) (BuildId: f91859724de2348fd7ddba26b49caf15bcd60546)The function name is still useful, but add -g when you want to go straight to the line.
Mistake 2: the program output is missing before the report
If a program writes its results with cout and no endl, and then leaks, the results can be lost. The text waits in a buffer, which is a block of memory where output sits before it is sent to the terminal. The leak check ends the program without flushing that buffer. In a test with the output sent to a file or pipe, the result lines were not written at all. Ending each line with endl made the lines appear. Use endl, or call cout.flush(), when you want the output to be complete before a report. A missing result line can make a correct run look broken.
Mistake 3: believing that a clean report means no problem
The third worked example shows a program that the leak check accepts, even though its memory grows forever. A clean report means no unreachable memory was found. It does not mean the program uses memory well. Read the code for every structure that gets bigger, and ask when its entries are removed.
Mistake 4: keeping data in a static field in Java
A static field lives as long as the program runs. Any list, map or cache stored there keeps its objects alive. The usual symptom is an OutOfMemoryError that appears only after a long run. Check every static collection and make sure something removes old entries.
Mistake 5: running the sanitizer build as the real program
The sanitizer is useful in tests, but it makes the program slower and uses more memory. Build the normal program for the final version, and use the sanitizer build only while you test.
Practice
Exercise 1: find the leak
The function below leaks memory on one path. Name the path, the line that allocates the block, and the change that fixes it.
#include <iostream>
using namespace std;
int total(int n) {
int* data = new int[10];
if (n == 0) {
return 0;
}
int sum = 0;
for (int i = 0; i < 10; ++i) { data[i] = i; sum += data[i]; }
delete[] data;
return sum;
}
int main() {
cout << total(0) << endl;
cout << total(3) << endl;
return 0;
}Show the answer
The path is the early return when n is 0. The block is allocated on line 5 with new int[10]. The return on line 7 leaves the function before delete[] data runs. The sanitizer reports a leak of 40 bytes, which is ten ints, allocated at line 5 and called from main at line 16, the call with 0. The fix is to use vector<int> data(10);, which frees itself on every return, or to move the delete[] so that it runs before every return.
Exercise 2: a static pointer
The program below stores a pointer in a global variable and never frees it. Will the sanitizer report a leak?
#include <iostream>
using namespace std;
static int* table = new int[100];
int main() {
for (int i = 0; i < 100; ++i) table[i] = i;
cout << "table[99] = " << table[99] << endl;
return 0;
}Show the answer
No. The global variable table still holds the address of the array when the program ends, so the array is reachable and the leak check does not report it. The output is table[99] = 99, and the exit code is 0. The array is still never freed, which is the kept kind of memory, and a clean report does not show that.
Exercise 3: read the report
The report below comes from a program that allocates one int, then overwrites the pointer. Which line of the code should you look at first, and what should you check there?
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#1 0x59b6d318d29e in main /home/chiranjeet/dev/anisha/scratch-offtmp/claude-ptr/leak_lost.cpp:5Show the answer
Look at line 5, which is where the block was allocated. Then find the line that changes the pointer, which in this program is line 7, p = new int(6);. Check that the old value is freed before the pointer is overwritten. In this case, the fix is delete p; before line 7, or a unique_ptr.
Exercise 4: find the kept objects
A Java program stores every incoming message in a static List<String> called LOG and never removes any message. The program runs for a week and then fails with an out of memory error. Name two things to check, and say what kind of fix stops the growth.
Show the answer
Check which code adds to LOG, and check whether anything removes entries from it. Also check how large each message is, because a list of big messages uses more memory than a list of short ones. The fix is to remove entries when they are no longer needed, for example when a message has been sent, or to keep only the newest entries with a limit, such as the LinkedHashMap pattern shown earlier.
Exercise 5: predict the limit
The StaticLeak program keeps one 1 MB array for each request. If you run it with -Xmx256m instead of -Xmx64m, how many requests does it handle before the error, roughly?
Show the answer
About 250 requests. Each request keeps 1 MB, and the heap limit is 256 MB. The run used for this lesson stopped after requests: 250, cached: 250, and printed the same out of memory error. The exact number can change with the memory the JVM uses for its own work, so the answer is an estimate.
Summary
- A leak is memory that the program holds and never frees. Lost memory has no pointer left to it. Kept memory is still reachable from a global or a long-lived list.
- Build with
-g -fsanitize=addressto get the leak check and line numbers. The report names the line where each leaked block was allocated. - A clean report does not mean the memory use is sound. The leak check cannot find kept memory that grows without limit.
- In Java, a leak is an object that a static field or another long-lived reference still reaches. Limit the size of such collections, and remove old entries.
- Use the GC log and a heap dump to see which objects fill the heap, and which chain of references keeps them alive.