• JVM源码剖析之软、弱、虚引用的处理细节


    目录

    写在前面:

    源码剖析:

    Java层面:

    JVM层面:

    使用危险点:

    总结:


    版本信息:

    1. jdk版本:jdk8u40
    2. 垃圾回收器:Serial new/old

    写在前面:

    不同的垃圾回收器所对应的算法不一样,效率更不一样。在JDK8中默认为ParallelScavenge new/old。而笔者写文时使用Serial new/old,两者算法一致,只不过ParallelScavenge new/old发挥了多线程的优势,所以在算法细节上大同小异。

    对于大大大大大大部分Java业务场景来说都是强引用,基本上不会使用到软、弱、虚引用。而在JDK1.2推出的软、弱、虚引用大部分出现场景都是在缓存中,在JDK类库ThreadLocal、WeakHashMap。框架:Mybatis、Netty、以及各种缓存框架等等。至于为什么要用在缓存中呢,也很好理解,因为这些引用实际上可有可无,完美契合于缓存,在有的时候给系统加速,在系统内存紧张的时候清除缓存给核心业务使用。

    源码剖析:

    这篇文章的篇幅会比较长,也不容易理解。因为对于软、弱、虚引用处理细节体现在Java层面和JVM层面,恰好JVM层面又与GC垃圾回收细节强关联,所以笔者只能竭尽所能~

    Java层面:

    在Java层面,就不得不补充一些前置知识,以及Java层面如何处理这些引用。

    软、弱、虚引用的基本表示

    上图是软、弱、虚引用最基本的表示,这里需要区分2个不同的对象,一个是软、弱、虚对象,一个是软、弱、虚引用的对象。

    软、弱、虚对象
    软、弱、虚引用的对象

    所以下文需要介绍软、弱、虚对象的回收机制和区分具体的使用场景(相信大家八股文多多少少背过,这里跟八股文会有一点点出入)

    软:当系统资源紧张但是又没那么那么紧张的时候根据最近最少使用回收软引用(LRU算法),当系统资源非常非常紧张的时候直接全部回收。可以携带引用对象,也可以使用ReferenceQueue去处理伴随对象

    弱:只要发生GC就会回收。可以携带引用对象,也可以使用ReferenceQueue去处理伴随对象

    虚:只要发生GC就会回收。不能携带引用对象。只能使用ReferenceQueue去处理伴随对象

    上文有介绍软、弱、虚对象的回收机制,这里有提到ReferenceQueue队列,所以下文开始介绍Java层面如何使用ReferenceQueue做回收。

    1. // java.lang.ref.Reference类中静态方法
    2. static {
    3. // 创建一个ReferenceHandler线程。
    4. Thread handler = new ReferenceHandler(tg, "Reference Handler");
    5. handler.setPriority(Thread.MAX_PRIORITY);
    6. handler.setDaemon(true);
    7. handler.start();
    8. }

    在java.lang.ref.Reference类中静态方法中创建了一个ReferenceHandler线程。所以接下来看线程的执行体。

    1. public void run() {
    2. while (true) {
    3. tryHandlePending(true);
    4. }
    5. }
    6. static boolean tryHandlePending(boolean waitForNotify) {
    7. Reference r;
    8. …………
    9. synchronized (lock) {
    10. if (pending != null) {
    11. // 如果pedding不为null,那么就代表GC回收到了软、弱、虚引用
    12. r = pending;
    13. pending = r.discovered;
    14. r.discovered = null;
    15. } else {
    16. if (waitForNotify) {
    17. // 当还没产生pending链表的时候(也即没有触发GC回收软、弱、虚引用)
    18. // 当前线程直接去阻塞,等待被JVM唤醒。
    19. lock.wait();
    20. }
    21. return waitForNotify;
    22. }
    23. }
    24. …………
    25. // 把GC回收到了软、弱、虚引用放入到对应的ReferenceQueue中。
    26. // 等待业务自己去处理ReferenceQueue队列。
    27. ReferenceQueuesuper Object> q = r.queue;
    28. if (q != ReferenceQueue.NULL) q.enqueue(r);
    29. return true;
    30. }
    31. boolean enqueue(Reference r) {
    32. synchronized (lock) {
    33. ReferenceQueue queue = r.queue;
    34. if ((queue == NULL) || (queue == ENQUEUED)) {
    35. return false;
    36. }
    37. // 头插法
    38. r.queue = ENQUEUED;
    39. r.next = (head == null) ? r : head;
    40. head = r;
    41. queueLength++;
    42. lock.notifyAll();
    43. return true;
    44. }
    45. }
      1. 判断当前pedding 是否为空
      2. 如果为空,代表当前GC没有触发回收软、弱、虚引用
      3. 如果不为空,代表当前GC回收软、弱、虚引用,并且放入到pedding中
      4. 把pedding的值放入到ReferenceQueue队列中
      5. 业务维护的ReferenceQueue队列,从队列中poll值去做对应的处理。

      所以ReferenceQueue队列是业务层面自己维护,传入到Reference中,GC回收软、弱、虚引用后会把当前Reference放入到ReferenceQueue队列中。业务层面再通过poll取到Reference做对应的处理(可以是处理伴随对象)

      下面是WeakHashMap对ReferenceQueue的使用。

      WeakHashMap的使用

      至此,Java层面的处理已经看完,接下来我们需要明白JVM是如何GC处理软、弱、虚引用,并且放入到pedding中,这样就全部闭环~

      JVM层面:

      具体的GC回收过程本文肯定是忽略,当作黑盒即可~

      /hotspot/src/share/vm/memory/genCollectedHeap.cpp 文件中

      1. // /hotspot/src/share/vm/memory/genCollectedHeap.cpp
      2. // 这里是GC垃圾回收的过程
      3. void GenCollectedHeap::do_collection(bool full,
      4. bool clear_all_soft_refs,
      5. size_t size,
      6. bool is_tlab,
      7. int max_level) {
      8. …………
      9. // 是否需要清理所有的软引用
      10. const bool do_clear_all_soft_refs = clear_all_soft_refs ||
      11. collector_policy()->should_clear_all_soft_refs();
      12. {
      13. …………
      14. for (int i = starting_level; i <= max_level; i++) {
      15. if (_gens[i]->should_collect(full, size, is_tlab)) {
      16. {
      17. // 从这里可以看出,不同带都有一个引用的处理器。
      18. ReferenceProcessor* rp = _gens[i]->ref_processor();
      19. rp->enable_discovery(true /*verify_disabled*/, true /*verify_no_refs*/);
      20. // 改变回收策略
      21. rp->setup_policy(do_clear_all_soft_refs);
      22. // 不同代进行垃圾回收。
      23. _gens[i]->collect(full, do_clear_all_soft_refs, size, is_tlab);
      24. // gc回收后,把回收的软、弱、虚引用赋值给pedding,交给Java层面处理
      25. // 这里也对应到上下文了。
      26. if (!rp->enqueuing_is_done()) {
      27. rp->enqueue_discovered_references();
      28. } else {
      29. rp->set_enqueuing_is_done(false);
      30. }
      31. }
      32. }
      33. }
      34. …………
      35. }
      36. }
      1. 这里根据策略决定是否要清理所有的软引用(一般是内存资源极度不够的时候才会)
      2. 新生代或者老年代的垃圾回收器进行垃圾回收(这也对应了YGC和FullGC)
      3. 在GC回收后把回收到的软、弱、虚引用赋值给pedding,交给Java层面处理

      所以接下来需要看到老年代的垃圾回收器进行垃圾回收的时候如何处理的软、弱、虚引用。

      /hotspot/src/share/vm/memory/defNewGeneration.cpp 文件中

      1. // 新生代的垃圾回收
      2. void DefNewGeneration::collect(bool full,
      3. bool clear_all_soft_refs,
      4. size_t size,
      5. bool is_tlab) {
      6. …………
      7. // 用于扫描软、弱、虚引用是否存活。
      8. ScanWeakRefClosure scan_weak_ref(this);
      9. // 对象扫描器,用于GC root的复制
      10. FastScanClosure fsc_with_no_gc_barrier(this, false);
      11. FastScanClosure fsc_with_gc_barrier(this, true);
      12. // Klass的GC root扫描。
      13. KlassScanClosure klass_scan_closure(&fsc_with_no_gc_barrier,
      14. gch->rem_set()->klass_rem_set());
      15. // GC Root广度搜索的扫描器
      16. // 也就是找到GC Root的引用作为下一批GC Root,直到找完所有的存活对象。
      17. FastEvacuateFollowersClosure evacuate_followers(gch, _level, this,
      18. &fsc_with_no_gc_barrier,
      19. &fsc_with_gc_barrier);
      20. // 寻找根GC Root。
      21. // 因为是新生代的算法,所以这里会把根GC Root复制到to区或者是老年代。
      22. gch->gen_process_strong_roots(_level,
      23. true, // Process younger gens, if any,
      24. // as strong roots.
      25. true, // activate StrongRootsScope
      26. true, // is scavenging
      27. SharedHeap::ScanningOption(so),
      28. &fsc_with_no_gc_barrier,
      29. true, // walk *all* scavengable nmethods
      30. &fsc_with_gc_barrier,
      31. &klass_scan_closure);
      32. // 根据GC Root找出GC Root所有的引用
      33. // 因为这里是处理引用,所以这里会处理软、弱、虚等等引用。
      34. evacuate_followers.do_void();
      35. // 用于处理引用对象的存活。
      36. FastKeepAliveClosure keep_alive(this, &scan_weak_ref);
      37. ReferenceProcessor* rp = ref_processor();
      38. // 根据clear_all_soft_refs这个bool字段决定是否清理全部的软引用。
      39. rp->setup_policy(clear_all_soft_refs);
      40. // 具体的处理细节。
      41. const ReferenceProcessorStats& stats =
      42. rp->process_discovered_references(&is_alive, &keep_alive, &evacuate_followers,
      43. NULL, _gc_timer);
      44. …………
      45. }

      以上是YGC时,新生代的回收,不管是Full GC还是YGC都会对软、弱、虚引用做处理,所以挑选YGC来做分析(因为YGC简单一些,但是对于软、弱、虚引用做处理都是一样的)

      由于处理软、弱、虚引用一定会和GC回收细节强关联,所以很多是GC回收的细节代码,笔者有吧注释给上,并且当作黑盒就好。

      1. 创建好各种GC回收所需要扫描器
      2. 这些扫描器最终都有一个共同的任务,就是把存活对象复制到to区或者老年代
      3. GC Root的扫描
      4. 根据已有的GC Root做广度遍历,找出GC Root引用的对象作为下一批GC Root继续找引用,直到遍历完整个堆
      5. 软、弱、虚引的处理(这也是接下来的重点)

      经过GC Root全部查找后,Java堆的对象排布可能是这样

      注意,这里的软、弱、虚对象和软、弱、虚对象所引用对象是有区别的,复制算法只会把软、弱、虚对象做复制,软、弱、虚对象引用的对象要后续再做处理。

      在看ReferenceProcessor类process_discovered_references方法之前,需要介绍一下ReferenceProcessor类。

      /hotspot/src/share/vm/memory/referenceProcessor.hpp 文件中

      1. class ReferenceProcessor : public CHeapObj {
      2. protected:
      3. static ReferencePolicy* _default_soft_ref_policy;
      4. static ReferencePolicy* _always_clear_soft_ref_policy;
      5. ReferencePolicy* _current_soft_ref_policy;
      6. uint _num_q;
      7. uint _max_num_q;
      8. // 作为基地址。
      9. DiscoveredList* _discovered_refs;
      10. DiscoveredList* _discoveredSoftRefs; // 基于基地址的第一部分
      11. DiscoveredList* _discoveredWeakRefs; // 基于基地址的第二部分
      12. DiscoveredList* _discoveredFinalRefs; // 基于基地址的第三部分
      13. DiscoveredList* _discoveredPhantomRefs; // 基于基地址的第四部分
      14. }

      可以很清楚的看到,这里有策略对象和几个DiscoveredList链表。链表中是保存了被处理的软、弱、虚的Java对象。并且在遍历完所有的GC Root后,这里会把软、弱、虚的Java对象行程如下的链表。

      所以接下来,看到process_discovered_references方法具体处理细节。

      /hotspot/src/share/vm/memory/referenceProcessor.cpp 文件中

      1. ReferenceProcessorStats ReferenceProcessor::process_discovered_references(
      2. BoolObjectClosure* is_alive,
      3. OopClosure* keep_alive,
      4. VoidClosure* complete_gc,
      5. AbstractRefProcTaskExecutor* task_executor,
      6. GCTimer* gc_timer) {
      7. _soft_ref_timestamp_clock = java_lang_ref_SoftReference::clock();
      8. // 软引用的处理
      9. size_t soft_count = 0;
      10. {
      11. GCTraceTime tt("SoftReference", trace_time, false, gc_timer);
      12. soft_count =
      13. process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true,
      14. is_alive, keep_alive, complete_gc, task_executor);
      15. }
      16. // 修改时间戳。
      17. // 时间戳用于LRU算法,寻找最近最少使用的软引用。
      18. update_soft_ref_master_clock();
      19. // 弱引用的处理
      20. size_t weak_count = 0;
      21. {
      22. GCTraceTime tt("WeakReference", trace_time, false, gc_timer);
      23. weak_count =
      24. process_discovered_reflist(_discoveredWeakRefs, NULL, true,
      25. is_alive, keep_alive, complete_gc, task_executor);
      26. }
      27. // 最终引用处理,这个一般是用于收尾工作
      28. size_t final_count = 0;
      29. {
      30. GCTraceTime tt("FinalReference", trace_time, false, gc_timer);
      31. final_count =
      32. process_discovered_reflist(_discoveredFinalRefs, NULL, false,
      33. is_alive, keep_alive, complete_gc, task_executor);
      34. }
      35. // 虚引用处理
      36. size_t phantom_count = 0;
      37. {
      38. GCTraceTime tt("PhantomReference", trace_time, false, gc_timer);
      39. phantom_count =
      40. process_discovered_reflist(_discoveredPhantomRefs, NULL, false,
      41. is_alive, keep_alive, complete_gc, task_executor);
      42. }
      43. return ReferenceProcessorStats(soft_count, weak_count, final_count, phantom_count);
      44. }

      可以看到不管是软、弱、虚引用的处理都是调用process_discovered_reflist方法。

      /hotspot/src/share/vm/memory/referenceProcessor.cpp 文件中

      1. size_t
      2. ReferenceProcessor::process_discovered_reflist(
      3. DiscoveredList refs_lists[],
      4. ReferencePolicy* policy,
      5. bool clear_referent,
      6. BoolObjectClosure* is_alive,
      7. OopClosure* keep_alive,
      8. VoidClosure* complete_gc,
      9. AbstractRefProcTaskExecutor* task_executor)
      10. {
      11. // 根据策略决定是否能处理引用。
      12. // 策略只有软引用才有。
      13. // 弱、虚引用是不配有策略的,弱、虚引用只要发生GC久回收
      14. if (policy != NULL) {
      15. for (uint i = 0; i < _max_num_q; i++) {
      16. process_phase1(refs_lists[i], policy,
      17. is_alive, keep_alive, complete_gc);
      18. }
      19. }
      20. // 遍历剩下的队列,继续做过滤操作
      21. // 这个过滤是判断软、弱、虚对象引用的对象是否还活着,如果活着那就不能处理这个引用。
      22. for (uint i = 0; i < _max_num_q; i++) {
      23. process_phase2(refs_lists[i], is_alive, keep_alive, complete_gc);
      24. }
      25. // 根据clear_referent变量决定最终是否处理引用。
      26. for (uint i = 0; i < _max_num_q; i++) {
      27. process_phase3(refs_lists[i], clear_referent,
      28. is_alive, keep_alive, complete_gc);
      29. }
      30. return total_list_count;
      31. }
      1. 软引用才有策略,根据策略决定是否回收对象,如果策略不让回收的对象,那么就需要从DiscoveredList链表中remove,并且保持存活,直到下次GC 再尝试回收
      2. 经过策略的决策后,活下来的对象继续做过滤,这次过滤是判断软、弱、虚对象引用的对象是否还活着,如果活着那就不能处理这个引用(所以用不好,随时可能内存泄漏),如果引用对象是存活的,那么就需要从DiscoveredList链表中remove,并且保持存活,直到下次GC 再尝试回收
      3. 经过第二步的过滤,活下来的对象还要根据clear_referent变量决定最终是否处理引用对象。这一步只有虚引用才不能处理引用(因为虚对象不能引用对象),如果clear_reference为false,那么就需要从DiscoveredList链表中remove,并且保持存活,直到下次GC 再尝试回收,但是虚引用为false也没关系,因为他指向本来就是null。

      所以接下来可以看一下软引用的策略处理。

      这里就比较简单了,要不永远回收、要不永远不回收,要不根据LRU算法得到最近最少使用的软引用,优先回收没用的~

      所以在本文最上面写到:软引用,在内存紧张的时候但是不是非常紧张的时候会回收最少使用的(根据LRU算法),在内存非常非常紧张的时候策略直接是AlwaysCLearPolicy策略了,就回收所有软引用~

      当经过层层过滤后,最终存活的软、弱、虚对象就存在不同DiscoveredList链表中。我们在Java层面是从pedding获取到对象,所以这边还需要把不同的DiscoveredList链表设置到pedding中。

      所以接下来回到GenCollectedHeap::do_collection方法,看到enqueue_discovered_references方法

      /hotspot/src/share/vm/memory/referenceProcessor.cpp 文件中

      1. bool ReferenceProcessor::enqueue_discovered_references(AbstractRefProcTaskExecutor* task_executor) {
      2. return enqueue_discovered_ref_helper(this, task_executor);
      3. }
      4. template <class T>
      5. bool enqueue_discovered_ref_helper(ReferenceProcessor* ref,
      6. AbstractRefProcTaskExecutor* task_executor) {
      7. // 拿到Reference类中的pedding变量的地址,因为pending是一个静态变量,所以从mirror拿。
      8. T* pending_list_addr = (T*)java_lang_ref_Reference::pending_list_addr();
      9. // 把链表链到pedding上
      10. ref->enqueue_discovered_reflists((HeapWord*)pending_list_addr, task_executor);
      11. return old_pending_list_value != *pending_list_addr;
      12. }
      13. void ReferenceProcessor::enqueue_discovered_reflists(HeapWord* pending_list_addr,
      14. AbstractRefProcTaskExecutor* task_executor) {
      15. // 串行化遍历4个链表。
      16. for (uint i = 0; i < _max_num_q * number_of_subclasses_of_ref(); i++) {
      17. // 只需要把每个链表的头部链到pending就行了。
      18. enqueue_discovered_reflist(_discovered_refs[i], pending_list_addr);
      19. _discovered_refs[i].set_head(NULL);
      20. _discovered_refs[i].set_length(0);
      21. }
      22. }

      这里就是把经过层层筛选的软、弱、虚链表中的对象链到Reference类中pedding字段上。最终交给Java层面的ReferenceHandler线程去处理。

      使用危险点:

      上面我们把所有的处理细节都分析完了,所以接下来回忆到一处细节点。

      /hotspot/src/share/vm/memory/referenceProcessor.cpp 文件中,process_discovered_reflist方法,这个方法是做过滤处理,在process_phase2这个方法做过滤的时候,会判断软、弱、虚对象的引用对象是否存活,如果存活的情况下是不能做回收的。所以这里很容易发生内存泄露,看到如下的Java代码。

      1. public class ReferenceTest {
      2. public static void main(String[] args) {
      3. WeakHashMap weakHashMap = new WeakHashMap<>();
      4. Object o1 = new Object();
      5. weakHashMap.put(o1,new User("lihayyds")); // 只要o1不释放这就是内存泄露。
      6. weakHashMap.put("1",new User("lihayyds")); // "1"是JVM字符串常量池指向的,所以这也是一个内存泄露
      7. byte[] bytes1 = new byte[1024 * 1024 * 1024];
      8. byte[] bytes2 = new byte[1024 * 1024 * 1024];
      9. byte[] bytes3 = new byte[1024 * 1024 * 1024];
      10. byte[] bytes4 = new byte[1024 * 1024 * 1024];
      11. byte[] bytes5 = new byte[1024 * 1024 * 1024];
      12. // 手动Full GC。
      13. System.gc();
      14. // Reference Queue 处理后的大小,因为在size里面会去处理
      15. System.out.println("Reference Queue 处理后的大小为:"+weakHashMap.size());
      16. }
      17. }
      18. class User{
      19. String name;
      20. public User(String name) {
      21. this.name = name;
      22. }
      23. public String getName() {
      24. return name;
      25. }
      26. public void setName(String name) {
      27. this.name = name;
      28. }
      29. }

      结果如上图所示,发生GC后,弱引用根本没有回收,就是因为弱引用指向的对象被其他地方强引用,导致于在做筛选的过程中,被筛选出去了,不能去回收它。那么如果外部的这个强引用不释放,那么这个弱引用引用的对象和弱引用对象永远无法回收,从而无法达到弱引用的优势,变相地说,这就是内存泄漏~

      那么下面改进一下Java代码。

      1. public class ReferenceTest {
      2. public static void main(String[] args) {
      3. WeakHashMap weakHashMap = new WeakHashMap<>();
      4. // 这里直接不让外部引用这个Object对象
      5. weakHashMap.put(new Object(),new User("lihayyds"));
      6. weakHashMap.put(new Object(),new User("lihayyds"));
      7. byte[] bytes1 = new byte[1024 * 1024 * 1024];
      8. byte[] bytes2 = new byte[1024 * 1024 * 1024];
      9. byte[] bytes3 = new byte[1024 * 1024 * 1024];
      10. byte[] bytes4 = new byte[1024 * 1024 * 1024];
      11. byte[] bytes5 = new byte[1024 * 1024 * 1024];
      12. // 手动Full GC。
      13. System.gc();
      14. // Reference Queue 处理后的大小,因为在size里面会去处理
      15. System.out.println("Reference Queue 处理后的大小为:"+weakHashMap.size());
      16. }
      17. }
      18. class User{
      19. String name;
      20. public User(String name) {
      21. this.name = name;
      22. }
      23. public String getName() {
      24. return name;
      25. }
      26. public void setName(String name) {
      27. this.name = name;
      28. }
      29. }

      如上图所示,弱引用引用的对象不让外部有强引用后,直接正常了,发生GC就回收了~

      总结:

      因为流程特别大,强关联GC回收部分,所以笔者只能竭尽所能,源码注释+总结+画图来尽量描述明白~

    46. 相关阅读:
      Jetson Nano TensorRT C++加速 YOLOV5,集成进qt项目中
      外包干了一个月,技术明显进步。。。。。
      1.0、什么是软件测试
      Typescript 笔记:初识Typescript
      开源在线客服系统源码微信小程序
      学习记录-----AXI Burst地址计算
      算法学习day10(贪心算法)
      elementui组件兼容移动端
      登录注册页面的模拟
      我在VScode学Java(Java方法method)
    47. 原文地址:https://blog.csdn.net/qq_43799161/article/details/134326181