An article that explains the ThreadLocal memory leak problem in depth from the source code

1. What causes the memory leak?

threadLocal is to solve the problem that objects cannot be shared by multiple threads . The object instance is saved in the threadLocalMap owned by each thread through the threadLocal.set method, so that each thread uses its own object instance without affecting each other to achieve isolation. The role of the object thus solves the thread safety problem caused by the shared access of the object. If the synchronization mechanism and threadLocal are compared horizontally, the synchronization mechanism is to control the order in which threads access shared objects, and threadLocal is to allocate an object for each thread, and each use does not affect each other. For example, now there are 100 students who need to fill out a form but there is only one pen. The synchronization is equivalent to A using the pen and then giving it to B, and B to C after using it... The teacher controls it. The order in which the pen is used will prevent students from conflicting with each other. And threadLocal is equivalent to the teacher directly prepared 100 pens, so that each student can use his own, and there will be no conflicts among the students. Obviously, these are two different ideas. The synchronization mechanism uses "time for space". Since each thread can only be accessed by one thread at the same time, the overall response time increases, but the object only occupies one piece of memory, sacrificing Time efficiency is exchanged for space efficiency, that is, "time for space". On the other hand, threadLocal allocates an object to each thread. Naturally, the memory usage rate increases, and each thread uses its own. The overall time efficiency will increase a lot, sacrificing space efficiency in exchange for time efficiency, that is, "space for time" .

For more details about threadLocal and threadLocalMap, you can read this article , which gives very detailed knowledge of various aspects (many are also high-frequency test sites for interviews). The relationship between threadLocal, threadLocalMap, and entry is shown in the following figure:

threadLocal reference diagram

In the above figure, the solid line represents a strong reference, and the dotted line represents a weak reference. If the strong reference outside the threadLocal is set to null (threadLocalInstance=null), there is no reference link to the threadLocal instance. Obviously, in the gc (garbage) Recycling) is bound to be recycled, so the entry has a case where the key is null, and the value of the entry cannot be accessed through a key that is null. At the same time, there is such a reference chain: threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory, which results in that when the reachability analysis is performed during garbage collection, the value is reachable and will not be recycled. , but the value can never be accessed, so there is a memory leak . Of course, if the thread execution ends, threadLocal, threadRef will be disconnected, so threadLocal, threadLocalMap, entry will be recycled. However, in actual use, we will use the thread pool to maintain our threads. For example, when creating threads in Executors.newFixedThreadPool(), the threads will not end in order to reuse them, so threadLocal memory leaks are worthy of our attention. .

2. What improvements have been made?

In fact, Josh Bloch and Doug Lea have made some improvements to address the potential memory leak of threadLocal. There are corresponding processing in the set and get methods of threadLocal. For the description below, for the entry whose key is null, the source code is annotated as stale entry, which is literally translated as stale entry, which I call "dirty entry" here. For example, in the set method of ThreadLocalMap:

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
     }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

In this method, the dirty entry is processed as follows:

  1. If the current table[i]! =null, it means that the hash conflict needs to be searched backwards. If a dirty entry is encountered during the search process, it will be processed by replaceStaleEntry;
  2. If the current table[i]==null, it means that the new entry can be inserted directly, but after insertion, the cleanSomeSlots method will be called to detect and clear the dirty entry

2.1 cleanSomeSlots

The source code of this method is:

/* @param i a position known NOT to hold a stale entry. The
 * scan starts at the element after i.
 *
 * @param n scan control: {@code log2(n)} cells are scanned,
 * unless a stale entry is found, in which case
 * {@code log2(table.length)-1} additional cells are scanned.
 * When called from insertions, this parameter is the number
 * of elements, but when from replaceStaleEntry, it is the
 * table length. (Note: all this could be changed to be either
 * more or less aggressive by weighting n instead of just
 * using straight log n. But this version is simple, fast, and
 * seems to work well.)
 *
 * @return true if any stale entries have been removed.
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

Input parameters:

  1. i means: the position i of the entry is inserted, obviously in the above case 2 (table[i]==null), the position i is obviously not a dirty entry after the entry is inserted;

  2. parameter n

    2.1. Use of n

    Mainly used for scan control (scan control), from the while statement that the condition is judged by n is used to control the number of scan passes (cycle times) . During the scanning process, if no dirty entry is encountered, the entire scanning process continues log2(n) times. The log2(n) is obtained because n >>>= 1each shift of n to the right is equivalent to dividing n by 2. If a dirty entry is encountered during the scanning process, n is the length of the current hash table ( n=len), and then log2(n) times are scanned. Note that the increase in n at this time is nothing more than an increase in the number of loops to search backward through nextIndex The scope is expanded, the schematic diagram is as follows

cleanSomeSlots schematic.png

According to the initial value of n, the search range is the black line. When a dirty entry is encountered, n becomes the length of the hash array (the value of n increases), and the search range log2(n) increases, indicated by the red line. If no dirty entry is encountered during the entire search process, the search ends. This method is mainly used to balance time efficiency.

2.2. The value of n

If it is called after the set method inserts a new entry (case 2 above), n bits are the size of the currently inserted entries; if it is called in the replaceSateleEntry method, n is the length len of the hash table.

2.2 expungeStaleEntry

If you can understand the input parameters, then the cleanSomeSlots method search is basically cleared, but you still need to master the expungeStaleEntry method. When you encounter a dirty entry during the search process, you will call this method to clean up the dirty entry. The source code is:

/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

	//清除当前脏entry
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
	//2.往后环形继续查找,直到遇到table[i]==null时结束
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
		//3. 如果在向后搜索过程中再次遇到脏entry,同样将其清理掉
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
			//处理rehash的情况
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

Please see the comments for the logic of this method (steps 1, 2, and 3), which mainly do the following:

  1. Clean up the current dirty entry, that is, set its value reference to null, and also set table[staleSlot] to null. After the value is set to null, the value field becomes unreachable and will be recycled in the next gc. At the same time, after table[staleSlot] is null, it is convenient to store new entries;
  2. Continue searching backward from the current staleSlot position (nextIndex) until it exits when the hash bucket (tab[i]) is null;
  3. If dirty entries are encountered again during the search process, continue to clear them.

That is to say, this method, after cleaning up the current dirty entry, does not have time to continue searching backwards. If the dirty entry is encountered again, it will continue to clean up until the hash bucket (table[i]) is null and exit . Therefore, the result of the method execution is from the current dirty entry (staleSlot) bit to the returned i bit, and all the entries in the middle are not dirty entries . Why does it encounter null exit? The reason is that the prerequisite for dirty entry is that the current hash bucket (table[i]) is not null , but the key field of the entry is null. If the hash bucket is null, it is obvious that it does not even have the prerequisites for a dirty entry.

Now make a summary of the cleanSomeSlot method. The schematic diagram of the method execution is as follows:

cleanSomeSlots schematic.png

As shown in the figure, the cleanSomeSlot method mainly has the following points:

  1. Starting from the current position i (the entry at i must not be a dirty entry), start searching for dirty entries backwards in the initial small range (log2(n), where n is the size of the number of entries that have been inserted into the hash table). There is no dirty entry in the whole search process, the method ends and exits
  2. If a dirty entry is encountered during the search process, the current dirty entry is cleaned up by the expungeStaleEntry method, and the method will return the index position i of the next hash bucket (table[i]) to be null. At this time, let the search starting point be the index position i again, n be the length len of the hash table, and expand the search range to log2(n') to continue the search.

Let's take an example to make it clearer. Suppose the current table array is as shown below.

cleanSomeSlots execution scenario map.png

  1. As shown in the figure, the current n is equal to the size of the hash table, that is, n=10, i=1. In the first search process, through nextIndex, i points to the position where the index is 2. At this time, table[2] is null, indicating that the first search If no dirty entry is found, the first search ends and the second search is performed.

  2. The second search first passes the nextIndex method, the index is changed from 2 to i=3, the current table[3]!=null but the key of the entry is null, indicating that a dirty entry is found, first set n to The length of the hash table is len, and then continue to call the expungeStaleEntry method , which will clear the dirty entry with the current index of 3 (let the value be null, and table[3] is also null), but this method does not want to be lazy, It will continue to search backwards in a circular manner, and will find that the entry at the position of index 4 and 5 is also a dirty entry, and the entry at the position of index 6 is not a dirty entry and remains unchanged until i=7. Here table[ 7] bit null, the method returns with i=7. At this point, the second search is over;

  3. Since a dirty entry is found in the second search, n is increased to the length len of the array, so expand the search range (increase the number of loops) and continue the backward circular search;

  4. Until no dirty entry is found in the entire search range, the cleanSomeSlot method ends and exits.

2.3 replaceStaleEntry

Let's first look at the replaceStaleEntry method. The source code of this method is:

/*
 * @param  key the key
 * @param  value the value to be associated with key
 * @param  staleSlot index of the first stale entry encountered while
 *         searching for key.
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).

	//向前找到第一个脏entry
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
1.          slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
			
			//如果在向后环形查找过程中发现key相同的entry就覆盖并且和脏entry进行交换
2.            e.value = value;
3.            tab[i] = tab[staleSlot];
4.            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
			//如果在查找过程中还未发现脏entry,那么就以当前位置作为cleanSomeSlots
			//的起点
            if (slotToExpunge == staleSlot)
5.                slotToExpunge = i;
			//搜索脏entry并进行清理
6.            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
		//如果向前未搜索到脏entry,则在查找过程遇到脏entry的话,后面就以此时这个位置
		//作为起点执行cleanSomeSlots
        if (k == null && slotToExpunge == staleSlot)
7.            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
	//如果在查找过程中没有找到可以覆盖的entry,则将新的entry插入在脏entry
8.    tab[staleSlot].value = null;
9.    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
10.    if (slotToExpunge != staleSlot)
		//执行cleanSomeSlots
11.        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

Please refer to the comments for the logic of this method. Below, I will describe the execution process of this method in detail in combination with various situations. First look at this part of the code:

int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

This part of the code uses the PreIndex method to realize the function of searching for dirty entries forward in a circular manner. Initially, slotToExpunge is the same as staleSlot. If a dirty entry is found during the search process, slotToExpunge is updated to the current index i. In addition, it shows that replaceStaleEntry is not limited to processing the currently known dirty entries. It believes that there is a high probability of dirty entries appearing in the adjacent positions of dirty entries. Therefore, in order to process them in place at one time, it is necessary to search forward in a circular manner to find Dirty entry at the front . Then according to whether there are dirty entries in the forward search and whether a coverable entry is found in the circular search after the for loop, we can fully understand this method in four cases:

  • 1. Forward dirty entry

    • 1.1 Backward ring search to find overridable entries

      This situation is shown in the figure below.

Dirty entries are found in a forward loop, and overwritten entries are found in a backward loop.png

	如图,slotToExpunge初始状态和staleSlot相同,当前向环形搜索遇到脏entry时,在第1行代码中slotToExpunge会更新为当前脏entry的索引i,直到遇到哈希桶(table[i])为null的时候,前向搜索过程结束。在接下来的for循环中进行后向环形查找,若查找到了可覆盖的entry,第2,3,4行代码先覆盖当前位置的entry,然后再与staleSlot位置上的脏entry进行交换。交换之后脏entry就更换到了i处,最后使用cleanSomeSlots方法从slotToExpunge为起点开始进行清理脏entry的过程

- 1.2后向环形查找未找到可覆盖的entry 
	该情形如下图所示。
	![前向环形搜索到脏entry,向后环形未搜索可覆盖entry.png](http://upload-images.jianshu.io/upload_images/2615789-423c8c8dfb2e9557.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
	如图,slotToExpunge初始状态和staleSlot相同,当前向环形搜索遇到脏entry时,在第1行代码中slotToExpunge会更新为当前脏entry的索引i,直到遇到哈希桶(table[i])为null的时候,前向搜索过程结束。在接下来的for循环中进行后向环形查找,若没有查找到了可覆盖的entry,哈希桶(table[i])为null的时候,后向环形查找过程结束。那么接下来在8,9行代码中,将插入的新entry直接放在staleSlot处即可,最后使用cleanSomeSlots方法从slotToExpunge为起点开始进行清理脏entry的过程
  • 2. No dirty entries in the forward direction

    • 2.1 Backward circular search to find the entry that can be covered The situation is shown in the figure below.

      Dirty entries are not searched forward, and coverable entries.png.png are found in a circular search backward
      As shown in the figure, the initial state of slotToExpunge is the same as that of staleSlot. The forward search process ends when the hash bucket (table[i]) is null in the current circular search. If no dirty entry is encountered during the whole process, the initial state of slotToExpunge Still the same as staleSlot. In the next for loop, a backward circular search is performed. If a dirty entry is encountered, the slotToExpunge is updated to position i in the 7th line of code. If an overridable entry is found, lines 2, 3, and 4 first overwrite the entry at the current location, and then exchange it with the dirty entry at the staleSlot location. After the exchange, the dirty entry is replaced at i. If no dirty entry is encountered during the entire search process, the fifth line of code will update slotToExpunge to the current i, and finally use the cleanSomeSlots method to start the process of cleaning dirty entries from slotToExpunge as the starting point.

    • 2.2 Backward circular search does not find a coverable entry The situation is shown in the figure below.

Dirty entries are not found in the forward loop, and overwritten entries are not found in the backward loop.

	如图,slotToExpunge初始状态和staleSlot相同,当前向环形搜索直到遇到哈希桶(table[i])为null的时候,前向搜索过程结束,若在整个过程未遇到脏entry,slotToExpunge初始状态依旧和staleSlot相同。在接下来的for循环中进行后向环形查找,若遇到了脏entry,在第7行代码中更新slotToExpunge为位置i。若没有查找到了可覆盖的entry,哈希桶(table[i])为null的时候,后向环形查找过程结束。那么接下来在8,9行代码中,将插入的新entry直接放在staleSlot处即可。另外,如果发现slotToExpunge被重置,则第10行代码if判断为true,就使用cleanSomeSlots方法从slotToExpunge为起点开始进行清理脏entry的过程。

Let's use an example to have an intuitive feeling. The sample code is not given. The table state when the code is debugged is shown in the following figure:

1.2 Schematic diagram of the situation.png

As shown in the figure, the current staleSolt is i=4. First, the forward search is performed for dirty entries. When i=3, a dirty entry is encountered, and slotToExpung is updated to 3. When i=2, tabel[2] is null. , so the forward search process for dirty entries ends. Then perform a backward circular search, know that table[7] is null when i=7, end the backward search process, and no entry that can be covered is found in this process. Finally, only a new entry can be inserted at staleSlot(4), and then cleanSomeSlots is performed from slotToExpunge(3) to clean up dirty entries. Is not the case of 1.2 above.

These core methods, through the source code and giving example diagrams, should be able to master in the end, and it is quite interesting. If you think it's good, you can give encouragement to my hard work. Welcome to like it and encourage my younger brother. Thank you here :).

When we call the get method of threadLocal, if table[i] is not the same as the key we are looking for, we will continue to search backward through the getEntryAfterMiss method of threadLocalMap. The method is:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

When key==null, even if a dirty entry is encountered, expungeStleEntry will be called to clean up the dirty entry.

When we call the threadLocal.remove method , we actually call the remove method of threadLocalMap. The source code of this method is:

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

It can also be seen that when a dirty entry with a key of null is encountered, expungeStaleEntry is also called to clean up the dirty entry.

It can be seen from the above set, getEntry, and remove methods that in the life cycle of threadLocal, for the problem of memory leaks in threadLocal, dirty entries with null keys will be cleaned up through the three methods of expungeStaleEntry, cleanSomeSlots, and replaceStaleEntry .

2.4 Why use weak references?

From the beginning of the article, it seems that threadLocal has a memory leak problem through the reference relationship of threadLocal, threadLocalMap and entry because threadLocal is modified by weak references. So why use weak references?

If you use strong references

Assuming that threadLocal uses strong references, operations are performed in business code threadLocalInstance==nullto clean up threadLocal instances. However, because the Entry of threadLocalMap strongly references threadLocal, the accessibility analysis is performed during gc, and threadLocal is still reachable. Garbage collection will not be performed, so that the purpose of business logic cannot be truly achieved, and logic errors will occur.

If you use weak references

Assuming that Entry weakly references threadLocal, although there will be a memory leak problem, in the life cycle of threadLocal (set, getEntry, remove), dirty entries with a key of null will be processed.

It can be seen from the above analysis that the use of weak references will ensure that there is no memory leak problem as much as possible in the threadLocal life cycle and achieve a safe state.

2.5 Thread.exit()

The exit method is executed when the thread exits:

private void exit() {
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    /* Aggressively null out all reference fields: see bug 4006245 */
    target = null;
    /* Speed the release of some of these resources */
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
}

It can be seen from the source code that when the thread ends, threadLocals=null, which means that threadLocalMap can be garbage collected during GC. In other words, the life cycle of threadLocalMap is actually the same as that of thread.

3. threadLocal best practices

Through this article, we have made a detailed analysis of threadLocal memory leaks. We can fully understand the causes and consequences of threadLocal memory leaks. So what should we do in practice?

  1. Every time ThreadLocal is used, its remove() method is called to clear the data.
  2. In the case of using the thread pool, failing to clean up ThreadLocal in time is not only a problem of memory leaks, but more seriously, it may cause problems with business logic. Therefore, using ThreadLocal is like unlocking after locking, and cleaning up after use.

References

"Java High Concurrency Programming" blog.xiaohansong.com/2016/08/06/…

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325966870&siteId=291194637