美文网首页
以 ZGC 为例,谈一谈 JVM 是如何实现 Reference

以 ZGC 为例,谈一谈 JVM 是如何实现 Reference

作者: bin的技术小屋 | 来源:发表于2024-06-12 12:01 被阅读0次

    《以 ZGC 为例,谈一谈 JVM 是如何实现 Reference 语义的(上)》

    4. JVM 如何实现 Reference 语义

    在前面的几个小节中,笔者为大家全面且详细地介绍了 Reference 在各中间件以及 JDK 中的应用场景,相信大家现在对于在什么情况下该使用哪种具体的 Reference, JDK 又如何处理这些 Reference 的流程已经非常清晰了。

    但这还远远不够,因为我们一直还没触达到 Reference 的本质,而在经过前面三个小节的内容铺垫之后,笔者将会在本小节中,带着大家深入到 JVM 内部,去看看 JVM 到底是如何实现 PhantomReference,WeakReference,SoftReference 以及 FinalReference 相关语义的。

    在这个过程中,笔者会把之前在概念层面介绍的各种 Reference 语义,一一映射到源码级别的实现上,让大家从抽象层面再到具象层面彻底理解 Reference 的本质。大家对各种 Reference 的模糊理解,在后面的内容中都会得到清晰的解答。

    哈哈,铺垫了这么久,终于和标题开始挂上钩了

    但在本小节的内容开始之前,笔者想和大家明确两个概念,因为后面笔者不想在用大段的语言重复解释他们。JDK 对于引用类的设计层级有两层,PhantomReference,WeakReference,SoftReference 以及 FinalReference 都继承于 Reference 类中。

    image.png

    后续笔者将会用 Reference 这个概念来统称以上四种引用类型,除非遇到不同 Reference 的语义实现时,笔者才会特殊指明具体的 Reference 类型。而 Reference 所引用的普通 Java 对象,存放在 referent 字段中,后面我们将会用 referent 来统一指代被引用的 Java 对象。

    public abstract class Reference<T> {
      private T referent;
    }
    

    好了,在我们明确了 Reference 和 referent 的概念之后,笔者先向大家抛一个问题出来,大家可以先自己思考下。我们都知道,只要一个 Java 对象存在从 GcRoot 到它的强引用链,那么这个 Java 对象就会被 JVM 标记为 alive,本轮 GC 就不会回收它。

    这个强引用链是什么呢 ?其实就是 GC 根对象的所有非静态成员变量,而这些非静态的成员变量也会引用到其他 Java 对象,这些 Java 对象也会有自己的非静态成员变量,这些成员变量又会引用到其他 Java 对象,这就慢慢形成了从 GC 根对象出发的有向引用关系图,这个引用关系图就是强引用链。

    image.png

    如果按照这种思路的话,那么从本质上来说 Reference 类也是一个普通的 Java 类,它的实例也是一个普通的对象实例,referent 也是它的一个成员变量,按理说,JVM 也可以从 GcRoot 开始遍历到 Reference 对象,近而通过它的成员变量 referent 遍历到被它引用的普通 Java 对象。

    这里我们先不用考虑什么软引用,弱引用,虚引用的概念,我们只从本质上来说,JVM 是不是也可以通过这条引用链将 referent 标记为 alive 呢 ?那为什么在 GC 的时候,这个 referent 就被当做垃圾回收了呢 ?

    image.png

    这里笔者先以 WeakReference 为例说明,事实上 JVM 对于 PhantomReference,WeakReference,SoftReference 以及 FinalReference 的处理总体上都是一样的,只不过对于 PhantomReference,SoftReference,FinalReference 会进行一些小小的特殊处理,这个笔者后面会放到单独的小节中讨论。我们先以 WeakReference 来说明 JVM 对于 Reference 的总体处理流程。

    要明白这个问题,我们就需要弄明白 JVM 在 GC 的时候是如何遍历对象的引用关系图的,在处理普通 Java 对象的引用关系时和处理 Reference 对象的引用关系时有何不同 ?

    4.1 JVM 如何遍历对象的引用关系图

    这里笔者要再次提醒大家,现在请你立刻,马上忘掉脑海中关于软引用,弱引用,虚引用的所有概念,让我们回归 Reference 类的本质,它其实就是一个普通的 Java 类,Reference 相关的实例就是一个普通的 Java 对象。

    image.png

    看透了这一层,剩下的就好办了,现在这些所有的问题最终汇结成了 —— JVM 如何遍历普通 Java 对象的引用关系图。我们先从一个简单的例子开始~~

    JVM 从 GcRoot 开始遍历,期间遇到的每一个对象,在 JVM 看来就是活跃的,当 GC 线程遍历到一个对象时,就会将这个对象标记为 alive,然后在看这个对象的非静态成员变量引用了哪些对象,顺藤摸瓜,沿着这些成员变量所引用的对象继续标记 alive,直到所有的引用关系图被遍历完。

    现在问题的关键就是 JVM 如何找到对象中的这些成员变量,而对象的本质其实就是一段内存,当我们通过 new 关键字分配对象的时候,JVM 首先会为这个对象分配一段内存,然后根据 Java 的对象模型初始化这段内存。下图中展示的就是 Java 对象的内存模型,其中存放了对象的 MarkWord , 类型信息,以及实例数据(下图中的蓝色区域)。

    Java对象的内存布局.png

    对象内存模型中的实例数据区中包含了对象中的基本类型字段还有引用类型字段,当 JVM 遍历到一个对象实例时,这个实例所占用的内存地址是不是就知道了,知道了对象所占内存的地址,那么实例数据区的地址也就知道了。

    JVM 近而可以遍历这段实例数据内存区域,如果发现是基本类型的字段就跳过,如果是引用类型的字段,那么这就是我们要找的成员变量,该成员变量引用的对象地址是不是就知道了,最后根据引用类型的成员变量指向的对象地址,找到被引用的对象,然后标记为 alive,最后再次从这个对象出发,循环上述逻辑,慢慢的就找到了所有存活的对象。

    当然了这只是一种可行的方案,实际上 JVM 并不会这么做,因为这样效率太低了,系统中有成千上万的对象实例,JVM 不可能每遍历一个对象,就到对象内存中的实例数据区去挨个寻找引用类型的成员变量。

    那么有没有一种索引结构来提前记录对象中究竟有哪些引用类型的成员变量,并且这些成员变量在对象内存中的位置偏移呢 ?这个索引结构就是 JVM 中的 OopMapBlock 。

    // Describes where oops are located in instances of this klass.
    class OopMapBlock {
     public:
      // Byte offset of the first oop mapped by this block.
      int offset() const          { return _offset; }
      void set_offset(int offset) { _offset = offset; }
    
      // Number of oops in this block.
      uint count() const         { return _count; }
      void set_count(uint count) { _count = count; }
    
     private:
      int  _offset;
      uint _count;
    };
    

    OopMapBlock 结构用于描述对象中定义的那些引用类型的非静态成员变量在对象内存中的偏移位置,每个 OopMapBlock 中包含多个非静态成员变量的地址偏移索引,而且这些非静态成员变量的地址必须是连续的。

    什么意思呢 ? 比如我们在写代码的时候,在一个类中连续的定义了多个非静态成员变量(引用类型),那么这些成员变量的地址偏移就被封装到了一个 OopMapBlock 中。

    但如果我们在类中定义成员变量的时候,中间插入了一个基本类型的成员变量,这么原本连续的引用类型的成员变量就不连续了,被分割成了两段。JVM 就会用两个 OopMapBlock 来索引他们的地址偏移。

    除了由于被基本类型的成员变量分割而导致产生多个 OopMapBlock 之外,在对象类型的父类中也可能会定义非静态成员变量,父类中也会有多个 OopMapBlock。

    image.png

    OopMapBlock 结构中的 _offset 用来指定第一个非静态引用类型的成员变量在对象内存地址中的偏移。_count 表示 OopMapBlock 中地址连续的非静态成员变量个数。

    另外还有字段与字段之间的字节填充,由于字节填充造成字段之间的地址不连续,也会产生多个 OopMapBlock。但是每个 OopMapBlock 中封装的非静态成员变量地址一定是连续的。

    这样一来,一个 Java 类型在 JVM 中就会拥有多个 OopMapBlock,这些 OopMapBlock 被组织在一个叫做 nonstatic_oop_maps 的数组中,当 JVM 遍历到一个对象实例时,如果能找到这个 nonstatic_oop_maps,近而通过数组中的这些 OopMapBlock 是不是就能立马将对象实例中的所有非静态成员变量都找出来了。

    好了,现在又有一个新的问题摆在我们面前了,这个 nonstatic_oop_maps 数组存放在哪里 ?JVM 如何找到这个 nonstatic_oop_maps ?

    从 JVM 对于 Java 类型的设计层面来讲,nonstatic_oop_maps 属于 Java 类的元信息,我们比较熟悉的是,在 JDK 层面每一个 Java 类都会对应一个 Class 对象来描述 Java 类的元信息,而 JDK 层面上的这个 Class 对象,对应于 JVM 层面来说就是 InstanceKlass 实例。

    JVM 在加载 Java 类的时候会为 Java 类构建 nonstatic_oop_maps,类加载完成之后,JVM 会为 Java 类创建一个 InstanceKlass 实例,nonstatic_oop_maps 就放在这个 InstanceKlass 实例里。

    从 InstanceKlass 实例的内存布局中我们可以看出,nonstatic_oop_maps 是紧挨在 vtable,itable 之后的。

    InstanceKlass* InstanceKlass::allocate_instance_klass(const ClassFileParser& parser, TRAPS) {
      // InstanceKlass 实例的内存布局
      const int size = InstanceKlass::size(parser.vtable_size(),
                                           parser.itable_size(),
                                           nonstatic_oop_map_size(parser.total_oop_map_count()),
                                           parser.is_interface());
    
      const Symbol* const class_name = parser.class_name();
      assert(class_name != NULL, "invariant");
      ClassLoaderData* loader_data = parser.loader_data();
      assert(loader_data != NULL, "invariant");
    
      InstanceKlass* ik;
    
      // Allocation
      if (REF_NONE == parser.reference_type()) {
        if (class_name == vmSymbols::java_lang_Class()) {
              ...... 省略 .... 
        }
        else if (is_class_loader(class_name, parser)) {
              ...... 省略 .... 
        } else {
          // 对于普通的 Java 类来说,这里创建的是 InstanceKlass 实例
          ik = new (loader_data, size, THREAD) InstanceKlass(parser, InstanceKlass::_kind_other);
        }
      } else {
        // 对于 Reference 类来说,这里创建的是 InstanceRefKlass 实例
        ik = new (loader_data, size, THREAD) InstanceRefKlass(parser);
      }
      return ik;
    }
    

    对于普通的 Java 类型来说,这里创建的是 InstanceKlass 实例,对于 Reference 类型来说,这里创建的是 InstanceRefKlass 实例,大家要牢记这一点。

    了解了 InstanceKlass 实例的内存布局之后,获取 nonstatic_oop_maps 数组就很简单了,我们只需要跳过 vtable 和 itable 就得到了 nonstatic_oop_maps 的起始内存地址。

    inline OopMapBlock* InstanceKlass::start_of_nonstatic_oop_maps() const {
      return (OopMapBlock*)(start_of_itable() + itable_length());
    }
    
    inline intptr_t* InstanceKlass::start_of_itable()   const { return (intptr_t*)start_of_vtable() + vtable_length(); }
    

    但 JVM 遍历的是 Java 对象,那如何通过 Java 对象获取到其对应在 JVM 中的 InstanceKlass 呢 ? 这就用到了前面笔者提到的 Java 对象的内存模型,在 JVM 中用 oopDesc 结构来描述 Java 对象的内存模型。

    class oopDesc {
     private:
      volatile markWord _mark;
      union _metadata {
        Klass*      _klass;
        narrowKlass _compressed_klass;
      } _metadata;
    }
    

    我们在 Java 层面见到的对象,对应于 JVM 层面就是一个 oopDesc 实例,JVM 直接处理的就是这个 oopDesc 实例,在 oopDesc 中有一个 _klass 类型指针,指向的就是 Java 对象所属的 Java 类在 JVM 层面上对应的 InstanceKlass 实例,

    我们可以通过 klass() 函数来获取 oopDesc 中的 _klass:

    Klass* oopDesc::klass() const {
      if (UseCompressedClassPointers) {
        // 开启压缩指针的情况
        return CompressedKlassPointers::decode_not_null(_metadata._compressed_klass);
      } else {
        return _metadata._klass;
      }
    }
    

    当 JVM 遍历到一个 oop (JVM 层面的 Java 对象)时,首先会将它标记位 alive,随后通过 klass() 函数获取它对应的 InstanceKlass 实例。

    template <typename OopClosureType>
    void oopDesc::oop_iterate(OopClosureType* cl) {
      // 遍历对象的所有非静态成员变量
      OopIteratorClosureDispatch::oop_oop_iterate(cl, this, klass());
    }
    

    JVM 遍历对象引用关系图的核心逻辑就封装在 InstanceKlass 中的 oop_oop_iterate_oop_maps 方法中:

    template <typename T, class OopClosureType>
    ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_maps(oop obj, OopClosureType* closure) {
      // InstanceKlass 中有多个 OopMapBlock,它们在 InstanceKlass 实例内存中会放在一起
      // 获取首个 OopMapBlock 地址
      OopMapBlock* map           = start_of_nonstatic_oop_maps();
      // 获取 InstanceKlass 中包含的 OopMapBlock 个数,这些都是在类加载的时候决定的
      // class 文件中有字段表,在类加载的时候可以根据字段表建立 OopMapBlock
      OopMapBlock* const end_map = map + nonstatic_oop_map_count();
      // 挨个遍历 InstanceKlass 中所有的 OopMapBlock
      for (; map < end_map; ++map) {
        // OopMapBlock 中包含的是 java 类中非静态成员变量在对象地址中的偏移
        // 通过它直接可以获取到成员变量的指针
        oop_oop_iterate_oop_map<T>(map, obj, closure);
      }
    }
    

    在这里首先会通过 start_of_nonstatic_oop_maps 在 InstanceKlass 实例中获取 nonstatic_oop_maps 数组的起始地址 map,然后根据 nonstatic_oop_maps 中包含的 OopMapBlock 个数,获取最后一个 OopMapBlock 的地址 end_map

    根据这些 OopMapBlock 索引好的非静态成员变量地址偏移,挨个获取这些成员变量的地址,并通过 do_oop 逐个进行标记。

    template <typename T, class OopClosureType>
    ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_map(OopMapBlock* map, oop obj, OopClosureType* closure) {
      // 通过成员变量在 obj 对象内存中的偏移获取成员变量指针
      T* p         = (T*)obj->obj_field_addr<T>(map->offset());
      // 获取该 OopMapBlock 所映射的成员变量个数
      T* const end = p + map->count();
      // 遍历成员变量挨个标记
      for (; p < end; ++p) {
        // 标记成员变量
        Devirtualizer::do_oop(closure, p);
      }
    }
    

    以上就是 JVM 如何遍历对象引用关系图的所有核心逻辑,主要依靠的就是这个 OopMapBlock,那么它是在什么时候被 JVM 构建出来的呢 ?

    4.2 OopMapBlock 在何时 ?又是如何 ?被 JVM 构建出来

    OopMapBlock 构建了 Java 类中定义的非静态成员变量在对象实例中的偏移地址索引,这些都属于类的元信息,在类加载的时候都可以确定下来。所以 nonstatic_oop_maps 自然也是在类加载的时候被构建出来的。

    Java 类的元信息存放在 .class 文件中,.class 文件是由 Java 编译器从 .java 文件中编译而来。.class 文件中存放的是字节码,本质上是一个具有特定二进制格式的二进制流。里面将 Java 类的元信息按照特定格式组织在一起。

    Java 类加载的过程就是 JVM 将 .class 文件中的二进制流加载到内存,并对字节流中的数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

    .class 文件中的二进制字节流可以来自于 JAR 包,也可以从数据库中读取,也可以通过动态代理在程序运行时生成。不管 .class 文件中的二进制字节流是从哪里获取的,最终都是通过 ClassLoader 的 native 方法 defineClass1 加载这些二进制字节流到内存中并生成代表该类的 java.lang.Class 对象,作为这个类在 Metaspace 中的数据访问入口。

    public abstract class ClassLoader {
        static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
                                            ProtectionDomain pd, String source);
    }
    

    在 defineClass1 的 native 实现中,会调用到一个 SystemDictionary::resolve_from_stream 方法,在这里会完成类的加载,验证,解析等操作,在完成类的解析之后就会构建 nonstatic_oop_maps,创建 InstanceKlass 实例,最后将 nonstatic_oop_maps 填充到 InstanceKlass 实例中。

    static jclass jvm_define_class_common(const char *name,
                                          jobject loader, const jbyte *buf,
                                          jsize len, jobject pd, const char *source,
                                          TRAPS) {
      // 将 class 文件的二进制字节流转换为 ClassFileStream
      ClassFileStream st((u1*)buf, len, source, ClassFileStream::verify);
      // 进行类的加载,验证,解析,随后为创建 InstanceKlass 实例
      Klass* k = SystemDictionary::resolve_from_stream(&st, class_name,
                                                       class_loader,
                                                       cl_info,
                                                       CHECK_NULL);
    }
    
    InstanceKlass* SystemDictionary::resolve_class_from_stream
      if (k == NULL) {
        k = KlassFactory::create_from_stream(st, class_name, loader_data, cl_info, CHECK_NULL);
      }
    }
    

    类的加载逻辑主要在 KlassFactory::create_from_stream 中进行:

    InstanceKlass* KlassFactory::create_from_stream(ClassFileStream* stream,
                                                    Symbol* name,
                                                    ClassLoaderData* loader_data,
                                                    const ClassLoadInfo& cl_info,
                                                    TRAPS) {
      // 这里完成类的加载,验证,解析 以及 nonstatic_oop_maps 的构建
      ClassFileParser parser(stream,
                             name,
                             loader_data,
                             &cl_info,
                             ClassFileParser::BROADCAST, // publicity level
                             CHECK_NULL);
      // 分配 InstanceKlass 实例,并将构建好的 nonstatic_oop_maps 填充到 InstanceKlass 实例中
      InstanceKlass* result = parser.create_instance_klass(old_stream != stream, *cl_inst_info, CHECK_NULL);
      return result;
    }
    

    nonstatic_oop_maps 的构建主要是在类解析阶段之后,由 post_process_parsed_stream 函数负责触发构建。

    ClassFileParser::ClassFileParser(ClassFileStream* stream,
                                     Symbol* name,
                                     ClassLoaderData* loader_data,
                                     const ClassLoadInfo* cl_info,
                                     Publicity pub_level,
                                     TRAPS) :
    
      ........ 省略 加载,验证,解析逻辑 ......
    
      // 这里会构建 nonstatic_oop_maps
      post_process_parsed_stream(stream, _cp, CHECK);
    }
    

    在 post_process_parsed_stream 函数中会对 Java 类中定义的所有字段进行布局:

    void ClassFileParser::post_process_parsed_stream(
      ..... 省略 .....
    
      // 对 Java 类中的字段信息进行布局
      _field_info = new FieldLayoutInfo();
      FieldLayoutBuilder lb(class_name(), super_klass(), _cp, _fields,
                            _parsed_annotations->is_contended(), _field_info);
      // 构建 nonstatic_oop_maps
      lb.build_layout();
    }
    
    void FieldLayoutBuilder::build_layout() {
      // 在对类中的字段完成布局之后,会调用一个 epilogue() 函数
      compute_regular_layout();
    }
    
    // nonstatic_oop_maps的构建逻辑就在这里
    void FieldLayoutBuilder::epilogue() {
    

    在字段布局完成之后,会在 epilogue() 函数中按照字段的布局信息,构建 nonstatic_oop_maps。

    1. 首先继承来自其父类的 nonstatic_oop_maps。

    2. _root_group->oop_fields() 中获取类中的所有非静态成员变量,并将相邻的成员变量构建在同一个 OopMapBlock 中

    3. 处理被 @Contended 标注过的非静态成员变量,属于同一个 content group 的成员变量在对象实例内存中必须连续存放,独占 CPU 缓存行。所以同一个 content group 下的成员变量会被构建在同一个 OopMapBlock 中。

    void FieldLayoutBuilder::epilogue() {
      // 开始构建 nonstatic_oop_maps
      int super_oop_map_count = (_super_klass == NULL) ? 0 :_super_klass->nonstatic_oop_map_count();
      int max_oop_map_count = super_oop_map_count + _nonstatic_oopmap_count;
    
      OopMapBlocksBuilder* nonstatic_oop_maps =
          new OopMapBlocksBuilder(max_oop_map_count);
      // 继承父类的 nonstatic_oop_maps
      if (super_oop_map_count > 0) {
        nonstatic_oop_maps->initialize_inherited_blocks(_super_klass->start_of_nonstatic_oop_maps(),
        _super_klass->nonstatic_oop_map_count());
      }
    
      // 为非静态成员变量构建 nonstatic_oop_maps
      if (_root_group->oop_fields() != NULL) {
        for (int i = 0; i < _root_group->oop_fields()->length(); i++) {
          LayoutRawBlock* b = _root_group->oop_fields()->at(i);
          // 构建 OopMapBlock,相邻的字段构建在一个 OopMapBlock 中
          // 不相邻的字段分别构建在不同的 OopMapBlock 中
          nonstatic_oop_maps->add(b->offset(), 1);
        }
      }
    
      // 为 @Contended 标注的非静态成员变量构建 nonstatic_oop_maps
      // 在静态成员变量上标注 @Contended 将会被忽略
      if (!_contended_groups.is_empty()) {
        for (int i = 0; i < _contended_groups.length(); i++) {
          FieldGroup* cg = _contended_groups.at(i);
          if (cg->oop_count() > 0) {
            assert(cg->oop_fields() != NULL && cg->oop_fields()->at(0) != NULL, "oop_count > 0 but no oop fields found");
            // 构建 OopMapBlock,属于同一个 contended_groups 的成员变量在内存中要放在一起
            nonstatic_oop_maps->add(cg->oop_fields()->at(0)->offset(), cg->oop_count());
          }
        }
      }
      // 对相邻的  OopMapBlock 进行排序整理
      // 确保在内存中相邻排列的非静态成员变量被构建在一个 OopMapBlock 中
      nonstatic_oop_maps->compact();
    
      int nonstatic_field_end = align_up(_layout->last_block()->offset(), heapOopSize);
    
      // Pass back information needed for InstanceKlass creation
      _info->oop_map_blocks = nonstatic_oop_maps;
      _info->_nonstatic_field_size = (nonstatic_field_end - instanceOopDesc::base_offset_in_bytes()) / heapOopSize;
      _info->_has_nonstatic_fields = _has_nonstatic_fields;
    }
    

    JVM 在完成类的加载,解析,以及构建完 nonstatic_oop_maps 之后,就会为 Java 类在 JVM 中分配一个 InstanceKlass 实例。

    InstanceKlass* ClassFileParser::create_instance_klass(bool changed_by_loadhook,
                                                          const ClassInstanceInfo& cl_inst_info,
                                                          TRAPS) {
      if (_klass != NULL) {
        return _klass;
      }
      // 分配 InstanceKlass 实例
      InstanceKlass* const ik =
        InstanceKlass::allocate_instance_klass(*this, CHECK_NULL);
      // 填充 InstanceKlass 实例
      fill_instance_klass(ik, changed_by_loadhook, cl_inst_info, CHECK_NULL);
    
      return ik;
    }
    

    InstanceKlass::allocate_instance_klass 只是分配了一个空的 InstanceKlass 实例 ,所以需要 fill_instance_klass 函数来填充 InstanceKlass 实例。在这里会将刚刚构建好的 nonstatic_oop_maps 填充到 InstanceKlass 实例中。

    void ClassFileParser::fill_instance_klass(InstanceKlass* ik,
                                              bool changed_by_loadhook,
                                              const ClassInstanceInfo& cl_inst_info,
                                              TRAPS) {
    
      ik->set_nonstatic_field_size(_field_info->_nonstatic_field_size);
      ik->set_has_nonstatic_fields(_field_info->_has_nonstatic_fields);
      assert(_fac != NULL, "invariant");
      ik->set_static_oop_field_count(_fac->count[STATIC_OOP]);
    
      // 将构建好的 nonstatic_oop_maps 填充到 InstanceKlass 实例中
      OopMapBlocksBuilder* oop_map_blocks = _field_info->oop_map_blocks;
      if (oop_map_blocks->_nonstatic_oop_map_count > 0) {
        oop_map_blocks->copy(ik->start_of_nonstatic_oop_maps());
      }
    }
    

    好了,现在我们已经清楚了 JVM 如何利用这个 nonstatic_oop_maps 来高效的遍历对象的引用关系图,并且也知道了 JVM 在何时 ?又是如何 ? 将 nonstatic_oop_maps 创建出来并填充到 InstanceKlass 实例中。

    有了这些背景知识的铺垫之后,我们再来看 Reference 语义的实现逻辑就很简单了。

    4.3 Reference 类型的 OopMapBlock 有何不同

    前面我们提到,当 JVM 遍历到一个对象并将其标记为 alive 之后,随后就会从这个普通 Java 对象的内存模型中将 _klass 指针取出来,对于普通的 Java 类型来说,它的 _klass 指针指向的是一个 InstanceKlass 实例,而对于 Reference 类型来说,它的 _klass 指针指向的是一个 InstanceRefKlass 实例。

    随后会从 InstanceKlass 实例中将 nonstatic_oop_maps 数组取出来,这个 nonstatic_oop_maps 是在类加载的时候被创建并填充到 InstanceKlass 实例中的。

    根据 nonstatic_oop_maps 中构建的类中所有非静态成员变量在对象内存中的地址偏移,JVM 可以轻松的获取到对象中成员变量的地址,顺藤摸瓜,再将这些非静态成员变量引用到的对象全部标记为 alive,反复循环这个逻辑,最终会将整个引用关系图遍历标记完毕。

    image.png

    但是别忘了 Reference 类型本质上也是一个 Java 类,referent 也是 Reference 类中定义的一个非静态成员变量。

    public abstract class Reference<T> {
      private T referent;
      volatile ReferenceQueue<? super T> queue;
      volatile Reference next;
      private transient Reference<?> discovered;
    }
    

    如果按照这个逻辑,JVM 是不是也可以通过 nonstatic_oop_maps 获取到 referent 的内存地址 ,近而将 Reference 引用的对象标记为 alive 呢 ?但是现实是,这个 referent 并没有被 JVM 标记到。

    image.png

    这就有点奇怪了是吧,JVM 是怎么做到的呢 ?Reference 的语义是如何实现的呢 ?

    我们从头来捋一捋,现在的现象是什么 ? 是 Reference 对象的非静态成员变量 referent 没有被标记到对吧。那么查找一个对象的非静态成员变量靠什么 ? 靠的是不是就是我们前面花了大量篇幅介绍的 OopMapBlock ?那这个 OopMapBlock 从哪里来的 ?对于普通对象是不是在它的 InstanceKlass 实例中,对于 Reference 类型的对象是不是在它的 InstanceRefKlass 实例 中 ?

    那为什么对于普通对象来说,可以通过 OopMapBlock 遍历到它的非静态成员变量,而对于 Reference 对象来说,就无法通过 OopMapBlock 遍历到它的 referent 呢 ?

    难道是 JVM 对于 InstanceRefKlass 的 nonstatic_oop_maps 进行了一系列的魔改,压根就没有为 referent 在 OopMapBlock 中建立索引 ?这样自然就不会遍历到 referent,也无法将它标记为 alive 了。

    事实上,JVM 就是这么干的,那么在哪里,又是如何对 InstanceRefKlass 进行魔改的呢 ?

    在 JVM 启动的时候会对 SystemDictionary 进行初始化,SystemDictionary 在 JVM 中的角色是用于管理系统中已经加载的所有 class 类,在 SystemDictionary 初始化的时候会调用到一个重要的函数 InstanceRefKlass::update_nonstatic_oop_maps

    void SystemDictionary::initialize(TRAPS) {
      // Resolve basic classes
      vmClasses::resolve_all(CHECK);
    }
    
    void vmClasses::resolve_all(TRAPS) {
        InstanceRefKlass::update_nonstatic_oop_maps(vmClasses::Reference_klass());
    }
    

    从函数命名上,我们就可以看出来,这里就是对 InstanceRefKlass 进行魔改的地方了。

    void InstanceRefKlass::update_nonstatic_oop_maps(Klass* k) {
    
      // Reference 类中的 referent 字段和 discovered 字段的索引偏移从 OopMapBlock 中清除掉
      // 在后面通过 Reference 遍历标记成员变量的时候不需要遍历标记这两个字段
      InstanceKlass* ik = InstanceKlass::cast(k);
    
      OopMapBlock* map = ik->start_of_nonstatic_oop_maps();
    
      // Updated map starts at "queue", covers "queue" and "next".
      const int new_offset = java_lang_ref_Reference::queue_offset();
      const unsigned int new_count = 2; // queue and next
    
       assert(map->offset() == referent_offset, "just checking");
       assert(map->count() == count, "just checking");
       map->set_offset(new_offset);
       map->set_count(new_count);
    }
    

    在 JVM 启动的时候会对所有基础类进行加载当然也包含 Reference 类,和普通的 Java 类型一样,Reference 类被加载之后,JVM 也会为它构建一个全量的 nonstatic_oop_maps,里面确实也包含了所有的非静态成员变量(referent 字段也包括在内)。

    随后就会在 update_nonstatic_oop_maps 中对 InstanceRefKlass 进行魔改。

    public abstract class Reference<T> {
      private T referent;
      volatile ReferenceQueue<? super T> queue;
      volatile Reference next;
      private transient Reference<?> discovered;
    }
    

    首先会通过 java_lang_ref_Reference::queue_offset() 将成员变量 queue 的地址偏移取出来 —— new_offset,然后将原来 OopMapBlock 的 _count 设置为 2 ,用新的 new_offset,new_count 重新构建 OopMapBlock。

    这里 new_count 设置为 2 的意思就是,只将 Reference 类中的非静态成员变量 queue 和 next 构建到 OopMapBlock 中。

    也就是说,当 JVM 遍历到一个 Reference 对象时,只能通过它的 OopMapBlock 遍历到 queue 和 next,无法遍历到 referent 和 discovered。

    经过这样的魔改之后,JVM 就巧妙地实现了 Reference 的语义。大家这里可以停下来回想回想 WeakReference 的语义,是不是就实现了当一个 Java 对象只存在一条弱引用链的时候,发生 GC 的时候,只被弱引用所关联的对象就会被回收掉。本质原因就是这个被 JVM 魔改之后的 OopMapBlock 产生了作用。

    有同学可能会问了,你说的只是 WeakReference 的语义啊,Reference 又不只是 WeakReference 这一种,还有 SoftReference,PhantomReference,FinalReference 这些 Reference 类型,好像在这一小节中并没有看到他们的语义实现。

    事实上,笔者在这一小节中只是为大家揭露 Reference 最为本质的面貌,SoftReference,PhantomReference,FinalReference 这些具体的语义都是在 WeakReference 语义的基础上进行了小小的魔改而已,等笔者把该铺垫的背景知识全部铺垫好,后面会有单独的小节专门为大家解释清楚其他 Reference 类型的语义实现。

    5. JVM 在 GC 的时候如何处理 Reference

    在本文的第三小节中,我们主要在 JVM 的外围来讨论 JDK 如何通过 ReferenceHandler 线程来处理 Reference 对象,其中提到 JVM 内部有一个非常重要的 _reference_pending_list 链表,当 Reference 的 referent 对象没有任何强引用链或者软引用链可达时,GC 线程就会回收这个 referent 对象。那么与之对应的 Reference 对象就会被 JVM 采用头插法的方式插入到这个 _reference_pending_list 中。

    // zReferenceProcessor.cpp 文件
    OopHandle Universe::_reference_pending_list;
    
    // Create a handle for reference_pending_list
     _reference_pending_list = OopHandle(vm_global(), NULL);
    

    如果 _reference_pending_list 中没有任何需要被处理的 Reference 对象时,ReferenceHandler 线程就会在一个 native 方法 —— waitForReferencePendingList() 上阻塞等待。

    当发生 GC 的时候,JVM 就会从 GcRoot 开始挨个遍历整个引用关系图中的对象,并将遍历到的对象标记为 alive,没有被标记到的对象就会被 JVM 当做垃圾回收掉。

    当 referent 对象没有被标记到,需要被 GC 线程回收的时候,JVM 就会将与它关联的 Reference 插入到 _reference_pending_list 中,并唤醒 ReferenceHandler 线程去处理,后面的内容我们在第三小节中已经详细的讨论过了。

    image.png

    本小节中,笔者将带着大家深入到 JVM 内部,看看发生 GC 的时候,JVM 如何处理这些 Reference 对象 ? 如何判断哪些 Reference 需要被插入到 _reference_pending_list 中? 和我们前面第三小节中的内容遥相呼应起来,这样一来我们就从 JDK 层面再到 JVM 层面将整个 Reference 的处理链路打通了。

    下面笔者就以 ZGC 为例,带着大家看一看 JVM 内部到底是如何处理 Reference 的 :

    void ZDriver::gc(const ZDriverRequest& request) {
      ZDriverGCScope scope(request);
    
      // Phase 1: Pause Mark Start
      // 初始化 gc 相关的统计信息,清空 object alocator 的缓存页,切换地址视图,设置标记条带个数
      pause_mark_start();
    
      // Phase 2: Concurrent Mark
      // 标记 gc root, 标记普通对象,以及 Reference 对象
      // 经过主动刷新,被动刷新之后,如果标记栈中还有对象,也不会再进行标记了
      // 剩下的对象标记任务放到 pause_mark_end 中 STW 阶段执行
      concurrent(mark);
    
      // Phase 3: Pause Mark End 再标记阶段,标记上一阶段剩下的对象
      // zgc 低延迟的精髓,如果 1ms 内结束不了 STW 标记,那么就在发起一轮 concurrent 标记
      // 目的是降低应用线程的停顿控制在 1ms 以内
      while (!pause_mark_end()) {
        // 1ms 内没有标记完应用线程本地标记栈的内容,那么就重新开始一轮并发标记。
        // Phase 3.5: Concurrent Mark Continue
        concurrent(mark_continue);
      }
    
      // Phase 4: Concurrent Mark Free
      // 释放标记栈资源
      concurrent(mark_free);
    
      // Phase 5: Concurrent Process Non-Strong References
      // 这里就是本小节讨论的重点
      concurrent(process_non_strong_references);
    
      ....... 省略 .......
    }
    

    ZGC 整个 GC 过程分为 10 个阶段,其中只有四个阶段需要非常短暂的 STW,剩下的六个阶段全部是与 Java 应用线程并发执行的,阶段虽然比较多,整个 GC 过程也非常的复杂,但与本小节相关的阶段只有两个,分别是第二阶段的并发标记阶段 —— Phase 2: Concurrent Mark,与第五阶段的并发处理非强引用 Reference 阶段 —— Phase 5: Concurrent Process Non-Strong References

    其中 Concurrent Mark 主要的任务就是从 GcRoot 开始并发标记根对象,并沿着根对象遍历整个堆中的引用关系,在整个遍历的过程中会逐渐发现那些需要被 ReferenceHandler 线程处理的 Reference 对象,随后会将这些 Reference 对象插入到 _discovered_list 中。

    这里大家可能会有疑问,你刚才不是说 JVM 会将需要被处理的 Reference 对象插入到 _reference_pending_list 中吗 ?怎么现在又变成 _discovered_list 了 ?

    事实上,大家可以将 _discovered_list 理解为一个临时的 _reference_pending_list,在 ZGC 的整个过程中会用到两个临时的 _reference_pending_list,它们分别是 _discovered_list,_pending_list。

    class ZReferenceProcessor : public ReferenceDiscoverer {
      ZPerWorker<oop>      _discovered_list;
      ZContended<oop>      _pending_list;
    }
    

    ZGC 有多个 GC 线程负责并发执行垃圾回收任务,_discovered_list 是 ZPerWorker 类型的,每一个 GC 线程都有一个 _discovered_list,负责临时存储由该 GC 线程在并发标记过程中发现的 Reference 对象。

    在并发标记结束之后,这些 GC 线程就会将各自在 _discovered_list 中收集到的 Reference 对象统一转移到 _pending_list 中,_pending_list 在所有 GC 线程中是共享的,负责汇总 ZGC 线程收集到的所有 Reference 对象。

    Concurrent Process Non-Strong References 阶段的最后,JVM 会将 _pending_list 中汇总的 Reference 对象再次统一转移到 _reference_pending_list 中,_reference_pending_list 是最终对外的发布形态,ReferenceHandler 线程只会和 _reference_pending_list 打交道。

    理解了这个背景,下面我们就来一起看下 Concurrent Mark 阶段是如何发现 Reference 对象的

    5.1 Concurrent Mark

    当 ZGC 遍历到一个对象 —— oop obj 并将其标记为 alive 之后,就会调用 follow_object 方法,来遍历 obj 的所有非静态成员变量,然后将这些成员变量所引用的 obj 标记为 alive,然后再次调用 follow_object 继续遍历引用关系图,这样循环往复。 ZGC 就是靠着这个 follow_object 方法驱动着所有 GC 线程去遍历整个堆的引用关系图。

    void ZMark::follow_object(oop obj, bool finalizable) {
      if (finalizable) {
        ZMarkBarrierOopClosure<true /* finalizable */> cl;
        obj->oop_iterate(&cl);
      } else {
        // 最终的标记逻辑是在这个闭包中完成的
        ZMarkBarrierOopClosure<false /* finalizable */> cl;
        // 遍历标记 obj 的所有非静态成员变量
        obj->oop_iterate(&cl);
      }
    }
    

    这里就来到了笔者在第四小节中介绍的内容,这个函数熟悉吗 ?没印象的话再去回顾下第四小节。

    template <typename OopClosureType>
    void oopDesc::oop_iterate(OopClosureType* cl) {
      OopIteratorClosureDispatch::oop_oop_iterate(cl, this, klass());
    }
    

    首先会通过 klass() 函数去获取 obj 中的 _klass 指针,对于普通类型的 Java 对象来说,_klass 指向的是 InstanceKlass 实例,对于 Reference 类型的对象来说,_klass 指向的是 InstanceRefKlass 实例。

    最终的遍历动作是在对应 Klass 中的 oop_oop_iterate 方法中进行的,本小节我们重点关注 InstanceRefKlass。

    template <typename T, class OopClosureType>
    void InstanceRefKlass::oop_oop_iterate(oop obj, OopClosureType* closure) {
      // 遍历 Reference 对象的非静态成员变量,注意这里 referent 字段和 discovered 字段是不会被遍历到的
      InstanceKlass::oop_oop_iterate<T>(obj, closure);
      // 判断该 Reference 对象是否需要加入到 _discovered_list 中
      oop_oop_iterate_ref_processing<T>(obj, closure);
    }
    

    首先会调用 InstanceKlass::oop_oop_iterate 函数,这个函数熟悉吗 ?我们在第四小节中重点介绍的就是这个函数。

    在这个函数中获取 InstanceRefKlass 实例中的 nonstatic_oop_maps,通过 OopMapBlock 去遍历标记 Reference 对象非静态成员变量。

    template <typename T, class OopClosureType>
    ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_maps(oop obj, OopClosureType* closure) {
      OopMapBlock* map           = start_of_nonstatic_oop_maps();
      OopMapBlock* const end_map = map + nonstatic_oop_map_count();
    
      for (; map < end_map; ++map) {
        oop_oop_iterate_oop_map<T>(map, obj, closure);
      }
    }
    

    但笔者前面介绍过,InstanceRefKlass 中的 nonstatic_oop_maps 是被 JVM 经过特殊魔改的,这里并不会遍历到 Reference 对象的 referent 字段和 discovered 字段。

    public abstract class Reference<T> {
      private T referent;
      volatile ReferenceQueue<? super T> queue;
      volatile Reference next;
      private transient Reference<?> discovered;
    }
    

    在遍历标记完 Reference 对象的非静态成员变量之后,JVM 会调用
    oop_oop_iterate_ref_processing 来判断该 Reference 对象是否应该插入到 _discovered_list 中。

    template <typename T, class OopClosureType, class Contains>
    void InstanceRefKlass::oop_oop_iterate_ref_processing(oop obj, OopClosureType* closure, Contains& contains) {
      switch (closure->reference_iteration_mode()) {
        case OopIterateClosure::DO_DISCOVERY:
          // 执行这里的 discovery 逻辑,发现需要被处理的 Reference 对象
          oop_oop_iterate_discovery<T>(obj, reference_type(), closure, contains);
          break;
    
         ...... 省略 .....
      }
    }
    
    template <typename T, class OopClosureType, class Contains>
    void InstanceRefKlass::oop_oop_iterate_discovery(oop obj, ReferenceType type, OopClosureType* closure, Contains& contains) {
      // Try to discover reference and return if it succeeds.
      if (try_discover<T>(obj, type, closure)) {
        // 走到这里说明 Reference 对象已经被加入到 _discovered_list 中了
        // 加入到 _discovered_list 的条件是:
        // 1. referent 没有被标记,说明不活跃
        // 2. Reference 对象之前没有被添加到 _discovered_list(第一次添加)
        return;
      }
    
    }
    

    在 try_discover 中,JVM 首先会通过 load_referent 从堆中加载 Reference 引用的 referent 对象。这里会判断 referent 对象是否已经被 GC 线程标记过了,如果已经被标记了,说明 referent 是 alive 的,那么这个 Reference 对象就不需要被放入 _discovered_list 中,直接 return 掉。

    如果 referent 没有被标记,则进入 ZReferenceProcessor->discover_reference 函数中作进一步的 discover 逻辑判断。

    template <typename T, class OopClosureType>
    bool InstanceRefKlass::try_discover(oop obj, ReferenceType type, OopClosureType* closure) {
    
      //  ZReferenceProcessor
      ReferenceDiscoverer* rd = closure->ref_discoverer();
      if (rd != NULL) {
          // 从堆中加载 Reference 对象的 referent
        oop referent = load_referent(obj, type);
        if (referent != NULL) {
          if (!referent->is_gc_marked()) {
            // Only try to discover if not yet marked.
            // true 表示 reference 被加入到 discover-list 中了
            return rd->discover_reference(obj, type);
          }
        }
      }
      return false;
    }
    

    discover_reference 的逻辑很简单,主要分为两步:

    1. 通过 should_discover判断该 Reference 对象是否需要被 ReferenceHandler 线程处理

    2. 如果 Reference 对象需要被处理的话就通过 discover 方法,将其插入到 _discovered_list 中。

    bool ZReferenceProcessor::discover_reference(oop reference, ReferenceType type) {
    
      // true : 表示 referent 还存活(被强引用或者软引用关联),那么就不能放到 _discovered_list
      // false : 表示 referent 不在存活,那么就需要把 reference 放入 _discovered_list
      if (!should_discover(reference, type)) {
        // Not discovered
        return false;
      }
      // 将 reference 插入到  _discovered_list 中(头插法)
      discover(reference, type);
    
      // Discovered
      return true;
    }
    

    should_discover 判断是否将 Reference 添加到 _discovered_list 中的逻辑依据主要有三个方面:

    如果 Reference 对象的状态是 inactive,那么 JVM 就不会将它放入 _discovered_list 中。那么什么时候 Reference 对象会变为 inactive 呢 ?

    比如,应用线程自己调用 Reference.enqueue() 方法,自己亲自将 Reference 对象添加到与其关联的 ReferenceQueue 中等待进一步的处理。那么这里 JVM 就不需要将 Reference 添加到 _discovered_list 中了。

    因为最终 ReferenceHandler 线程还是会从 _reference_pending_list 中将 Reference 添加到 ReferenceQueue 中,这样一来就重复了。应用线程在调用 Reference.enqueue() 方法之后,Reference 的状态就变为了 inactive

    还有一种变为 inactive 的情况就是应用线程直接调用 Reference.clear() 方法,表示应用线程自己已经处理过 Reference 对象了,JVM 就别管了,此时 Reference 的状态变为 inactive , 那么在下一轮 GC 的时候该 Reference 对象就会被回收,并且不会再次被添加到 _discovered_list 中。

    这也就解释了为什么 Reference 状态变为 inactive 之后,JVM 将不会再次将其放入 _discovered_list 的原因了,因为它已经被处理过了。处于 inactive 状态的 Reference 有一个共同的特点就是它的 referent = null

    第二个条件是如果它的 referent 仍然存在强引用链,那么这个 Reference 将不会被放入 _discovered_list。

    第三个条件是如果它的 referent 仍然存在软引用链,也就是还被软引用所关联,如果此时内存充足,软引用不会被回收的话,那么这个 Reference 也不会被放入 _discovered_list。

    bool ZReferenceProcessor::should_discover(oop reference, ReferenceType type) const {
      // 获取 referent 对象的地址视图
      volatile oop* const referent_addr = reference_referent_addr(reference);
      // 调整 referent 对象的地址视图为 remapped + mark0 也就是 weakgood 视图
      // 表示该 referent 对象目前只能通过弱引用链访问到,而不能通过强引用链访问到
      // 注意这里是调整 referent 的视图而不是调整 Reference 的视图
      const oop referent = ZBarrier::weak_load_barrier_on_oop_field(referent_addr);
    
      // 此时 Reference 的状态就是 inactive,那么这里将不会重复将 Reference 添加到 _discovered_list 重复处理
      if (is_inactive(reference, referent, type)) {
        return false;
      }
      // referent 还被强引用关联,那么 return false 也就是说不能被加入到 discover list 中
      if (is_strongly_live(referent)) {
        return false;
      }
      // referent 还被软引用有效关联,那么 return false 也就是说不能被加入到 discover list 中
      if (is_softly_live(reference, type)) {
        return false;
      }
    
      return true;
    }
    

    如果 Reference 对象的 referent 在当前堆中已经没有任何强引用或者软引用了,并且该 Reference 对象不是 inactive 状态的,那么 JVM 就会将该 Reference 对象通过下面的 discover 方法插入到 _discovered_list 中(头插法)。

    void ZReferenceProcessor::discover(oop reference, ReferenceType type) {
      // Add reference to discovered list
      // 确保 reference 不在 _discovered_list 中,不能重复添加
      assert(reference_discovered(reference) == NULL, "Already discovered");
      oop* const list = _discovered_list.addr();
      // 头插法,reference->discovered = *list
      reference_set_discovered(reference, *list);
      // reference 变为 _discovered_list 的头部
      *list = reference;
    }
    

    从以上过程我们可以看出,在 ZGC 的 Concurrent Mark 阶段, Reference 对象被 JVM 添加到 _discovered_list 中需要同时符合下面四个条件:

    1. Reference 对象引用的 referent 没有被 GC 标记过。
    2. Reference 对象的状态不能是 inactive, 也就是说这个 Reference 还没有被应用线程处理过,Reference 之前没有加入过 _discovered_list。
    3. referent 不存在任何强引用链。
    4. referent 不存在任何软引用链。
    image.png

    好了,现在 Reference 在 Concurrent Mark 阶段的处理过程,笔者就为大家介绍完了,这里需要注意的是,目前 _discovered_list 中收集到的 Reference 都只是临时的,因为当前所处的阶段为并发标记阶段,应用线程和 GC 线程是并发执行的,再加上标记阶段还没有结束,所以 Reference 加入到 _discovered_list 的条件可能随时会被应用线程和 GC 线程再次改变。

    _discovered_list 终态的确定需要等到并发标记阶段完全结束,在 ZGC 的第五阶段 —— Concurrent Process Non-Strong References 进行最终的处理。

    5.2 Concurrent Process Non-Strong References

    void ZHeap::process_non_strong_references() {
      // Process Soft/Weak/Final/PhantomReferences
      _reference_processor.process_references();
      // Enqueue Soft/Weak/Final/PhantomReferences
      _reference_processor.enqueue_references();
    }
    

    ZGC 在 Concurrent Process Non-Strong References 阶段对于 Reference 的最终处理是在 ZReferenceProcessor 中完成的,其中主要包括两个核心步骤:

    首先在 process_references() 函数中,判断 ZGC 在 Concurrent Mark 阶段的 _discovered_list 中收集到的临时 Reference 对象所引用的 referent 是否存活,如果这些 referent 仍然存活,那么就需要将对应的 Reference 对象从 _discovered_list 中移除。

    如果这些 referent 不再存活,那么就将与其关联的 Reference 对象继续保留在 _discovered_list,最后将 _discovered_list 中依然保留的 Reference 对象添加到 _pending_list 中,然后清空 _discovered_list。

    第二个步骤就是在 enqueue_references() 函数中,将最终确定下来的 _pending_list 再次添加到 _reference_pending_list 中,随后唤醒 ReferenceHandler 线程去处理 _reference_pending_list 中的 Reference 对象,最后清空 _pending_list,为下一轮 GC 做准备。

    以上就是 ZGC 对于 Non-Strong References 的总体处理流程,下面我们就来看下这两个核心步骤中的具体处理细节:

        void ZReferenceProcessor::process_references() {
            // Process discovered lists
            ZReferenceProcessorTask task(this);
            _workers->run(&task);
        }
    

    process_references() 对于 _discovered_list 的处理逻辑被封装在一个 ZReferenceProcessorTask 中,由所有 GC 线程来一起并发执行这个 Task。

    void ZReferenceProcessor::work() {
      // Process discovered references
      oop* const list = _discovered_list.addr();
      oop* p = list;
    
      // 循环遍历 _discovered_list,检查之前收集到的 Reference 对象的 referent 是否存活
      while (*p != NULL) {
        const oop reference = *p;
        const ReferenceType type = reference_type(reference);
        // 如果该 reference 已经被应用程序处理过了 -> referent == NULL, 那么就不需要再被处理了,直接丢弃
        // 如果 referent 依然存活,那么也要丢弃,不能放入 _discovered_list 中
        if (should_drop(reference, type)) {
          // 如果 referent 是 alive 的或者在上一轮 GC 中已经被处理过,则将 reference 从 _discovered_list 中删除
          *p = drop(reference, type);
        } else {
          // 如果 referent 不是 alive 的,在并发标记阶段没有被标记,那么就让它继续留在 _discovered_list 中
          // 这里会调用 reference 的 clear 方法 -> referent 置为 null
          // 返回 reference 在 _discovered_list 中的下一个对象,继续 while 循环
          p = keep(reference, type);
        }
      }
    
      // 将 _discovered_list 原子地添加到 _pending_list 中
      if (*list != NULL) {
        *p = Atomic::xchg(_pending_list.addr(), *list);
        // 清空 _discovered_list
        *list = NULL;
      }
    }
    

    首先通过 _discovered_list.addr() 获取 GC 线程的本地 _discovered_list,前面我们提到 _discovered_list 是一个 ZPerWorker 类型的,每一个 GC 线程对应一个,用于在 Concurrent Mark 阶段并发 discover Reference。

    循环遍历 _discovered_list,挨个获取链表中收集到的临时 Reference,通过 should_drop 方法判断是否需要将 Reference 对象从 _discovered_list 中移除。移除条件有两个:

    1. 如果 Reference 对象的 referent 被置为 null , 那么就需要将这里的 Reference 对象移除掉。因为在 Reference 被放入到 _pending_list 之前,JVM 会主动调用 Reference 对象的 clear 方法,将 referent 置空。referent = null 代表的语义是这个 Reference 之前已经被添加到 _discovered_list 中了,比如在上一轮 GC 中就已经被处理了,本轮 GC 直接将 Reference 对象回收掉就好了,不需要再重复添加到 _discovered_list。

    2. 如果 Reference 对象在前几轮 GC 没有被处理过,是在本轮 GC 中新发现的,那么就继续判断它的 referent 是否还存活,如果仍然存活的话,就将 Reference 对象移除,因为 referent 还活着,自然也不需要被 ReferenceHandler 线程处理

    bool ZReferenceProcessor::should_drop(oop reference, ReferenceType type) const {
      // 获取 Reference 所引用的 referent
      const oop referent = reference_referent(reference);
      // 如果 Reference 对象在上一轮 GC 中被处理过或者已经被应用线程自己处理了,那么本轮 GC 直接回收掉
      // 不会再将 Reference 对象重复添加到 _discovered_list
      if (referent == NULL) {
        return true;
      }
      
      // 如果 referent 仍然存活,那么也会将 Reference 对象移除,不需要被 ReferenceHandler 线程处理
      if (type == REF_PHANTOM) {
        // 针对 PhantomReference 对象的特殊处理,后面在专门的小节中讲解,这里先忽略
        return ZBarrier::is_alive_barrier_on_phantom_oop(referent);
      } else {
        // 本小节我们重点关注这个分支,主要就是判断这个 referent 是否被标记为 alive
        return ZBarrier::is_alive_barrier_on_weak_oop(referent);
      }
    }
    

    这里大家可能就有点懵了,因为笔者前面介绍过,在 ZGC 的 Concurrent Mark 阶段, Reference 对象被 JVM 添加到 _discovered_list 中的条件就是这个 Reference 对象的 referent 没有被标记过。那为什么这里又要判断一下呢 ?我们来看一个这样的场景:

    image.png

    上图中展示的这个场景是,一个 object 对象在 JVM 堆中同时被一个 StrongReference 对象和一个 WeakReference 对象所引用。

    假设在 ZGC 的 Concurrent Mark 阶段,GC 线程先遍历到 WeakReference 对象,注意此时还没有遍历到 StrongReference 对象。由于还没有遍历到 StrongReference ,所以这个 object 对象还没有被标记为 alive。

    而对于 WeakReference 对象来说,GC 线程并不会遍历标记它的 referent,对吧,这是我们第四小节中的内容了。这时这个 WeakReference 对象就会被 JVM 添加到 _discovered_list 中。

    好的,我们继续 Concurrent Mark,后面 GC 线程最终是要遍历到 StrongReference 对象的,对吧。当 GC 线程遍历到 StrongReference 对象的时候首先会标记这个 StrongReference 对象为 alive,随后开始遍历它的所有非静态成员变量,逐个进行标记 alive,在这个过程中 object 对象最终也会被标记为 alive。

    当 Concurrent Mark 结束之后,我们来到了本小节的 Concurrent Process Non-Strong References 阶段,那么对于此时被添加到 _discovered_list 中的这个 WeakReference 对象是不是就不对了,因为它的 referent 后面又被标记为 alive 了,所以在 should_drop 函数的最后还是要通过 is_alive_barrier_on_weak_oop 判断一下 referent 是否被标记,如果被标记过了,那么就需要将这个 WeakReference 对象从 _discovered_list 中移除。

    了解了这个背景,我们再来看 ZReferenceProcessor::work 中的处理逻辑就很清晰了。首先 GC 线程会在 while (*p != NULL) 循环中不停的遍历 _discovered_list 中临时存放的这些 Reference 对象。

    然后通过 should_drop 判断这个 Reference 对象是否应该从 _discovered_list 中移除,如果 should_drop 返回 true ,那么 JVM 就会通过 drop 方法将 Reference 对象移除。很简单的链表操作,这里笔者就不展开了。

    如果 should_drop 返回 false, JVM 就会让这个 Reference 对象继续保留在 _discovered_list 中,并调用 keep 方法获取该 Reference 对象在 _discovered_list 中的下一个元素,继续进行 while 循环重复上述的判断逻辑。

    oop* ZReferenceProcessor::keep(oop reference, ReferenceType type) {
    
      // 入队计数加 1
      _enqueued_count.get()[type]++;
    
      // 将 referent 置为 null ,此后 Reference 就变为了 inactive
      make_inactive(reference, type);
    
      // 从 _discovered_list 中获取下一个 Reference 继续循环
      return reference_discovered_addr(reference);
    }
    

    keep 方法中会调用一个 make_inactive 方法,JVM 在这里会调用 Reference 对象的 clear 方法将 referent 置为 null 。

    void ZReferenceProcessor::make_inactive(oop reference, ReferenceType type) const {
      if (type == REF_FINAL) {
          ..... 省略 FinalReference 的处理 ....
      } else {
        // 这里调用 Reference 对象的 clear 方法将,referent 置为 null
        reference_clear_referent(reference);
      }
    }
    

    那么此时如果我们在应用线程中调用这个 Reference 对象的 get() 方法的时候就会得到一个 null 值,referent 对象被 JVM 置为 null 的时机就是这个 Reference 对象确定要被添加到 _pending_list 的时候。

    WeakReference weakReference = new WeakReference<Object>(new Object());
    weakReference.get();
    

    当 _discovered_list 中的那些所有需要被移除的 Reference 对象都已经被移除之后,JVM 就会将终态的 _discovered_list 原子地添加到 _pending_list 中。

    Concurrent Process Non-Strong References 阶段的最后,ZGC 就会调用 enqueue_references 方法将 _pending_list 中的 Reference 对象转移到 _reference_pending_list 中。最后重置 pending list,为下一轮 GC 做准备。

    image.png
        void ZReferenceProcessor::enqueue_references() {
    
            if (_pending_list.get() == NULL) {
                // Nothing to enqueue
                return;
            }
    
            {
                // Heap_lock protects external pending list
                MonitorLocker ml(Heap_lock);
    
                // 将 _pending_list 添加到 _reference_pending_list 中
                *_pending_list_tail = Universe::swap_reference_pending_list(_pending_list.get());
    
                // 唤醒 ReferenceHandler 线程
                ml.notify_all();
            }
    
            // 重置 pending list,为下一轮 GC 做准备
            _pending_list.set(NULL);
            _pending_list_tail = _pending_list.addr();
        }
    

    这里我们看到 ZGC 在更新完 _reference_pending_list 之后,会调用一个 ml.notify_all(),那么这个操作是要唤醒谁呢 ?或者说谁会在 Heap_lock 上等待呢 ?

    还记不记得笔者在第三小节中为大家介绍的 native 方法 —— waitForReferencePendingList() :

    // Reference.c 文件
    JNIEXPORT void JNICALL
    Java_java_lang_ref_Reference_waitForReferencePendingList(JNIEnv *env, jclass ignore)
    {
        JVM_WaitForReferencePendingList(env);
    }
    
    // jvm.cpp 文件
    JVM_ENTRY(void, JVM_WaitForReferencePendingList(JNIEnv* env))
      MonitorLocker ml(Heap_lock);
      while (!Universe::has_reference_pending_list()) {
        // 如果 _reference_pending_list 还没有 Reference 对象,那么当前线程在 Heap_lock 上 wait
        ml.wait();
      }
    JVM_END
    

    那么谁会在这个方法上阻塞等待呢 ?答案就是 —— ReferenceHandler 线程。

    private static class ReferenceHandler extends Thread {
    
      private static void processPendingReferences() {
    
            waitForReferencePendingList();
    
            ........ 省略 .....
      }
    }
    

    至于 ReferenceHandler 线程被唤醒之后干了什么 ? 这不就是笔者在第三小节中详细为大家介绍的内容么,这样一来是不是就和前面的内容遥相呼应起来了~~~

    image.png

    6. SoftReference 具体在什么时候被回收 ? 如何量化内存不足 ?

    大家在网上或者在其他讲解 JVM 的书籍中多多少少会看到这样一段关于 SoftReference 的描述 —— “当 SoftReference 所引用的 referent 对象在整个堆中没有其他强引用的时候,发生 GC 的时候,如果此时内存充足,那么这个 referent 对象就和其他强引用一样,不会被 GC 掉,如果此时内存不足,系统即将 OOM 之前,那么这个 referent 对象就会被当做垃圾回收掉”。

    image.png

    当然了,如果仅从概念上理解的话,这样描述就够了,但是如果我们从 JVM 的实现角度上来说,那这样的描述至少是不准确的,为什么呢 ? 笔者先提两个问题出来,大家可以先思考下:

    1. 内存充足的情况下,SoftReference 所引用的 referent 对象就一定不会被回收吗 ?

    2. 什么是内存不足 ?这个概念如何量化,SoftReference 所引用的 referent 对象到底什么时候被回收 ?

    下面笔者继续以 ZGC 为例,带大家深入到 JVM 内部去探寻下这两个问题的精确答案~~

    6.1 JVM 无条件回收 SoftReference 的场景

    经过前面第五小节的介绍,我们知道 ZGC 在 Concurrent Mark 以及 Concurrent Process Non-Strong References 阶段中处理 Reference 对象的关键逻辑都封装在 ZReferenceProcessor 中。

    在 ZReferenceProcessor 中有一个关键的属性 —— _soft_reference_policy,在 ZGC 的过程中,处理 SoftReference 的策略就封装在这里,本小节开头提出的那两个问题的答案就隐藏在 _soft_reference_policy 中。

    class ZReferenceProcessor : public ReferenceDiscoverer {
      // 关于 SoftReference 的处理策略
      ReferencePolicy*     _soft_reference_policy;
    }
    

    那下面的问题就是如果我们能够知道 _soft_reference_policy 的初始化逻辑,那是不是关于 SoftReference 的一切疑惑就迎刃而解了 ?我们来一起看下 _soft_reference_policy 的初始化过程。

    在 ZGC 开始的时候,首先会创建一个 ZDriverGCScope 对象,这里主要进行一些 GC 的准备工作,比如更新 GC 的相关统计信息,设置并行 GC 线程个数,以及本小节的重点,初始化 SoftReference 的处理策略 —— _soft_reference_policy。

    void ZDriver::gc(const ZDriverRequest& request) {
      ZDriverGCScope scope(request);
      ..... 省略 ......
    }
    
    class ZDriverGCScope : public StackObj {
    private:
      GCCause::Cause             _gc_cause;
    public:
      ZDriverGCScope(const ZDriverRequest& request) :
          _gc_cause(request.cause()),
     {
        // Set up soft reference policy
        const bool clear = should_clear_soft_references(request);
        ZHeap::heap()->set_soft_reference_policy(clear);
      }
    

    在 JVM 开始初始化 _soft_reference_policy 之前,会调用一个重要的方法 —— should_clear_soft_references,本小节的答案就在这里,该方法就是用来判断,ZGC 是否需要无条件清理 SoftReference 所引用的 referent 对象。

    • 返回 true 表示,在 GC 的过程中只要遇到 SoftReference 对象,那么它引用的 referent 对象就会被当做垃圾清理,SoftReference 对象也会被 JVM 加入到 _reference_pending_list 中等待 ReferenceHandler 线程去处理。这里就和 WeakReference 的语义一样了。

    • 返回 false 表示,内存充足的时候,JVM 就会把 SoftReference 当做普通的强引用一样处理,它所引用的 referent 对象不会被回收,但内存不足的时候,被 SoftReference 所引用的 referent 对象就会被回收,SoftReference 也会被加入到 _reference_pending_list 中。

    static bool should_clear_soft_references(const ZDriverRequest& request) {
      // Clear soft references if implied by the GC cause
      if (request.cause() == GCCause::_wb_full_gc ||
          request.cause() == GCCause::_metadata_GC_clear_soft_refs ||
          request.cause() == GCCause::_z_allocation_stall) {
        // 无条件清理 SoftReference
        return true;
      }
    
      // Don't clear
      return false;
    }
    

    这里我们看到,在 ZGC 的过程中,只要满足以下三种情况中的任意一种,那么在 GC 过程中就会无条件地清理 SoftReference 。

    1. 引起 GC 的原因是 —— _wb_full_gc,也就是由 WhiteBox 相关 API 触发的 Full GC,就会无条件清理 SoftReference。

    2. 引起 GC 的原因是 —— _metadata_GC_clear_soft_refs,也就是在元数据分配失败的时候触发的 Full GC,元空间内存不足,情况就很严重了,所以要无条件清理 SoftReference。

    3. 引起 GC 的原因是 —— _z_allocation_stall,在 ZGC 采用阻塞模式分配 Zpage 页面的时候,如果内存不足无法分配,那么就会触发一次 GC,这时 GC 的触发原因就是 _z_allocation_stall,这种情况下就会无条件清理 SoftReference。

    ZGC 非阻塞模式分配 Zpage 的时候如果内存不足、就直接抛出 OutOfMemoryError,不会启动 GC 。

    ZPage* ZPageAllocator::alloc_page(uint8_t type, size_t size, ZAllocationFlags flags) {
      EventZPageAllocation event;
    
    retry:
      ZPageAllocation allocation(type, size, flags);
      // 判断是否进行阻塞分配 ZPage
      if (!alloc_page_or_stall(&allocation)) {
        // 如果非阻塞分配  ZPage 失败,直接 Out of memory
        return NULL;
      }
    }
    

    在我们了解了这个背景之后,在回头来看下 _soft_reference_policy 的初始化过程 :

    参数 clear 就是 should_clear_soft_references 函数的返回值

    void ZReferenceProcessor::set_soft_reference_policy(bool clear) {
      static AlwaysClearPolicy always_clear_policy;
      static LRUMaxHeapPolicy lru_max_heap_policy;
    
      if (clear) {
        log_info(gc, ref)("Clearing All SoftReferences");
        _soft_reference_policy = &always_clear_policy;
      } else {
        _soft_reference_policy = &lru_max_heap_policy;
      }
    
      _soft_reference_policy->setup();
    }
    

    ZGC 采用了两种策略来处理 SoftReference :

    1. always_clear_policy : 当 clear 为 true 的时候,ZGC 就会采用这种策略,在 GC 的过程中只要遇到 SoftReference,就会无条件回收其引用的 referent 对象,SoftReference 对象也会被 JVM 加入到 _reference_pending_list 中等待 ReferenceHandler 线程去处理。

    2. lru_max_heap_policy :当 clear 为 false 的时候,ZGC 就会采用这种策略,这种情况下 SoftReference 的存活时间取决于 JVM 堆中剩余可用内存的总大小,也是我们下一小节中讨论的重点。

    下面我们就来看一下 lru_max_heap_policy 的初始化过程,看看 JVM 是如何量化内存不足的 ~~

    6.2 JVM 如何量化内存不足

    LRUMaxHeapPolicy 的 setup() 方法主要用来确定被 SoftReference 所引用的 referent 对象最大的存活时间,这个存活时间是和堆的剩余空间大小有关系的,也就是堆的剩余空间越大 SoftReference 的存活时间就越长,堆的剩余空间越小 SoftReference 的存活时间就越短。

    void LRUMaxHeapPolicy::setup() {
      size_t max_heap = MaxHeapSize;
      // 获取最近一次 gc 之后,JVM 堆的最大剩余空间
      max_heap -= Universe::heap()->used_at_last_gc();
      // 转换为 MB
      max_heap /= M;
      //  -XX:SoftRefLRUPolicyMSPerMB 默认为 1000 ,单位毫秒
      // 表示每 MB 的剩余内存空间中允许 SoftReference 存活的最大时间
      _max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
      assert(_max_interval >= 0,"Sanity check");
    }
    

    JVM 首先会获取我们通过 -Xmx 参数指定的最大堆 —— MaxHeapSize,然后在通过 Universe::heap()->used_at_last_gc() 获取上一次 GC 之后 JVM 堆占用的空间,两者相减,就得到了当前 JVM 堆的最大剩余内存空间,并将单位转换为 MB

    现在 JVM 堆的剩余空间我们计算出来了,那如何根据这个 max_heap 计算 SoftReference 的最大存活时间呢 ?

    这里就用到了一个 JVM 参数 —— SoftRefLRUPolicyMSPerMB,我们可以通过 -XX:SoftRefLRUPolicyMSPerMB 来指定,默认为 1000 , 单位为毫秒。

    它表达的意思是每 MB 的堆剩余内存空间允许 SoftReference 存活的最大时长,比如当前堆中只剩余 1MB 的内存空间,那么 SoftReference 的最大存活时间就是 1000 ms,如果剩余内存空间为 2MB,那么 SoftReference 的最大存活时间就是 2000 ms 。

    现在我们剩余 max_heap 的空间,那么在本轮 GC 中,SoftReference 的最大存活时间就是 —— _max_interval = max_heap * SoftRefLRUPolicyMSPerMB

    从这里我们可以看出 SoftReference 的最大存活时间 _max_interval,取决于两个因素:

    1. 当前 JVM 堆的最大剩余空间。

    2. 我们指定的 -XX:SoftRefLRUPolicyMSPerMB 参数值,这个值越大 SoftReference 存活的时间就越久,这个值越小,SoftReference 存活的时间就越短。

    在我们得到了这个 _max_interval 之后,那么 JVM 是如何量化内存不足呢 ?被 SoftReference 引用的这个 referent 对象到底什么被回收 ?让我们再次回到 JDK 中,来看一下 SoftReference 的实现:

    public class SoftReference<T> extends Reference<T> {
        // 由 JVM 来设置,每次 GC 发生的时候,JVM 都会记录一个时间戳到这个 clock 字段中
        private static long clock;
        // 表示应用线程最近一次访问这个 SoftReference 的时间戳(当前的 clock 值)
        // 在 SoftReference 的 get 方法中设置
        private long timestamp;
    
        public SoftReference(T referent) {
            super(referent);
            this.timestamp = clock;
        }
    
        public T get() {
            T o = super.get();
            if (o != null && this.timestamp != clock)
                // 将最近一次的 gc 发生时间设置到 timestamp 中
                // 用这个表示当前 SoftReference 最近被访问的时间戳
                // 注意这里的时间戳语义是 最近一次的 gc 时间
                this.timestamp = clock;
            return o;
        }
    }
    

    SoftReference 中有两个非常重要的字段,一个是 clock ,另一个是 timestamp。clock 字段是由 JVM 来设置的,在每一次发生 GC 的时候,JVM 都会去更新这个时间戳。具体一点的话,就是在 ZGC 的 Concurrent Process Non-Strong References 阶段处理完所有 Reference 对象之后,JVM 就会来更新这个 clock 字段。

    void ZReferenceProcessor::process_references() {
      ZStatTimer timer(ZSubPhaseConcurrentReferencesProcess);
    
      // Process discovered lists
      ZReferenceProcessorTask task(this);
      // gc _workers 一起运行 ZReferenceProcessorTask
      _workers->run(&task);
    
      // Update SoftReference clock
      soft_reference_update_clock();
    }
    

    soft_reference_update_clock() 中 ,JVM 会将 SoftReference 类中的 clock 字段更新为当前时间戳,单位为毫秒。

    static void soft_reference_update_clock() {
      const jlong now = os::javaTimeNanos() / NANOSECS_PER_MILLISEC;
      java_lang_ref_SoftReference::set_clock(now);
    }
    

    而 timestamp 字段用来表示这个 SoftReference 对象有多久没有被访问到了,应用线程越久没有访问 SoftReference,JVM 就越倾向于回收它的 referent 对象。这也是 LRUMaxHeapPolicy 策略中 LRU 的语义体现。

    应用线程在每次调用 SoftReference 的 get 方法时候,都会将最近一次的 GC 时间戳 clock 更新到 timestamp 中,这样一来,如果一个 SoftReference 被频繁的访问,那么 clock 和 timestamp 的值一直是相等的。

    image.png

    如果一个 SoftReference 已经很久没有被访问了,timestamp 就会远远落后于 clock,因为在没有被访问的这段时间内可能已经发生好几次 GC 了。

    image.png

    在我们了解了这些背景之后,再来看一下 JVM 对于 SoftReference 的回收过程,在本文 5.1 小节中介绍的 ZGC Concurrent Mark 阶段中,当 GC 遍历到一个 Reference 类型的对象的时候,会在 should_discover 方法中判断一下这个 Reference 对象所引用的 referent 是否被标记过。如果 referent 没有被标记为 alive , 那么接下来就会将这个 Reference 对象放入 _discovered_list 中,等待后续被 ReferenHandler 处理,referent 也会在本轮 GC 中被回收掉。

    bool ZReferenceProcessor::should_discover(oop reference, ReferenceType type) const {
    
      // 此时 Reference 的状态就是 inactive,那么这里将不会重复将 Reference 添加到 _discovered_list 重复处理
      if (is_inactive(reference, referent, type)) {
        return false;
      }
      // referent 还被强引用关联,那么 return false 也就是说不能被加入到 discover list 中
      if (is_strongly_live(referent)) {
        return false;
      }
      // referent 现在只被软引用关联,那么就需要通过 LRUMaxHeapPolicy
      // 来判断这个 SoftReference 所引用的 referent 是否应该存活
      if (is_softly_live(reference, type)) {
        return false;
      }
    
      return true;
    }
    

    如果当前遍历到的 Reference 对象是 SoftReference 类型的,那么就需要在 is_softly_live 方法中根据前面介绍的 LRUMaxHeapPolicy 来判断这个 SoftReference 引用的 referent 对象是否满足存活的条件。

    bool ZReferenceProcessor::is_softly_live(oop reference, ReferenceType type) const {
      if (type != REF_SOFT) {
        // Not a SoftReference
        return false;
      }
    
      // Ask SoftReference policy
      // 获取 SoftReference 中的 clock 字段,这里存放的是上一次 gc 的时间戳
      const jlong clock = java_lang_ref_SoftReference::clock();
      // 判断是否应该清除这个 SoftReference
      return !_soft_reference_policy->should_clear_reference(reference, clock);
    }
    

    通过 java_lang_ref_SoftReference::clock() 获取到的就是前面介绍的 SoftReference.clock 字段 —— timestamp_clock。

    通过 java_lang_ref_SoftReference::timestamp(p) 获取到的就是前面介绍的 SoftReference.timestamp 字段。

    如果 SoftReference.clock 与 SoftReference.timestamp 的差值 —— interval,小于等于前面介绍的 SoftReference 最大存活时间 —— _max_interval,那么这个 SoftReference 所引用的 referent 对象在本轮 GC 中就不会被回收,SoftReference 对象也不会被放到 _reference_pending_list 中被 ReferenceHandler 线程处理。

    // The oop passed in is the SoftReference object, and not
    // the object the SoftReference points to.
    bool LRUMaxHeapPolicy::should_clear_reference(oop p,
                                                 jlong timestamp_clock) {
      // 相当于 SoftReference.clock - SoftReference.timestamp
      jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
    
    
      // The interval will be zero if the ref was accessed since the last scavenge/gc.
      // 如果 clock 与 timestamp 的差值小于等于 _max_interval (SoftReference 的最大存活时间)
      if(interval <= _max_interval) {
        // SoftReference 所引用的 referent 对象在本轮 GC 中就不会被回收
        return false;
      }
      // interval 大于 _max_interval,这个 SoftReference 所引用的 referent 对象就会被回收
      // SoftReference 也会被放到 _reference_pending_list 中等待 ReferenceHandler 线程去处理
      return true;
    }
    

    如果 interval 大于 _max_interval,那么这个 SoftReference 所引用的 referent 对象在本轮 GC 中就会被回收,SoftReference 对象也会被 JVM 放到 _reference_pending_list 中等待 ReferenceHandler 线程处理。

    从以上过程中我们可以看出,SoftReference 被 ZGC 回收的精确时机是,当一个 SoftReference 对象已经很久很久没有被应用线程访问到了,那么发生 GC 的时候这个 SoftReference 就会被回收掉。

    具体多久呢 ? 就是 _max_interval 指定的 SoftReference 最大存活时间,这个时间由当前 JVM 堆的最大剩余空间和 -XX:SoftRefLRUPolicyMSPerMB 共同决定。

    比如,发生 GC 的时候,当前堆的最大剩余空间为 1MB,SoftRefLRUPolicyMSPerMB 指定的是 1000 ms ,那么当一个 SoftReference 对象超过 1000 ms 没有被应用线程访问的时候,就会被 ZGC 回收掉。

    《以 ZGC 为例,谈一谈 JVM 是如何实现 Reference 语义的(下)》

    相关文章

      网友评论

          本文标题:以 ZGC 为例,谈一谈 JVM 是如何实现 Reference

          本文链接:https://www.haomeiwen.com/subject/ptipqjtx.html