Magical use of strong and weak virtual references in java

Posted Jun 28, 202011 min read

Foreword

Under what circumstances can ThreadLocal cause a memory leak? If you want to understand the ins and outs of this problem, looking at the source code is indispensable. After reading the source code, you find that the actual ThreadLocal actually uses static class Entry extends WeakReference<ThreadLocal<?>> {}, the actual answer is Weak reference WeakReference is used.

Summary of this article

  • Strong reference:Object o = new Object()
  • Soft reference:new SoftReference(o);
  • Weak reference:new WeakReference(o);
  • Virtual reference:new PhantomReference(o);
  • The use of ThreadLocal and the causes of memory leaks due to improper use

Jdk 1.2 added abstract classes Reference and SoftReference, WeakReference, and PhantomReference, which extended reference type classification to achieve more fine-grained control of memory.

For example, our cache data, when the memory is not enough, I hope the cache can release the memory, or store the cache off the heap, etc.

But how do we distinguish which objects need to be recycled(garbage collection algorithm, reachability analysis), when recycling allows us to get notification of recycling, so JDK 1.2 brings these several reference types.

Reference Type When to Recycle
Strong reference As long as the GC root is reachable, the strongly referenced object will not be reclaimed, and the memory is not enough, it will throw oom
Soft reference:SoftReference Soft reference object, in GC root, only soft reference can reach an object a, before oom, garbage collection will recycle the object a
Weak reference:WeakReference Weak reference, GC root, only weak reference can reach an object c, gc will be recycled c
Virtual reference:PhantomReference Virtual reference, must be used with ReferenceQueue, when to recycle is not known, but after recycling, you can operate ReferenceQueue to get the reference to be recycled

Strong reference

Strong reference is the way we often use:Object o = new Object(). During garbage collection, strongly referenced variables will not be collected. Only by setting o=null, jvm will pass the reachability analysis, and no GC root will reach the object, the garbage collector will clean up the objects in the heap and release the memory. When you continue to apply for memory allocation, it will oom.

Define a class Demo. The Demo instance occupies 10m of memory. Keep adding Demo examples to the list. Since the memory allocation cannot be applied, the program throws oom and terminates.

//-Xmx600m
public class SoftReferenceDemo {
    //1m
    private static int _1M = 1024 * 1024 * 1;
    public static void main(String[]args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        while(true) {
            Thread.sleep(100);
            //Get how much jvm free memory m
            long meme_free = Runtime.getRuntime().freeMemory()/_1M;
            if((meme_free-10) >= 0) {
                Demo demo = new Demo(count);
                objects.add(demo);
                count++;
                demo = null;
            }
            System.out.println("jvm free memory" + meme_free + "m");
            System.out.println(objects.size());
        }
    }

    @Data
    static class Demo {
        private byte[]a = new byte[_1M * 10];
        private String str;
        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

The result of the above code running, throwing oom program to stop

jvm free memory 41 m
54
Exception in thread "main" java.lang.OutOfMemoryError:Java heap space
    at com.fly.blog.ref.SoftReferenceDemo$Demo.<init>(SoftReferenceDemo.java:37)
    at com.fly.blog.ref.SoftReferenceDemo.main(SoftReferenceDemo.java:25)

However, some business scenarios require that we do not have enough memory to free up unnecessary data. For example, we store user information in the cache.

Soft reference

jdk added Reference from 1.2. SoftReference is one of the categories. Its function is to reach object a through GC root. Only SoftReference. Object a will be released by jvm gc before jvm oom. .

Infinite loop adds 10m data(SoftReference) to List and finds that no oom appears.

//-Xmx600m
public class SoftReferenceDemo {
    //1m
    private static int _1M = 1024 * 1024 * 1;
    public static void main(String[]args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        while(true) {
            Thread.sleep(500);
            //Get how much jvm free memory m
            long meme_free = Runtime.getRuntime().freeMemory()/_1M;
            if((meme_free-10) >= 0) {
                Demo demo = new Demo(count);
                SoftReference<Demo> demoSoftReference = new SoftReference<>(demo);
                objects.add(demoSoftReference);
                count++;
                //demo is null, there is only one demoSoftReference reference reaching the instance of Demo, GC will recycle the instance of Demo before oom
                demo = null;
            }
            System.out.println("jvm free memory" + meme_free + "m");
            System.out.println(objects.size());
        }
    }
    @Data
    static class Demo {
        private byte[]a = new byte[_1M * 10];
        private String str;
        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

image-20200625213429845

Check the usage of the jvm heap through jvisualvm, and you can see that the heap will be recycled when it is about to overflow. When the free memory is large, you actively perform execute garbage collection, and the memory will not be recycled.

Weak reference

When the reference of the object demo is only reachable by WeakReference, the demo will be recovered after gc to release the memory.

The following programs will continue to run, but the timing of memory release is different.

//-Xmx600m -XX:+PrintGCDetails
public class WeakReferenceDemo {
    //1m
    private static int _1M = 1024 * 1024 * 1;

    public static void main(String[]args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        while(true) {
            Thread.sleep(100);
            //Get how much jvm free memory m
            long meme_free = Runtime.getRuntime().freeMemory()/_1M;
            if((meme_free-10) >= 0) {
                Demo demo = new Demo(count);
                WeakReference<Demo> demoWeakReference = new WeakReference<>(demo);
                objects.add(demoWeakReference);
                count++;
                demo = null;
            }
            System.out.println("jvm free memory" + meme_free + "m");
            System.out.println(objects.size());
        }
    }

    @Data
    static class Demo {
        private byte[]a = new byte[_1M * 10];
        private String str;
        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

As a result of the operation, SoftReference free memory will release the memory when it is almost exhausted, and WeakReference will garbage every time the available memory reaches about 360m`, and release the memory

[GC(Allocation Failure) [PSYoungGen:129159K->1088K(153088K)]129175K->1104K(502784K), 0.0007990 secs][Times:user=0.00 sys=0.00, real=0.00 secs]
jvm free memory 364 m
36
jvm free memory 477 m

Virtual reference

It is also called phantom reference, because you don t know when it will be recycled, you must cooperate with ReferenceQueue, when the object is recycled, you can get an instance of PhantomReference from this queue.

//-Xmx600m -XX:+PrintGCDetails
public class PhantomReferenceDemo {
    //1m
    private static int _1M = 1024 * 1024 * 1;

    private static ReferenceQueue referenceQueue = new ReferenceQueue();

    public static void main(String[]args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        new Thread(() -> {
            while(true) {
                try {
                    Reference remove = referenceQueue.remove();
                    //objects reachability analysis, you can reach PhantomReference<Demo>, the memory cannot be released in time, we need to get the demo in the team and it is recycled, then
                    //remove this object from objects
                    if(objects.remove(remove)) {
                        System.out.println("Remove element");
                    }
                } catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        while(true) {
            Thread.sleep(500);
            //Get how much jvm free memory m
            long meme_free = Runtime.getRuntime().freeMemory()/_1M;
            if((meme_free-10)> 40) {
                Demo demo = new Demo(count);
                PhantomReference<Demo> demoWeakReference = new PhantomReference<>(demo, referenceQueue);
                objects.add(demoWeakReference);
                count++;
                demo = null;
            }
            System.out.println("jvm free memory" + meme_free + "m");
            System.out.println(objects.size());
        }
    }

    @Data
    static class Demo {
        private byte[]a = new byte[_1M * 10];
        private String str;

        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

ThreadLocal

In our actual development, ThreadLocal is still used a lot. So what exactly is it(thread local variables), we know local variables(variables defined in methods) and member variables(class properties).

Sometimes, we hope that the life cycle of a variable can run through a task running cycle of the entire thread(threads in the thread pool can be assigned to perform different tasks), we can get this preset variable when each method is called , This is the role of ThreadLocal.

For example, we want to get the HttpServletRequest of the current request, which can be obtained in the current methods. SpringBoot has been encapsulated for us. RequestContextFilter will be set via RequestContextHolder after each request. Thread local variables, the principle is to operate ThreadLocal`.

ThreadLocal is only for calls in the current thread, cross-thread calls are not acceptable, so Jdk implements by inheriting ThreadLocal through InheritableThreadLocal.

ThreadLocal Get the user information of the current request

You can roughly understand how the TheadLocal is used by reading the notes

/**
 * @author    
 * @date 2018/12/21-22:59
 */
@RestController
public class UserInfoController {
    @RequestMapping("/user/info")
    public UserInfoDTO getUserInfoDTO() {
        return UserInfoInterceptor.getCurrentRequestUserInfoDTO();
    }
}

@Slf4j
public class UserInfoInterceptor implements HandlerInterceptor {
    private static final ThreadLocal<UserInfoDTO> THREAD_LOCAL = new ThreadLocal();
    //Request header username
    private static final String USER_NAME = "userName";
    //Note this, only beans injected into ioc can be injected
    @Autowired
    private IUserInfoService userInfoService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //Determine if it is an interface request
        if(handler instanceof HandlerMethod) {
            String userName = request.getHeader(USER_NAME);
            UserInfoDTO userInfoByUserName = userInfoService.getUserInfoByUserName(userName);
            THREAD_LOCAL.set(userInfoByUserName);
            return true;
        }
        return false;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //Remember to release the memory after use
        THREAD_LOCAL.remove();
    }
    //Get the user information set by the current thread
    public static UserInfoDTO getCurrentRequestUserInfoDTO() {
        return THREAD_LOCAL.get();
    }
}

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * Inject UserInfoInterceptor into ioc container
     */
    @Bean
    public UserInfoInterceptor getUserInfoInterceptor() {
        return new UserInfoInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //Calling this method returns the ioc bean
        registry.addInterceptor(getUserInfoInterceptor()).addPathPatterns("/**");
    }
}

InheritableThreadLocal

Sometimes, we hope that the life cycle of the local variables of the current thread can be extended to the child thread, and the variables set by the parent thread are obtained in the child thread. InheritableThreadLocal provides this capability.

/**
 * @author    
 * @date 2020-06-27-21:18
 */
public class InheritableThreadLocalDemo {
    static InheritableThreadLocal<String> INHERITABLE_THREAD_LOCAL = new InheritableThreadLocal();
    static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
    public static void main(String[]args) throws InterruptedException {
        INHERITABLE_THREAD_LOCAL.set("Use InheritableThreadLocal to set variable in parent thread");
        THREAD_LOCAL.set("Use ThreadLocal to set variables in parent thread");
        Thread thread = new Thread(
               () -> {
                    //Can get the set variables
                    System.out.println("Take the variable set by the parent thread from InheritableThreadLocal:"+ INHERITABLE_THREAD_LOCAL.get());
                    //print as null
                    System.out.println("Take the variable set by the parent thread from ThreadLocal:"+ THREAD_LOCAL.get());
                }
       );
        thread.start();
        thread.join();
    }
}

ThreadLocal get method source code analysis

You can understand that Thead object has an attribute Map whose key is an instance of ThreadLoal, which obtains the source code of thread local variables

public class ThreadLocal<T> {
    public T get() {
        //Get running in that thread
        Thread t = Thread.currentThread();
        //Get Map from Thread
        ThreadLocalMap map = getMap(t);
        if(map != null) {
            //Use ThreadLocal instance to get value from Map
            ThreadLocalMap.Entry e = map.getEntry(this);
            if(e != null) {
                @SuppressWarnings("unchecked")
                T result =(T)e.value;
                return result;
            }
        }
        //Initialize the Map, and return the initialization value, the default is null, you can define a method, load the initialization value from this method
        return setInitialValue();
    }
}

InheritableThreadLocal Get data analysis set by parent thread

Each Thread also has a Map property inheritableThreadLocals, which is used to save the value copied from the parent thread.

When the child thread is initialized, it will copy the value of the parent thread's Map(inheritableThreadLocals) to its own Thead Map(inheritableThreadLocals), each thread maintains its own inheritableThreadLocals, so the child thread cannot change the data maintained by the parent thread, just The child thread can obtain the data set by the parent thread.

public class Thread{

    //Maintain thread local variables
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //Maintain the data of the parent thread that the child thread can inherit
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

   //Thread initialization
    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        init(group, target, name, stackSize);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if(inheritThreadLocals && parent.inheritableThreadLocals != null){
            //Copy the inheritableThreadLocals data of the parent thread to the child thread
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        }
    }
}

public class TheadLocal{
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        ///Create a Map of your own thread and copy the value of the parent thread
        return new ThreadLocalMap(parentMap);
    }

    static class ThreadLocalMap {
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[]parentTable = parentMap.table;
            int len   = parentTable.length;
            setThreshold(len);
            table = new Entry[len];
            //Traverse the parent thread, copy the data over
            for(int j = 0; j <len; j++) {
                Entry e = parentTable[j];
                if(e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key =(ThreadLocal<Object>) e.get();
                    if(key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode &(len-1);
                        while(table[h]!= null)
                            h = nextIndex(h, len);
                        table[h]= c;
                        size++;
                    }
                }
            }
        }
    }
}

demo verification, analysis above

image-20200627232351534

image-20200627225502636

Cause of memory leak

A 20-size thread pool is defined, 50 tasks are executed, and after execution, threadLocal is set to null to simulate a memory leak scenario. In order to eliminate interference factors, I set the jvm parameter to -Xms8g -Xmx8g -XX:+PrintGCDetails

public class ThreadLocalDemo {
    private static ExecutorService executorService = Executors.newFixedThreadPool(20);
    private static ThreadLocal threadLocal = new ThreadLocal();

    public static void main(String[]args) throws InterruptedException {
        for(int i = 0; i <50; i++) {
            executorService.submit(() -> {
                try {
                    threadLocal.set(new Demo());
                    Thread.sleep(50);
                } catch(InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if(Objects.nonNull(threadLocal)) {
                        //To prevent memory leaks, the current thread is used up and the value is cleared
//threadLocal.remove();
                    }
                }
            });
        }
        Thread.sleep(5000);
        threadLocal = null;
        while(true) {
            Thread.sleep(2000);
        }
    }
    @Data
    static class Demo {
        //
        private Demo[]demos = new Demo[1024 * 1024 * 5];
    }
}

Running the program, no gc log is printed, indicating no garbage collection

image-20200628020439866

image-20200628020512394

In Java VisualVM we execute garbage collection and the memory distribution after the collection, the 20 ThreadLocalDemo$Demo[] cannot be recovered, this is a memory leak.

image-20200628020811328

The program created 50 Demo in 50 cycles, and no garbage collection will be triggered during the running of the program(set by jvm parameter guarantee), so the number of live instances of ThreadLocalDemo$Demo[] is 50.

When I manually triggered GC, the number of instances dropped to 20, which was not what we expected to be 0. This is a memory leak in the program

Why did a memory leak occur?

Because each thread corresponds to a Thread, the thread pool size is 20. ThreadLocal.ThreadLocalMap threadLocals = null; in Thread`

There are Entry[]tables in ThreadLocalMap, k is weak reference. When we set threadLocal to null, the GC ROOT to ThreadLocalDemo$Demo[] reference chain still exists, but the k is recovered, the value still exists, the table length will not change, it will not be recovered of.

image-20200628023936332

ThreadLocal optimizes for the case where k is null when set and get, and sets the corresponding tables[i]to null. This way a single Entry can be recycled. But after we set ThreadLocal to null, we can't call the method. You can only wait until Thread calls another ThreadLocal and operate ThreadLocalMap according to the conditions, perform Map rehash, and delete the Entry with k null.

The above problem is also more convenient to solve. After the thread has used the thread local variable, call remove to actively clear the Entry.


This article was created by Zhang Panqin's blog http://www.mflyyou.cn/ . It can be reproduced and quoted freely, but the author must be signed and the source of the article should be indicated.

If reprinted to WeChat public account, please add the author's public account QR code at the end of the article. WeChat public account name:Mflyyou