To prevent memory leaks, the Key of the Map can be recycled when there are no other strong references

Author: Senior Mingming Ruyue, CSDN blog expert, senior Java engineer of Ant Group, author of “Performance Optimization Methodology”, “Unlocking Big Factory Thinking: Analysis of “Alibaba Java Development Manual”, “Re-learning Classics : “EffectiveJava” Exclusive Analysis “columnist.
Recommended popular articles:

  • “In the Age of Artificial Intelligence, Will Software Engineers Be Replaced? “
  • How to Write High-Quality Articles: From Strategy to Tactics
  • “My Technology Learning Methodology”

1. Problem description

Encountered a dilemma in development, you need to design a Map in a certain class (such as ValueHolder), where Key is another type (such as Source). Source has its own life cycle. Since ValueHolder has a longer life cycle, it should be recycled after the end of Source life cycle, but due to ValueHolder held by it cannot be recycled, resulting in a memory leak.

2. Background knowledge

2.1 Judgment of object survival

There are two common ways to judge whether a Java object can be recycled:

  • Reference counting method: Add a reference counter to the object. Whenever a reference points to it, the counter value increases by 1; whenever a reference becomes invalid, the counter value decreases by 1. An object whose counter value is 0 at any time is impossible to be used again, so this object is a recyclable object.
  • Reachability analysis is a method to determine whether a Java object can be recycled. Its basic idea is to start from a series of objects called GC Roots and search down the reference chain. If an object does not have any path connected to GC Roots, it means that the object is unreachable. Then this object It is a recyclable object.
    GC Roots usually include the following types of objects:
    • Objects referenced in the virtual machine stack
    • Objects referenced by class static properties in the method area
    • Objects referenced by constants in the method area
    • The object referenced by JNI (that is, the Native method in general) in the local method stack

Reachability analysis can effectively solve the problem of circular references. Even if two or more objects refer to each other, as long as they cannot be reached from GC Roots, they are all recyclable objects.
The Java virtual machine does not use reference counting to determine whether an object can be recycled, because this method cannot solve the problem of circular references. The Java virtual machine mainly uses reachability analysis for garbage collection.

2.2 Types of reference

  • Strong Reference (Strong Reference) is the most common common object reference. As long as there are strong references pointing to an object, the object will survive, and the garbage collector will not process the surviving object. Strong references can cause memory leaks.
  • Soft Reference (Soft Reference) is a relatively weakened reference that allows objects to be exempted from some garbage collection. When the system memory is sufficient, it will not be recycled; when the system insufficient memory, it will be recycled. Soft references are generally used in memory-sensitive programs, such as caches.
  • A weak reference is a weakened reference. For the object pointed to by a weak reference, as long as the garbage collection mechanism runs, no matter whether the memory space of the JVM is sufficient, will Reclaim the memory occupied by this object.
  • Phantom Reference (Phantom Reference) is a virtual reference, it does not determine the life cycle of the object. At any time, this object with only phantom references may be recycled. Therefore, phantom references are mainly used to track the recycling status of objects.

Therefore, we can use the knowledge point of weak reference to solve this problem.

3. Implementation ideas

3.1 Using WeakHashMap

WeakHashMap is a dynamic hash table based on weak references, which can realize “auto-cleaning” memory cache. When its key object is not referenced by other strong references, the garbage collector will reclaim it and the corresponding value object, thereby avoiding memory leaks or waste.
The usage scenarios of WeakHashMap are as follows:

  • Cache system: Use WeakHashMap as a secondary cache to store expired or infrequent data. When the memory is insufficient, the data can be released automatically.
  • Listener or callback function: Use WeakHashMap to prevent the monitored or callback object from being recycled due to the strong reference of the listener or callback function.
  • Thread local variables: Use WeakHashMap as a container for thread local variables. When the thread ends, the thread local variables can be automatically cleaned up.
  • Other scenarios that require an automatic cleanup mechanism.

This scenario is to automatically recycle when there are no other strong references to avoid memory leaks.

But WeakHashMap also has some disadvantages:

  • The behavior of WeakHashMap depends on when the garbage collector runs, which is unpredictable. Therefore, you cannot determine when an element in a WeakHashMap is removed.
  • WeakHashMap is not thread-safe, if multiple threads access or modify it at the same time, it may cause inconsistency or concurrency exceptions. Need to use synchronization mechanism to ensure thread safety.
  • WeakHashMap‘s iterator (Iterator) does not support fail-fast (fail-fast) mechanism, that is, if there are other The thread modifies the WeakHashMap, and the iterator will not throw a ConcurrentModificationException exception.
  • The performance of WeakHashMap may not be as good as HashMap because it requires extra work to handle weak references and garbage collection.

The advantage of adopting this solution is that there is no need to manually handle the release of Keys, but in multi-threaded scenarios, additional synchronization is required.

3.2 Using WeakReference

WeakReference is a weak reference that can be used to describe an object that does not have to exist. When the object it points to is not referenced by other strong references, the garbage collector will reclaim it.
Therefore, Key can be wrapped with WeakReference so that Source can be recycled when there are no other strong references.

Of course, WeakReference also has some disadvantages:

  • WeakReference cannot guarantee the survival time of the object. When the object is only referenced by WeakReference, it may be reclaimed by the garbage collector at any time, which may cause some unexpected situations or data loss.
  • WeakReference requires additional memory space and time to maintain reference queues and weak reference objects, which may affect program performance and efficiency.
  • WeakReference cannot prevent memory leaks. If the weak reference object itself is not cleaned up or released in time, it will still occupy memory space.
  • WeakReference cannot be used alone, it needs to cooperate with other strong references or soft references to implement functions such as caching or monitoring.

The advantage of adopting this solution is that it can be combined with the thread-safe Map, making it easier to achieve thread safety, but you need to clean it up at the right time.

4. Code implementation

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Source {<!-- -->

    private String id;
}

import lombok. Data;

import java.util.Map;
import java.util.WeakHashMap;

@Data
public class ValueHolder {<!-- -->

    private Map<Source, String> map = new HashMap<>(8);

    public void putValue(Source source, String value) {<!-- -->
        map. put(source, value);
    }

    public void print() {<!-- -->
        System.out.println(map);
    }
}

Test code:

package org.example.demo.weak;

import java.util.concurrent.TimeUnit;

public class WeakHashMapDemo {<!-- -->
    private static final ValueHolder valueHolder = new ValueHolder();

    public static void main(String[] args) throws InterruptedException {<!-- -->

        for (int i = 0; i < 100; i ++ ) {<!-- -->
            test("index" + i);
            if (i % 10 == 0) {<!-- -->
                System.gc();
                TimeUnit. MILLISECONDS. sleep(30);
               
                valueHolder. print();
            }
        }
    }

    private static void test(String id) {<!-- -->
        Source source = new Source(id);
        String value = "test";
        valueHolder. putValue(source, value);
    }
}

According to the accessibility analysis, valueHolder will be referenced by GCRoot (valueHolder) before the main method is executed, because Source is referenced by ValueHoder in Map is held, and cannot be released after the test is executed.
Then, the effect to be achieved in this article is to allow Source to be recycled after the test method is executed.

4.1 Using WeakHashMap

You can replace HashMap with WeakHashMap.

import lombok. Data;

import java.util.Map;
import java.util.WeakHashMap;

@Data
public class ValueHolder {<!-- -->

    private Map<Source, String> map = new WeakHashMap<>(8);

    public void putValue(Source source, String value) {<!-- -->
        map. put(source, value);
    }

    public void print() {<!-- -->
        System.out.println(map);
    }
}

If you want to ensure thread safety, you can use Collections.synchronizedMap for packaging.

import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;

@Data
public class ValueHolder {<!-- -->

    private Map<Source, String> map = Collections. synchronizedMap(new WeakHashMap<>(8));

    public void putValue(Source source, String value) {<!-- -->
        synchronized (map) {<!-- -->
            map. put(source, value);
        }
    }

    public void print() {<!-- -->
        synchronized (map) {<!-- -->
            System.out.println(map);
        }
    }
}

Test code:

package org.example.demo.weak;

import java.util.concurrent.TimeUnit;

public class WeakHashMapDemo {<!-- -->
    private static final ValueHolder valueHolder = new ValueHolder();

    public static void main(String[] args) throws InterruptedException {<!-- -->

        for (int i = 0; i < 100; i ++ ) {<!-- -->
            test("index" + i);
            if (i % 10 == 0) {<!-- -->
                System.gc();
                TimeUnit. MILLISECONDS. sleep(30);
               
                valueHolder. print();
            }
        }
    }

    private static void test(String id) {<!-- -->
        Source source = new Source(id);
        String value = "test";
        valueHolder. putValue(source, value);
    }
}

4.2 Using WeakReference

You can use WeakReference to encapsulate Key, so that Source will be released when there are no other strong references.

import lombok. Data;

import java.util.Map;
import java.util.WeakHashMap;
import lombok.Data;

import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

@Data
public class ValueHolder {<!-- -->

    private Map<WeakReference<Source>, String> map = new ConcurrentHashMap<>(8);

    public void putValue(Source source, String value) {<!-- -->
        map.put(new WeakReference<>(source), value);
    }

    public void print() {<!-- -->
        System.out.println("mapSize:" + map.size());
        for (Map.Entry<WeakReference<Source>, String> entry : map.entrySet()) {<!-- -->
            System.out.println("element:" + entry.getKey().get());
        }
    }
}

There is a problem, Source can be recycled, but WeakReference is not.

(1) You can design a clear method to clear the data corresponding to the recycled Source.

package org.example.demo.weak;

import lombok.Data;

import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Data
public class ValueHolder {<!-- -->

    private Map<WeakReference<Source>, String> map = new ConcurrentHashMap<>(8);

    public void putValue(Source source, String value) {<!-- -->
        map.put(new WeakReference<>(source), value);
    }

    public void print() {<!-- -->
        System.out.println("mapSize:" + map.size());
        clear();
         for (Map.Entry<WeakReference<Source>, String> entry : map.entrySet()) {<!-- -->
            System.out.println("element:" + entry.getKey().get());
        }
    }

    public void clear() {<!-- -->
        Iterator<Map.Entry<WeakReference<Source>, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {<!-- -->
            Map.Entry<WeakReference<Source>, String> entry = iterator.next();
            WeakReference<Source> key = entry. getKey();
            if (key.get() == null) {<!-- -->
                iterator. remove();
            }
        }
    }
}

(2) You can also pass in the queue when constructing WeakReferece, and the recovered Source will be automatically placed in the queue and cleaned up regularly.

import lombok. Data;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Data
public class ValueHolder {<!-- -->

    private Map<WeakReference<Source>, String> map = new ConcurrentHashMap<>(8);
    private ReferenceQueue<Source> queue = new ReferenceQueue<>();

    public void putValue(Source source, String value) {<!-- -->
        map.put(new WeakReference<>(source, queue), value);
    }

    public void print() {<!-- -->
        System.out.println("mapSize:" + map.size());
        clear();
        for (Map.Entry<WeakReference<Source>, String> entry : map.entrySet()) {<!-- -->
            System.out.println("element:" + entry.getKey().get());
        }
    }

    public void clear() {<!-- -->
        WeakReference<Source> ref;
        while ((ref = (WeakReference<Source>) queue. poll()) != null) {<!-- -->
            map. remove(ref);
        }
    }
}

Test code:

package org.example.demo.weak;

import java.util.concurrent.TimeUnit;

public class WeakHashMapDemo {<!-- -->
    private static final ValueHolder valueHolder = new ValueHolder();

    public static void main(String[] args) throws InterruptedException {<!-- -->

        for (int i = 0; i < 100; i ++ ) {<!-- -->
            test("index" + i);
            if (i % 10 == 0) {<!-- -->
                System.gc();
                TimeUnit. MILLISECONDS. sleep(30);
                valueHolder. clear();
                valueHolder. print();
            }
        }
    }

    private static void test(String id) {<!-- -->
        Source source = new Source(id);
        String value = "test";
        valueHolder. putValue(source, value);
    }
}

5. Summary

Although many people ridicule, “the interview makes the wheel, go in and screw the screw”, but when you are really facing complex problems, the knowledge points that are often asked in the interview are still very important. A solid professional foundation can help you quickly find ideas to solve problems.
In addition, there is more than one way to solve the problem. It is necessary to compare the pros and cons and analyze comprehensively to choose a more suitable solution.
In addition, now that the era of artificial intelligence has come, you can try to use AI to find solutions to problems, and even use AI to help me complete some basic codes.

Extended reading
(1) “What? You haven’t used Cursor yet? Intelligent AI code generation tool Cursor installation and usage introduction》
(2) “My Technology Learning Methodology”
(3) In the era of artificial intelligence, will software engineers be replaced? “