以 ZGC 為例,談一談 JVM 是如何實(shí)現(xiàn) Reference 語義的(中)

《以 ZGC 為例糕篇,談一談 JVM 是如何實(shí)現(xiàn) Reference 語義的(上)》

4. JVM 如何實(shí)現(xiàn) Reference 語義

在前面的幾個(gè)小節(jié)中,筆者為大家全面且詳細(xì)地介紹了 Reference 在各中間件以及 JDK 中的應(yīng)用場景,相信大家現(xiàn)在對(duì)于在什么情況下該使用哪種具體的 Reference危虱, JDK 又如何處理這些 Reference 的流程已經(jīng)非常清晰了。

但這還遠(yuǎn)遠(yuǎn)不夠唐全,因?yàn)槲覀円恢边€沒觸達(dá)到 Reference 的本質(zhì)埃跷,而在經(jīng)過前面三個(gè)小節(jié)的內(nèi)容鋪墊之后,筆者將會(huì)在本小節(jié)中邮利,帶著大家深入到 JVM 內(nèi)部弥雹,去看看 JVM 到底是如何實(shí)現(xiàn) PhantomReference,WeakReference延届,SoftReference 以及 FinalReference 相關(guān)語義的剪勿。

在這個(gè)過程中,筆者會(huì)把之前在概念層面介紹的各種 Reference 語義方庭,一一映射到源碼級(jí)別的實(shí)現(xiàn)上厕吉,讓大家從抽象層面再到具象層面徹底理解 Reference 的本質(zhì)。大家對(duì)各種 Reference 的模糊理解械念,在后面的內(nèi)容中都會(huì)得到清晰的解答头朱。

哈哈,鋪墊了這么久龄减,終于和標(biāo)題開始掛上鉤了

但在本小節(jié)的內(nèi)容開始之前项钮,筆者想和大家明確兩個(gè)概念,因?yàn)楹竺婀P者不想在用大段的語言重復(fù)解釋他們。JDK 對(duì)于引用類的設(shè)計(jì)層級(jí)有兩層烁巫,PhantomReference鳖敷,WeakReference,SoftReference 以及 FinalReference 都繼承于 Reference 類中程拭。

image.png

后續(xù)筆者將會(huì)用 Reference 這個(gè)概念來統(tǒng)稱以上四種引用類型定踱,除非遇到不同 Reference 的語義實(shí)現(xiàn)時(shí),筆者才會(huì)特殊指明具體的 Reference 類型恃鞋。而 Reference 所引用的普通 Java 對(duì)象崖媚,存放在 referent 字段中,后面我們將會(huì)用 referent 來統(tǒng)一指代被引用的 Java 對(duì)象恤浪。

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

好了畅哑,在我們明確了 Reference 和 referent 的概念之后,筆者先向大家拋一個(gè)問題出來水由,大家可以先自己思考下荠呐。我們都知道,只要一個(gè) Java 對(duì)象存在從 GcRoot 到它的強(qiáng)引用鏈砂客,那么這個(gè) Java 對(duì)象就會(huì)被 JVM 標(biāo)記為 alive泥张,本輪 GC 就不會(huì)回收它。

這個(gè)強(qiáng)引用鏈?zhǔn)鞘裁茨?鞠值?其實(shí)就是 GC 根對(duì)象的所有非靜態(tài)成員變量媚创,而這些非靜態(tài)的成員變量也會(huì)引用到其他 Java 對(duì)象,這些 Java 對(duì)象也會(huì)有自己的非靜態(tài)成員變量彤恶,這些成員變量又會(huì)引用到其他 Java 對(duì)象钞钙,這就慢慢形成了從 GC 根對(duì)象出發(fā)的有向引用關(guān)系圖,這個(gè)引用關(guān)系圖就是強(qiáng)引用鏈声离。

image.png

如果按照這種思路的話芒炼,那么從本質(zhì)上來說 Reference 類也是一個(gè)普通的 Java 類,它的實(shí)例也是一個(gè)普通的對(duì)象實(shí)例术徊,referent 也是它的一個(gè)成員變量本刽,按理說,JVM 也可以從 GcRoot 開始遍歷到 Reference 對(duì)象弧关,近而通過它的成員變量 referent 遍歷到被它引用的普通 Java 對(duì)象盅安。

這里我們先不用考慮什么軟引用,弱引用世囊,虛引用的概念别瞭,我們只從本質(zhì)上來說,JVM 是不是也可以通過這條引用鏈將 referent 標(biāo)記為 alive 呢 株憾?那為什么在 GC 的時(shí)候蝙寨,這個(gè) referent 就被當(dāng)做垃圾回收了呢 晒衩?

image.png

這里筆者先以 WeakReference 為例說明,事實(shí)上 JVM 對(duì)于 PhantomReference墙歪,WeakReference听系,SoftReference 以及 FinalReference 的處理總體上都是一樣的,只不過對(duì)于 PhantomReference虹菲,SoftReference靠胜,F(xiàn)inalReference 會(huì)進(jìn)行一些小小的特殊處理,這個(gè)筆者后面會(huì)放到單獨(dú)的小節(jié)中討論毕源。我們先以 WeakReference 來說明 JVM 對(duì)于 Reference 的總體處理流程浪漠。

要明白這個(gè)問題,我們就需要弄明白 JVM 在 GC 的時(shí)候是如何遍歷對(duì)象的引用關(guān)系圖的霎褐,在處理普通 Java 對(duì)象的引用關(guān)系時(shí)和處理 Reference 對(duì)象的引用關(guān)系時(shí)有何不同 址愿?

4.1 JVM 如何遍歷對(duì)象的引用關(guān)系圖

這里筆者要再次提醒大家,現(xiàn)在請(qǐng)你立刻冻璃,馬上忘掉腦海中關(guān)于軟引用响谓,弱引用,虛引用的所有概念省艳,讓我們回歸 Reference 類的本質(zhì)娘纷,它其實(shí)就是一個(gè)普通的 Java 類,Reference 相關(guān)的實(shí)例就是一個(gè)普通的 Java 對(duì)象拍埠。

image.png

看透了這一層失驶,剩下的就好辦了土居,現(xiàn)在這些所有的問題最終匯結(jié)成了 —— JVM 如何遍歷普通 Java 對(duì)象的引用關(guān)系圖枣购。我們先從一個(gè)簡單的例子開始~~

JVM 從 GcRoot 開始遍歷,期間遇到的每一個(gè)對(duì)象擦耀,在 JVM 看來就是活躍的棉圈,當(dāng) GC 線程遍歷到一個(gè)對(duì)象時(shí),就會(huì)將這個(gè)對(duì)象標(biāo)記為 alive眷蜓,然后在看這個(gè)對(duì)象的非靜態(tài)成員變量引用了哪些對(duì)象分瘾,順藤摸瓜,沿著這些成員變量所引用的對(duì)象繼續(xù)標(biāo)記 alive吁系,直到所有的引用關(guān)系圖被遍歷完德召。

現(xiàn)在問題的關(guān)鍵就是 JVM 如何找到對(duì)象中的這些成員變量,而對(duì)象的本質(zhì)其實(shí)就是一段內(nèi)存汽纤,當(dāng)我們通過 new 關(guān)鍵字分配對(duì)象的時(shí)候上岗,JVM 首先會(huì)為這個(gè)對(duì)象分配一段內(nèi)存,然后根據(jù) Java 的對(duì)象模型初始化這段內(nèi)存蕴坪。下圖中展示的就是 Java 對(duì)象的內(nèi)存模型肴掷,其中存放了對(duì)象的 MarkWord 敬锐, 類型信息,以及實(shí)例數(shù)據(jù)(下圖中的藍(lán)色區(qū)域)呆瞻。

Java對(duì)象的內(nèi)存布局.png

對(duì)象內(nèi)存模型中的實(shí)例數(shù)據(jù)區(qū)中包含了對(duì)象中的基本類型字段還有引用類型字段台夺,當(dāng) JVM 遍歷到一個(gè)對(duì)象實(shí)例時(shí),這個(gè)實(shí)例所占用的內(nèi)存地址是不是就知道了痴脾,知道了對(duì)象所占內(nèi)存的地址颤介,那么實(shí)例數(shù)據(jù)區(qū)的地址也就知道了。

JVM 近而可以遍歷這段實(shí)例數(shù)據(jù)內(nèi)存區(qū)域赞赖,如果發(fā)現(xiàn)是基本類型的字段就跳過买窟,如果是引用類型的字段,那么這就是我們要找的成員變量薯定,該成員變量引用的對(duì)象地址是不是就知道了始绍,最后根據(jù)引用類型的成員變量指向的對(duì)象地址,找到被引用的對(duì)象话侄,然后標(biāo)記為 alive亏推,最后再次從這個(gè)對(duì)象出發(fā),循環(huán)上述邏輯年堆,慢慢的就找到了所有存活的對(duì)象吞杭。

當(dāng)然了這只是一種可行的方案,實(shí)際上 JVM 并不會(huì)這么做变丧,因?yàn)檫@樣效率太低了芽狗,系統(tǒng)中有成千上萬的對(duì)象實(shí)例,JVM 不可能每遍歷一個(gè)對(duì)象痒蓬,就到對(duì)象內(nèi)存中的實(shí)例數(shù)據(jù)區(qū)去挨個(gè)尋找引用類型的成員變量童擎。

那么有沒有一種索引結(jié)構(gòu)來提前記錄對(duì)象中究竟有哪些引用類型的成員變量,并且這些成員變量在對(duì)象內(nèi)存中的位置偏移呢 攻晒?這個(gè)索引結(jié)構(gòu)就是 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 結(jié)構(gòu)用于描述對(duì)象中定義的那些引用類型的非靜態(tài)成員變量在對(duì)象內(nèi)存中的偏移位置,每個(gè) OopMapBlock 中包含多個(gè)非靜態(tài)成員變量的地址偏移索引鲁捏,而且這些非靜態(tài)成員變量的地址必須是連續(xù)的芯砸。

什么意思呢 ? 比如我們在寫代碼的時(shí)候给梅,在一個(gè)類中連續(xù)的定義了多個(gè)非靜態(tài)成員變量(引用類型)假丧,那么這些成員變量的地址偏移就被封裝到了一個(gè) OopMapBlock 中。

但如果我們在類中定義成員變量的時(shí)候动羽,中間插入了一個(gè)基本類型的成員變量包帚,這么原本連續(xù)的引用類型的成員變量就不連續(xù)了,被分割成了兩段曹质。JVM 就會(huì)用兩個(gè) OopMapBlock 來索引他們的地址偏移婴噩。

除了由于被基本類型的成員變量分割而導(dǎo)致產(chǎn)生多個(gè) OopMapBlock 之外擎场,在對(duì)象類型的父類中也可能會(huì)定義非靜態(tài)成員變量,父類中也會(huì)有多個(gè) OopMapBlock几莽。

image.png

OopMapBlock 結(jié)構(gòu)中的 _offset 用來指定第一個(gè)非靜態(tài)引用類型的成員變量在對(duì)象內(nèi)存地址中的偏移迅办。_count 表示 OopMapBlock 中地址連續(xù)的非靜態(tài)成員變量個(gè)數(shù)。

另外還有字段與字段之間的字節(jié)填充章蚣,由于字節(jié)填充造成字段之間的地址不連續(xù)站欺,也會(huì)產(chǎn)生多個(gè) OopMapBlock。但是每個(gè) OopMapBlock 中封裝的非靜態(tài)成員變量地址一定是連續(xù)的纤垂。

這樣一來矾策,一個(gè) Java 類型在 JVM 中就會(huì)擁有多個(gè) OopMapBlock,這些 OopMapBlock 被組織在一個(gè)叫做 nonstatic_oop_maps 的數(shù)組中峭沦,當(dāng) JVM 遍歷到一個(gè)對(duì)象實(shí)例時(shí)贾虽,如果能找到這個(gè) nonstatic_oop_maps,近而通過數(shù)組中的這些 OopMapBlock 是不是就能立馬將對(duì)象實(shí)例中的所有非靜態(tài)成員變量都找出來了吼鱼。

好了蓬豁,現(xiàn)在又有一個(gè)新的問題擺在我們面前了,這個(gè) nonstatic_oop_maps 數(shù)組存放在哪里 ?JVM 如何找到這個(gè) nonstatic_oop_maps ?

從 JVM 對(duì)于 Java 類型的設(shè)計(jì)層面來講诺祸,nonstatic_oop_maps 屬于 Java 類的元信息,我們比較熟悉的是泡一,在 JDK 層面每一個(gè) Java 類都會(huì)對(duì)應(yīng)一個(gè) Class 對(duì)象來描述 Java 類的元信息,而 JDK 層面上的這個(gè) Class 對(duì)象,對(duì)應(yīng)于 JVM 層面來說就是 InstanceKlass 實(shí)例。

JVM 在加載 Java 類的時(shí)候會(huì)為 Java 類構(gòu)建 nonstatic_oop_maps质礼,類加載完成之后,JVM 會(huì)為 Java 類創(chuàng)建一個(gè) InstanceKlass 實(shí)例飞蹂,nonstatic_oop_maps 就放在這個(gè) InstanceKlass 實(shí)例里几苍。

從 InstanceKlass 實(shí)例的內(nèi)存布局中我們可以看出,nonstatic_oop_maps 是緊挨在 vtable陈哑,itable 之后的。

InstanceKlass* InstanceKlass::allocate_instance_klass(const ClassFileParser& parser, TRAPS) {
  // InstanceKlass 實(shí)例的內(nèi)存布局
  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 {
      // 對(duì)于普通的 Java 類來說伸眶,這里創(chuàng)建的是 InstanceKlass 實(shí)例
      ik = new (loader_data, size, THREAD) InstanceKlass(parser, InstanceKlass::_kind_other);
    }
  } else {
    // 對(duì)于 Reference 類來說惊窖,這里創(chuàng)建的是 InstanceRefKlass 實(shí)例
    ik = new (loader_data, size, THREAD) InstanceRefKlass(parser);
  }
  return ik;
}

對(duì)于普通的 Java 類型來說,這里創(chuàng)建的是 InstanceKlass 實(shí)例厘贼,對(duì)于 Reference 類型來說界酒,這里創(chuàng)建的是 InstanceRefKlass 實(shí)例,大家要牢記這一點(diǎn)嘴秸。

了解了 InstanceKlass 實(shí)例的內(nèi)存布局之后毁欣,獲取 nonstatic_oop_maps 數(shù)組就很簡單了庇谆,我們只需要跳過 vtable 和 itable 就得到了 nonstatic_oop_maps 的起始內(nèi)存地址。

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 對(duì)象凭疮,那如何通過 Java 對(duì)象獲取到其對(duì)應(yīng)在 JVM 中的 InstanceKlass 呢 饭耳? 這就用到了前面筆者提到的 Java 對(duì)象的內(nèi)存模型,在 JVM 中用 oopDesc 結(jié)構(gòu)來描述 Java 對(duì)象的內(nèi)存模型执解。

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

我們在 Java 層面見到的對(duì)象寞肖,對(duì)應(yīng)于 JVM 層面就是一個(gè) oopDesc 實(shí)例,JVM 直接處理的就是這個(gè) oopDesc 實(shí)例衰腌,在 oopDesc 中有一個(gè) _klass 類型指針新蟆,指向的就是 Java 對(duì)象所屬的 Java 類在 JVM 層面上對(duì)應(yīng)的 InstanceKlass 實(shí)例,

我們可以通過 klass() 函數(shù)來獲取 oopDesc 中的 _klass:

Klass* oopDesc::klass() const {
  if (UseCompressedClassPointers) {
    // 開啟壓縮指針的情況
    return CompressedKlassPointers::decode_not_null(_metadata._compressed_klass);
  } else {
    return _metadata._klass;
  }
}

當(dāng) JVM 遍歷到一個(gè) oop (JVM 層面的 Java 對(duì)象)時(shí)右蕊,首先會(huì)將它標(biāo)記位 alive琼稻,隨后通過 klass() 函數(shù)獲取它對(duì)應(yīng)的 InstanceKlass 實(shí)例。

template <typename OopClosureType>
void oopDesc::oop_iterate(OopClosureType* cl) {
  // 遍歷對(duì)象的所有非靜態(tài)成員變量
  OopIteratorClosureDispatch::oop_oop_iterate(cl, this, klass());
}

JVM 遍歷對(duì)象引用關(guān)系圖的核心邏輯就封裝在 InstanceKlass 中的 oop_oop_iterate_oop_maps 方法中:

template <typename T, class OopClosureType>
ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_maps(oop obj, OopClosureType* closure) {
  // InstanceKlass 中有多個(gè) OopMapBlock饶囚,它們在 InstanceKlass 實(shí)例內(nèi)存中會(huì)放在一起
  // 獲取首個(gè) OopMapBlock 地址
  OopMapBlock* map           = start_of_nonstatic_oop_maps();
  // 獲取 InstanceKlass 中包含的 OopMapBlock 個(gè)數(shù)欣簇,這些都是在類加載的時(shí)候決定的
  // class 文件中有字段表,在類加載的時(shí)候可以根據(jù)字段表建立 OopMapBlock
  OopMapBlock* const end_map = map + nonstatic_oop_map_count();
  // 挨個(gè)遍歷 InstanceKlass 中所有的 OopMapBlock
  for (; map < end_map; ++map) {
    // OopMapBlock 中包含的是 java 類中非靜態(tài)成員變量在對(duì)象地址中的偏移
    // 通過它直接可以獲取到成員變量的指針
    oop_oop_iterate_oop_map<T>(map, obj, closure);
  }
}

在這里首先會(huì)通過 start_of_nonstatic_oop_maps 在 InstanceKlass 實(shí)例中獲取 nonstatic_oop_maps 數(shù)組的起始地址 map坯约,然后根據(jù) nonstatic_oop_maps 中包含的 OopMapBlock 個(gè)數(shù)熊咽,獲取最后一個(gè) OopMapBlock 的地址 end_map

根據(jù)這些 OopMapBlock 索引好的非靜態(tài)成員變量地址偏移闹丐,挨個(gè)獲取這些成員變量的地址横殴,并通過 do_oop 逐個(gè)進(jìn)行標(biāo)記。

template <typename T, class OopClosureType>
ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_map(OopMapBlock* map, oop obj, OopClosureType* closure) {
  // 通過成員變量在 obj 對(duì)象內(nèi)存中的偏移獲取成員變量指針
  T* p         = (T*)obj->obj_field_addr<T>(map->offset());
  // 獲取該 OopMapBlock 所映射的成員變量個(gè)數(shù)
  T* const end = p + map->count();
  // 遍歷成員變量挨個(gè)標(biāo)記
  for (; p < end; ++p) {
    // 標(biāo)記成員變量
    Devirtualizer::do_oop(closure, p);
  }
}

以上就是 JVM 如何遍歷對(duì)象引用關(guān)系圖的所有核心邏輯卿拴,主要依靠的就是這個(gè) OopMapBlock衫仑,那么它是在什么時(shí)候被 JVM 構(gòu)建出來的呢 ?

4.2 OopMapBlock 在何時(shí) 堕花?又是如何 文狱?被 JVM 構(gòu)建出來

OopMapBlock 構(gòu)建了 Java 類中定義的非靜態(tài)成員變量在對(duì)象實(shí)例中的偏移地址索引,這些都屬于類的元信息缘挽,在類加載的時(shí)候都可以確定下來瞄崇。所以 nonstatic_oop_maps 自然也是在類加載的時(shí)候被構(gòu)建出來的。

Java 類的元信息存放在 .class 文件中壕曼,.class 文件是由 Java 編譯器從 .java 文件中編譯而來苏研。.class 文件中存放的是字節(jié)碼,本質(zhì)上是一個(gè)具有特定二進(jìn)制格式的二進(jìn)制流腮郊。里面將 Java 類的元信息按照特定格式組織在一起摹蘑。

Java 類加載的過程就是 JVM 將 .class 文件中的二進(jìn)制流加載到內(nèi)存,并對(duì)字節(jié)流中的數(shù)據(jù)進(jìn)行校驗(yàn)轧飞,轉(zhuǎn)換解析和初始化衅鹿,最終形成可以被虛擬機(jī)直接使用的 Java 類型撒踪。

.class 文件中的二進(jìn)制字節(jié)流可以來自于 JAR 包,也可以從數(shù)據(jù)庫中讀取大渤,也可以通過動(dòng)態(tài)代理在程序運(yùn)行時(shí)生成制妄。不管 .class 文件中的二進(jìn)制字節(jié)流是從哪里獲取的,最終都是通過 ClassLoader 的 native 方法 defineClass1 加載這些二進(jìn)制字節(jié)流到內(nèi)存中并生成代表該類的 java.lang.Class 對(duì)象兼犯,作為這個(gè)類在 Metaspace 中的數(shù)據(jù)訪問入口忍捡。

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

在 defineClass1 的 native 實(shí)現(xiàn)中,會(huì)調(diào)用到一個(gè) SystemDictionary::resolve_from_stream 方法切黔,在這里會(huì)完成類的加載砸脊,驗(yàn)證,解析等操作纬霞,在完成類的解析之后就會(huì)構(gòu)建 nonstatic_oop_maps凌埂,創(chuàng)建 InstanceKlass 實(shí)例,最后將 nonstatic_oop_maps 填充到 InstanceKlass 實(shí)例中诗芜。

static jclass jvm_define_class_common(const char *name,
                                      jobject loader, const jbyte *buf,
                                      jsize len, jobject pd, const char *source,
                                      TRAPS) {
  // 將 class 文件的二進(jìn)制字節(jié)流轉(zhuǎn)換為 ClassFileStream
  ClassFileStream st((u1*)buf, len, source, ClassFileStream::verify);
  // 進(jìn)行類的加載瞳抓,驗(yàn)證,解析伏恐,隨后為創(chuàng)建 InstanceKlass 實(shí)例
  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 中進(jìn)行:

InstanceKlass* KlassFactory::create_from_stream(ClassFileStream* stream,
                                                Symbol* name,
                                                ClassLoaderData* loader_data,
                                                const ClassLoadInfo& cl_info,
                                                TRAPS) {
  // 這里完成類的加載孩哑,驗(yàn)證,解析 以及 nonstatic_oop_maps 的構(gòu)建
  ClassFileParser parser(stream,
                         name,
                         loader_data,
                         &cl_info,
                         ClassFileParser::BROADCAST, // publicity level
                         CHECK_NULL);
  // 分配 InstanceKlass 實(shí)例翠桦,并將構(gòu)建好的 nonstatic_oop_maps 填充到 InstanceKlass 實(shí)例中
  InstanceKlass* result = parser.create_instance_klass(old_stream != stream, *cl_inst_info, CHECK_NULL);
  return result;
}

nonstatic_oop_maps 的構(gòu)建主要是在類解析階段之后横蜒,由 post_process_parsed_stream 函數(shù)負(fù)責(zé)觸發(fā)構(gòu)建。

ClassFileParser::ClassFileParser(ClassFileStream* stream,
                                 Symbol* name,
                                 ClassLoaderData* loader_data,
                                 const ClassLoadInfo* cl_info,
                                 Publicity pub_level,
                                 TRAPS) :

  ........ 省略 加載销凑,驗(yàn)證丛晌,解析邏輯 ......

  // 這里會(huì)構(gòu)建 nonstatic_oop_maps
  post_process_parsed_stream(stream, _cp, CHECK);
}

在 post_process_parsed_stream 函數(shù)中會(huì)對(duì) Java 類中定義的所有字段進(jìn)行布局:

void ClassFileParser::post_process_parsed_stream(
  ..... 省略 .....

  // 對(duì) Java 類中的字段信息進(jìn)行布局
  _field_info = new FieldLayoutInfo();
  FieldLayoutBuilder lb(class_name(), super_klass(), _cp, _fields,
                        _parsed_annotations->is_contended(), _field_info);
  // 構(gòu)建 nonstatic_oop_maps
  lb.build_layout();
}

void FieldLayoutBuilder::build_layout() {
  // 在對(duì)類中的字段完成布局之后,會(huì)調(diào)用一個(gè) epilogue() 函數(shù)
  compute_regular_layout();
}

// nonstatic_oop_maps的構(gòu)建邏輯就在這里
void FieldLayoutBuilder::epilogue() {

在字段布局完成之后斗幼,會(huì)在 epilogue() 函數(shù)中按照字段的布局信息澎蛛,構(gòu)建 nonstatic_oop_maps。

  1. 首先繼承來自其父類的 nonstatic_oop_maps蜕窿。

  2. _root_group->oop_fields() 中獲取類中的所有非靜態(tài)成員變量谋逻,并將相鄰的成員變量構(gòu)建在同一個(gè) OopMapBlock 中

  3. 處理被 @Contended 標(biāo)注過的非靜態(tài)成員變量,屬于同一個(gè) content group 的成員變量在對(duì)象實(shí)例內(nèi)存中必須連續(xù)存放渠羞,獨(dú)占 CPU 緩存行斤贰。所以同一個(gè) content group 下的成員變量會(huì)被構(gòu)建在同一個(gè) OopMapBlock 中。

void FieldLayoutBuilder::epilogue() {
  // 開始構(gòu)建 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());
  }

  // 為非靜態(tài)成員變量構(gòu)建 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);
      // 構(gòu)建 OopMapBlock次询,相鄰的字段構(gòu)建在一個(gè) OopMapBlock 中
      // 不相鄰的字段分別構(gòu)建在不同的 OopMapBlock 中
      nonstatic_oop_maps->add(b->offset(), 1);
    }
  }

  // 為 @Contended 標(biāo)注的非靜態(tài)成員變量構(gòu)建 nonstatic_oop_maps
  // 在靜態(tài)成員變量上標(biāo)注 @Contended 將會(huì)被忽略
  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");
        // 構(gòu)建 OopMapBlock,屬于同一個(gè) contended_groups 的成員變量在內(nèi)存中要放在一起
        nonstatic_oop_maps->add(cg->oop_fields()->at(0)->offset(), cg->oop_count());
      }
    }
  }
  // 對(duì)相鄰的  OopMapBlock 進(jìn)行排序整理
  // 確保在內(nèi)存中相鄰排列的非靜態(tài)成員變量被構(gòu)建在一個(gè) 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 在完成類的加載瓷叫,解析屯吊,以及構(gòu)建完 nonstatic_oop_maps 之后送巡,就會(huì)為 Java 類在 JVM 中分配一個(gè) InstanceKlass 實(shí)例。

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

  return ik;
}

InstanceKlass::allocate_instance_klass 只是分配了一個(gè)空的 InstanceKlass 實(shí)例 盒卸,所以需要 fill_instance_klass 函數(shù)來填充 InstanceKlass 實(shí)例骗爆。在這里會(huì)將剛剛構(gòu)建好的 nonstatic_oop_maps 填充到 InstanceKlass 實(shí)例中。

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]);

  // 將構(gòu)建好的 nonstatic_oop_maps 填充到 InstanceKlass 實(shí)例中
  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());
  }
}

好了蔽介,現(xiàn)在我們已經(jīng)清楚了 JVM 如何利用這個(gè) nonstatic_oop_maps 來高效的遍歷對(duì)象的引用關(guān)系圖摘投,并且也知道了 JVM 在何時(shí) ?又是如何 虹蓄? 將 nonstatic_oop_maps 創(chuàng)建出來并填充到 InstanceKlass 實(shí)例中犀呼。

有了這些背景知識(shí)的鋪墊之后,我們再來看 Reference 語義的實(shí)現(xiàn)邏輯就很簡單了薇组。

4.3 Reference 類型的 OopMapBlock 有何不同

前面我們提到外臂,當(dāng) JVM 遍歷到一個(gè)對(duì)象并將其標(biāo)記為 alive 之后,隨后就會(huì)從這個(gè)普通 Java 對(duì)象的內(nèi)存模型中將 _klass 指針取出來律胀,對(duì)于普通的 Java 類型來說宋光,它的 _klass 指針指向的是一個(gè) InstanceKlass 實(shí)例,而對(duì)于 Reference 類型來說炭菌,它的 _klass 指針指向的是一個(gè) InstanceRefKlass 實(shí)例罪佳。

隨后會(huì)從 InstanceKlass 實(shí)例中將 nonstatic_oop_maps 數(shù)組取出來,這個(gè) nonstatic_oop_maps 是在類加載的時(shí)候被創(chuàng)建并填充到 InstanceKlass 實(shí)例中的黑低。

根據(jù) nonstatic_oop_maps 中構(gòu)建的類中所有非靜態(tài)成員變量在對(duì)象內(nèi)存中的地址偏移赘艳,JVM 可以輕松的獲取到對(duì)象中成員變量的地址,順藤摸瓜投储,再將這些非靜態(tài)成員變量引用到的對(duì)象全部標(biāo)記為 alive第练,反復(fù)循環(huán)這個(gè)邏輯,最終會(huì)將整個(gè)引用關(guān)系圖遍歷標(biāo)記完畢玛荞。

image.png

但是別忘了 Reference 類型本質(zhì)上也是一個(gè) Java 類娇掏,referent 也是 Reference 類中定義的一個(gè)非靜態(tài)成員變量。

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

如果按照這個(gè)邏輯勋眯,JVM 是不是也可以通過 nonstatic_oop_maps 獲取到 referent 的內(nèi)存地址 婴梧,近而將 Reference 引用的對(duì)象標(biāo)記為 alive 呢 ?但是現(xiàn)實(shí)是客蹋,這個(gè) referent 并沒有被 JVM 標(biāo)記到塞蹭。

image.png

這就有點(diǎn)奇怪了是吧,JVM 是怎么做到的呢 讶坯?Reference 的語義是如何實(shí)現(xiàn)的呢 番电?

我們從頭來捋一捋,現(xiàn)在的現(xiàn)象是什么 ? 是 Reference 對(duì)象的非靜態(tài)成員變量 referent 沒有被標(biāo)記到對(duì)吧漱办。那么查找一個(gè)對(duì)象的非靜態(tài)成員變量靠什么 这刷? 靠的是不是就是我們前面花了大量篇幅介紹的 OopMapBlock ?那這個(gè) OopMapBlock 從哪里來的 娩井?對(duì)于普通對(duì)象是不是在它的 InstanceKlass 實(shí)例中暇屋,對(duì)于 Reference 類型的對(duì)象是不是在它的 InstanceRefKlass 實(shí)例 中 ?

那為什么對(duì)于普通對(duì)象來說洞辣,可以通過 OopMapBlock 遍歷到它的非靜態(tài)成員變量咐刨,而對(duì)于 Reference 對(duì)象來說,就無法通過 OopMapBlock 遍歷到它的 referent 呢 扬霜?

難道是 JVM 對(duì)于 InstanceRefKlass 的 nonstatic_oop_maps 進(jìn)行了一系列的魔改定鸟,壓根就沒有為 referent 在 OopMapBlock 中建立索引 ?這樣自然就不會(huì)遍歷到 referent畜挥,也無法將它標(biāo)記為 alive 了仔粥。

事實(shí)上,JVM 就是這么干的蟹但,那么在哪里躯泰,又是如何對(duì) InstanceRefKlass 進(jìn)行魔改的呢 ?

在 JVM 啟動(dòng)的時(shí)候會(huì)對(duì) SystemDictionary 進(jìn)行初始化华糖,SystemDictionary 在 JVM 中的角色是用于管理系統(tǒng)中已經(jīng)加載的所有 class 類麦向,在 SystemDictionary 初始化的時(shí)候會(huì)調(diào)用到一個(gè)重要的函數(shù) 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());
}

從函數(shù)命名上客叉,我們就可以看出來诵竭,這里就是對(duì) InstanceRefKlass 進(jìn)行魔改的地方了。

void InstanceRefKlass::update_nonstatic_oop_maps(Klass* k) {

  // Reference 類中的 referent 字段和 discovered 字段的索引偏移從 OopMapBlock 中清除掉
  // 在后面通過 Reference 遍歷標(biāo)記成員變量的時(shí)候不需要遍歷標(biāo)記這兩個(gè)字段
  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 啟動(dòng)的時(shí)候會(huì)對(duì)所有基礎(chǔ)類進(jìn)行加載當(dāng)然也包含 Reference 類兼搏,和普通的 Java 類型一樣卵慰,Reference 類被加載之后,JVM 也會(huì)為它構(gòu)建一個(gè)全量的 nonstatic_oop_maps佛呻,里面確實(shí)也包含了所有的非靜態(tài)成員變量(referent 字段也包括在內(nèi))裳朋。

隨后就會(huì)在 update_nonstatic_oop_maps 中對(duì) InstanceRefKlass 進(jìn)行魔改。

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

首先會(huì)通過 java_lang_ref_Reference::queue_offset() 將成員變量 queue 的地址偏移取出來 —— new_offset吓著,然后將原來 OopMapBlock 的 _count 設(shè)置為 2 鲤嫡,用新的 new_offset,new_count 重新構(gòu)建 OopMapBlock绑莺。

這里 new_count 設(shè)置為 2 的意思就是暖眼,只將 Reference 類中的非靜態(tài)成員變量 queue 和 next 構(gòu)建到 OopMapBlock 中。

也就是說纺裁,當(dāng) JVM 遍歷到一個(gè) Reference 對(duì)象時(shí)诫肠,只能通過它的 OopMapBlock 遍歷到 queue 和 next,無法遍歷到 referent 和 discovered。

經(jīng)過這樣的魔改之后区赵,JVM 就巧妙地實(shí)現(xiàn)了 Reference 的語義惭缰。大家這里可以停下來回想回想 WeakReference 的語義浪南,是不是就實(shí)現(xiàn)了當(dāng)一個(gè) Java 對(duì)象只存在一條弱引用鏈的時(shí)候笼才,發(fā)生 GC 的時(shí)候,只被弱引用所關(guān)聯(lián)的對(duì)象就會(huì)被回收掉络凿。本質(zhì)原因就是這個(gè)被 JVM 魔改之后的 OopMapBlock 產(chǎn)生了作用骡送。

有同學(xué)可能會(huì)問了,你說的只是 WeakReference 的語義啊絮记,Reference 又不只是 WeakReference 這一種摔踱,還有 SoftReference,PhantomReference怨愤,F(xiàn)inalReference 這些 Reference 類型派敷,好像在這一小節(jié)中并沒有看到他們的語義實(shí)現(xiàn)。

事實(shí)上撰洗,筆者在這一小節(jié)中只是為大家揭露 Reference 最為本質(zhì)的面貌篮愉,SoftReference,PhantomReference差导,F(xiàn)inalReference 這些具體的語義都是在 WeakReference 語義的基礎(chǔ)上進(jìn)行了小小的魔改而已试躏,等筆者把該鋪墊的背景知識(shí)全部鋪墊好,后面會(huì)有單獨(dú)的小節(jié)專門為大家解釋清楚其他 Reference 類型的語義實(shí)現(xiàn)设褐。

5. JVM 在 GC 的時(shí)候如何處理 Reference

在本文的第三小節(jié)中颠蕴,我們主要在 JVM 的外圍來討論 JDK 如何通過 ReferenceHandler 線程來處理 Reference 對(duì)象,其中提到 JVM 內(nèi)部有一個(gè)非常重要的 _reference_pending_list 鏈表助析,當(dāng) Reference 的 referent 對(duì)象沒有任何強(qiáng)引用鏈或者軟引用鏈可達(dá)時(shí)犀被,GC 線程就會(huì)回收這個(gè) referent 對(duì)象。那么與之對(duì)應(yīng)的 Reference 對(duì)象就會(huì)被 JVM 采用頭插法的方式插入到這個(gè) _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 對(duì)象時(shí)寡键,ReferenceHandler 線程就會(huì)在一個(gè) native 方法 —— waitForReferencePendingList() 上阻塞等待。

當(dāng)發(fā)生 GC 的時(shí)候锥惋,JVM 就會(huì)從 GcRoot 開始挨個(gè)遍歷整個(gè)引用關(guān)系圖中的對(duì)象昌腰,并將遍歷到的對(duì)象標(biāo)記為 alive,沒有被標(biāo)記到的對(duì)象就會(huì)被 JVM 當(dāng)做垃圾回收掉膀跌。

當(dāng) referent 對(duì)象沒有被標(biāo)記到遭商,需要被 GC 線程回收的時(shí)候,JVM 就會(huì)將與它關(guān)聯(lián)的 Reference 插入到 _reference_pending_list 中捅伤,并喚醒 ReferenceHandler 線程去處理劫流,后面的內(nèi)容我們在第三小節(jié)中已經(jīng)詳細(xì)的討論過了。

image.png

本小節(jié)中,筆者將帶著大家深入到 JVM 內(nèi)部祠汇,看看發(fā)生 GC 的時(shí)候仍秤,JVM 如何處理這些 Reference 對(duì)象 ? 如何判斷哪些 Reference 需要被插入到 _reference_pending_list 中可很? 和我們前面第三小節(jié)中的內(nèi)容遙相呼應(yīng)起來诗力,這樣一來我們就從 JDK 層面再到 JVM 層面將整個(gè) Reference 的處理鏈路打通了。

下面筆者就以 ZGC 為例我抠,帶著大家看一看 JVM 內(nèi)部到底是如何處理 Reference 的 :

void ZDriver::gc(const ZDriverRequest& request) {
  ZDriverGCScope scope(request);

  // Phase 1: Pause Mark Start
  // 初始化 gc 相關(guān)的統(tǒng)計(jì)信息苇本,清空 object alocator 的緩存頁,切換地址視圖菜拓,設(shè)置標(biāo)記條帶個(gè)數(shù)
  pause_mark_start();

  // Phase 2: Concurrent Mark
  // 標(biāo)記 gc root, 標(biāo)記普通對(duì)象瓣窄,以及 Reference 對(duì)象
  // 經(jīng)過主動(dòng)刷新,被動(dòng)刷新之后纳鼎,如果標(biāo)記棧中還有對(duì)象俺夕,也不會(huì)再進(jìn)行標(biāo)記了
  // 剩下的對(duì)象標(biāo)記任務(wù)放到 pause_mark_end 中 STW 階段執(zhí)行
  concurrent(mark);

  // Phase 3: Pause Mark End 再標(biāo)記階段,標(biāo)記上一階段剩下的對(duì)象
  // zgc 低延遲的精髓贱鄙,如果 1ms 內(nèi)結(jié)束不了 STW 標(biāo)記劝贸,那么就在發(fā)起一輪 concurrent 標(biāo)記
  // 目的是降低應(yīng)用線程的停頓控制在 1ms 以內(nèi)
  while (!pause_mark_end()) {
    // 1ms 內(nèi)沒有標(biāo)記完應(yīng)用線程本地標(biāo)記棧的內(nèi)容,那么就重新開始一輪并發(fā)標(biāo)記贰逾。
    // Phase 3.5: Concurrent Mark Continue
    concurrent(mark_continue);
  }

  // Phase 4: Concurrent Mark Free
  // 釋放標(biāo)記棧資源
  concurrent(mark_free);

  // Phase 5: Concurrent Process Non-Strong References
  // 這里就是本小節(jié)討論的重點(diǎn)
  concurrent(process_non_strong_references);

  ....... 省略 .......
}

ZGC 整個(gè) GC 過程分為 10 個(gè)階段悬荣,其中只有四個(gè)階段需要非常短暫的 STW,剩下的六個(gè)階段全部是與 Java 應(yīng)用線程并發(fā)執(zhí)行的疙剑,階段雖然比較多氯迂,整個(gè) GC 過程也非常的復(fù)雜,但與本小節(jié)相關(guān)的階段只有兩個(gè)言缤,分別是第二階段的并發(fā)標(biāo)記階段 —— Phase 2: Concurrent Mark嚼蚀,與第五階段的并發(fā)處理非強(qiáng)引用 Reference 階段 —— Phase 5: Concurrent Process Non-Strong References

其中 Concurrent Mark 主要的任務(wù)就是從 GcRoot 開始并發(fā)標(biāo)記根對(duì)象管挟,并沿著根對(duì)象遍歷整個(gè)堆中的引用關(guān)系轿曙,在整個(gè)遍歷的過程中會(huì)逐漸發(fā)現(xiàn)那些需要被 ReferenceHandler 線程處理的 Reference 對(duì)象,隨后會(huì)將這些 Reference 對(duì)象插入到 _discovered_list 中僻孝。

這里大家可能會(huì)有疑問导帝,你剛才不是說 JVM 會(huì)將需要被處理的 Reference 對(duì)象插入到 _reference_pending_list 中嗎 ?怎么現(xiàn)在又變成 _discovered_list 了 穿铆?

事實(shí)上您单,大家可以將 _discovered_list 理解為一個(gè)臨時(shí)的 _reference_pending_list,在 ZGC 的整個(gè)過程中會(huì)用到兩個(gè)臨時(shí)的 _reference_pending_list荞雏,它們分別是 _discovered_list虐秦,_pending_list平酿。

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

ZGC 有多個(gè) GC 線程負(fù)責(zé)并發(fā)執(zhí)行垃圾回收任務(wù),_discovered_list 是 ZPerWorker 類型的悦陋,每一個(gè) GC 線程都有一個(gè) _discovered_list蜈彼,負(fù)責(zé)臨時(shí)存儲(chǔ)由該 GC 線程在并發(fā)標(biāo)記過程中發(fā)現(xiàn)的 Reference 對(duì)象。

在并發(fā)標(biāo)記結(jié)束之后俺驶,這些 GC 線程就會(huì)將各自在 _discovered_list 中收集到的 Reference 對(duì)象統(tǒng)一轉(zhuǎn)移到 _pending_list 中幸逆,_pending_list 在所有 GC 線程中是共享的,負(fù)責(zé)匯總 ZGC 線程收集到的所有 Reference 對(duì)象痒钝。

Concurrent Process Non-Strong References 階段的最后秉颗,JVM 會(huì)將 _pending_list 中匯總的 Reference 對(duì)象再次統(tǒng)一轉(zhuǎn)移到 _reference_pending_list 中,_reference_pending_list 是最終對(duì)外的發(fā)布形態(tài)送矩,ReferenceHandler 線程只會(huì)和 _reference_pending_list 打交道。

理解了這個(gè)背景哪替,下面我們就來一起看下 Concurrent Mark 階段是如何發(fā)現(xiàn) Reference 對(duì)象的

5.1 Concurrent Mark

當(dāng) ZGC 遍歷到一個(gè)對(duì)象 —— oop obj 并將其標(biāo)記為 alive 之后栋荸,就會(huì)調(diào)用 follow_object 方法,來遍歷 obj 的所有非靜態(tài)成員變量凭舶,然后將這些成員變量所引用的 obj 標(biāo)記為 alive晌块,然后再次調(diào)用 follow_object 繼續(xù)遍歷引用關(guān)系圖,這樣循環(huán)往復(fù)帅霜。 ZGC 就是靠著這個(gè) follow_object 方法驅(qū)動(dòng)著所有 GC 線程去遍歷整個(gè)堆的引用關(guān)系圖匆背。

void ZMark::follow_object(oop obj, bool finalizable) {
  if (finalizable) {
    ZMarkBarrierOopClosure<true /* finalizable */> cl;
    obj->oop_iterate(&cl);
  } else {
    // 最終的標(biāo)記邏輯是在這個(gè)閉包中完成的
    ZMarkBarrierOopClosure<false /* finalizable */> cl;
    // 遍歷標(biāo)記 obj 的所有非靜態(tài)成員變量
    obj->oop_iterate(&cl);
  }
}

這里就來到了筆者在第四小節(jié)中介紹的內(nèi)容,這個(gè)函數(shù)熟悉嗎 身冀?沒印象的話再去回顧下第四小節(jié)钝尸。

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

首先會(huì)通過 klass() 函數(shù)去獲取 obj 中的 _klass 指針,對(duì)于普通類型的 Java 對(duì)象來說搂根,_klass 指向的是 InstanceKlass 實(shí)例珍促,對(duì)于 Reference 類型的對(duì)象來說,_klass 指向的是 InstanceRefKlass 實(shí)例剩愧。

最終的遍歷動(dòng)作是在對(duì)應(yīng) Klass 中的 oop_oop_iterate 方法中進(jìn)行的猪叙,本小節(jié)我們重點(diǎn)關(guān)注 InstanceRefKlass。

template <typename T, class OopClosureType>
void InstanceRefKlass::oop_oop_iterate(oop obj, OopClosureType* closure) {
  // 遍歷 Reference 對(duì)象的非靜態(tài)成員變量仁卷,注意這里 referent 字段和 discovered 字段是不會(huì)被遍歷到的
  InstanceKlass::oop_oop_iterate<T>(obj, closure);
  // 判斷該 Reference 對(duì)象是否需要加入到 _discovered_list 中
  oop_oop_iterate_ref_processing<T>(obj, closure);
}

首先會(huì)調(diào)用 InstanceKlass::oop_oop_iterate 函數(shù)穴翩,這個(gè)函數(shù)熟悉嗎 ?我們在第四小節(jié)中重點(diǎn)介紹的就是這個(gè)函數(shù)锦积。

在這個(gè)函數(shù)中獲取 InstanceRefKlass 實(shí)例中的 nonstatic_oop_maps芒帕,通過 OopMapBlock 去遍歷標(biāo)記 Reference 對(duì)象非靜態(tài)成員變量。

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 經(jīng)過特殊魔改的副签,這里并不會(huì)遍歷到 Reference 對(duì)象的 referent 字段和 discovered 字段遥椿。

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

在遍歷標(biāo)記完 Reference 對(duì)象的非靜態(tài)成員變量之后,JVM 會(huì)調(diào)用
oop_oop_iterate_ref_processing 來判斷該 Reference 對(duì)象是否應(yīng)該插入到 _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:
      // 執(zhí)行這里的 discovery 邏輯冠场,發(fā)現(xiàn)需要被處理的 Reference 對(duì)象
      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 對(duì)象已經(jīng)被加入到 _discovered_list 中了
    // 加入到 _discovered_list 的條件是:
    // 1. referent 沒有被標(biāo)記,說明不活躍
    // 2. Reference 對(duì)象之前沒有被添加到 _discovered_list(第一次添加)
    return;
  }

}

在 try_discover 中本砰,JVM 首先會(huì)通過 load_referent 從堆中加載 Reference 引用的 referent 對(duì)象碴裙。這里會(huì)判斷 referent 對(duì)象是否已經(jīng)被 GC 線程標(biāo)記過了,如果已經(jīng)被標(biāo)記了点额,說明 referent 是 alive 的舔株,那么這個(gè) Reference 對(duì)象就不需要被放入 _discovered_list 中,直接 return 掉还棱。

如果 referent 沒有被標(biāo)記载慈,則進(jìn)入 ZReferenceProcessor->discover_reference 函數(shù)中作進(jìn)一步的 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 對(duì)象的 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 對(duì)象是否需要被 ReferenceHandler 線程處理

  2. 如果 Reference 對(duì)象需要被處理的話就通過 discover 方法老虫,將其插入到 _discovered_list 中模孩。

bool ZReferenceProcessor::discover_reference(oop reference, ReferenceType type) {

  // true : 表示 referent 還存活(被強(qiáng)引用或者軟引用關(guān)聯(lián))露懒,那么就不能放到 _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 中的邏輯依據(jù)主要有三個(gè)方面:

如果 Reference 對(duì)象的狀態(tài)是 inactive,那么 JVM 就不會(huì)將它放入 _discovered_list 中稚补。那么什么時(shí)候 Reference 對(duì)象會(huì)變?yōu)? inactive 呢 童叠?

比如,應(yīng)用線程自己調(diào)用 Reference.enqueue() 方法课幕,自己親自將 Reference 對(duì)象添加到與其關(guān)聯(lián)的 ReferenceQueue 中等待進(jìn)一步的處理厦坛。那么這里 JVM 就不需要將 Reference 添加到 _discovered_list 中了。

因?yàn)樽罱K ReferenceHandler 線程還是會(huì)從 _reference_pending_list 中將 Reference 添加到 ReferenceQueue 中撰豺,這樣一來就重復(fù)了粪般。應(yīng)用線程在調(diào)用 Reference.enqueue() 方法之后,Reference 的狀態(tài)就變?yōu)榱?inactive 污桦。

還有一種變?yōu)?inactive 的情況就是應(yīng)用線程直接調(diào)用 Reference.clear() 方法亩歹,表示應(yīng)用線程自己已經(jīng)處理過 Reference 對(duì)象了,JVM 就別管了凡橱,此時(shí) Reference 的狀態(tài)變?yōu)?inactive , 那么在下一輪 GC 的時(shí)候該 Reference 對(duì)象就會(huì)被回收小作,并且不會(huì)再次被添加到 _discovered_list 中。

這也就解釋了為什么 Reference 狀態(tài)變?yōu)?inactive 之后稼钩,JVM 將不會(huì)再次將其放入 _discovered_list 的原因了顾稀,因?yàn)樗呀?jīng)被處理過了。處于 inactive 狀態(tài)的 Reference 有一個(gè)共同的特點(diǎn)就是它的 referent = null坝撑。

第二個(gè)條件是如果它的 referent 仍然存在強(qiáng)引用鏈静秆,那么這個(gè) Reference 將不會(huì)被放入 _discovered_list粮揉。

第三個(gè)條件是如果它的 referent 仍然存在軟引用鏈,也就是還被軟引用所關(guān)聯(lián)抚笔,如果此時(shí)內(nèi)存充足扶认,軟引用不會(huì)被回收的話,那么這個(gè) Reference 也不會(huì)被放入 _discovered_list殊橙。

bool ZReferenceProcessor::should_discover(oop reference, ReferenceType type) const {
  // 獲取 referent 對(duì)象的地址視圖
  volatile oop* const referent_addr = reference_referent_addr(reference);
  // 調(diào)整 referent 對(duì)象的地址視圖為 remapped + mark0 也就是 weakgood 視圖
  // 表示該 referent 對(duì)象目前只能通過弱引用鏈訪問到辐宾,而不能通過強(qiáng)引用鏈訪問到
  // 注意這里是調(diào)整 referent 的視圖而不是調(diào)整 Reference 的視圖
  const oop referent = ZBarrier::weak_load_barrier_on_oop_field(referent_addr);

  // 此時(shí) Reference 的狀態(tài)就是 inactive,那么這里將不會(huì)重復(fù)將 Reference 添加到 _discovered_list 重復(fù)處理
  if (is_inactive(reference, referent, type)) {
    return false;
  }
  // referent 還被強(qiáng)引用關(guān)聯(lián)膨蛮,那么 return false 也就是說不能被加入到 discover list 中
  if (is_strongly_live(referent)) {
    return false;
  }
  // referent 還被軟引用有效關(guān)聯(lián)叠纹,那么 return false 也就是說不能被加入到 discover list 中
  if (is_softly_live(reference, type)) {
    return false;
  }

  return true;
}

如果 Reference 對(duì)象的 referent 在當(dāng)前堆中已經(jīng)沒有任何強(qiáng)引用或者軟引用了,并且該 Reference 對(duì)象不是 inactive 狀態(tài)的敞葛,那么 JVM 就會(huì)將該 Reference 對(duì)象通過下面的 discover 方法插入到 _discovered_list 中(頭插法)誉察。

void ZReferenceProcessor::discover(oop reference, ReferenceType type) {
  // Add reference to discovered list
  // 確保 reference 不在 _discovered_list 中,不能重復(fù)添加
  assert(reference_discovered(reference) == NULL, "Already discovered");
  oop* const list = _discovered_list.addr();
  // 頭插法制肮,reference->discovered = *list
  reference_set_discovered(reference, *list);
  // reference 變?yōu)?_discovered_list 的頭部
  *list = reference;
}

從以上過程我們可以看出冒窍,在 ZGC 的 Concurrent Mark 階段, Reference 對(duì)象被 JVM 添加到 _discovered_list 中需要同時(shí)符合下面四個(gè)條件:

  1. Reference 對(duì)象引用的 referent 沒有被 GC 標(biāo)記過豺鼻。
  2. Reference 對(duì)象的狀態(tài)不能是 inactive, 也就是說這個(gè) Reference 還沒有被應(yīng)用線程處理過,Reference 之前沒有加入過 _discovered_list款慨。
  3. referent 不存在任何強(qiáng)引用鏈儒飒。
  4. referent 不存在任何軟引用鏈。
image.png

好了檩奠,現(xiàn)在 Reference 在 Concurrent Mark 階段的處理過程桩了,筆者就為大家介紹完了,這里需要注意的是埠戳,目前 _discovered_list 中收集到的 Reference 都只是臨時(shí)的井誉,因?yàn)楫?dāng)前所處的階段為并發(fā)標(biāo)記階段,應(yīng)用線程和 GC 線程是并發(fā)執(zhí)行的整胃,再加上標(biāo)記階段還沒有結(jié)束颗圣,所以 Reference 加入到 _discovered_list 的條件可能隨時(shí)會(huì)被應(yīng)用線程和 GC 線程再次改變。

_discovered_list 終態(tài)的確定需要等到并發(fā)標(biāo)記階段完全結(jié)束屁使,在 ZGC 的第五階段 —— Concurrent Process Non-Strong References 進(jìn)行最終的處理在岂。

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 階段對(duì)于 Reference 的最終處理是在 ZReferenceProcessor 中完成的,其中主要包括兩個(gè)核心步驟:

首先在 process_references() 函數(shù)中蛮寂,判斷 ZGC 在 Concurrent Mark 階段的 _discovered_list 中收集到的臨時(shí) Reference 對(duì)象所引用的 referent 是否存活蔽午,如果這些 referent 仍然存活,那么就需要將對(duì)應(yīng)的 Reference 對(duì)象從 _discovered_list 中移除酬蹋。

如果這些 referent 不再存活及老,那么就將與其關(guān)聯(lián)的 Reference 對(duì)象繼續(xù)保留在 _discovered_list抽莱,最后將 _discovered_list 中依然保留的 Reference 對(duì)象添加到 _pending_list 中,然后清空 _discovered_list骄恶。

第二個(gè)步驟就是在 enqueue_references() 函數(shù)中食铐,將最終確定下來的 _pending_list 再次添加到 _reference_pending_list 中,隨后喚醒 ReferenceHandler 線程去處理 _reference_pending_list 中的 Reference 對(duì)象叠蝇,最后清空 _pending_list璃岳,為下一輪 GC 做準(zhǔn)備。

以上就是 ZGC 對(duì)于 Non-Strong References 的總體處理流程悔捶,下面我們就來看下這兩個(gè)核心步驟中的具體處理細(xì)節(jié):

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

process_references() 對(duì)于 _discovered_list 的處理邏輯被封裝在一個(gè) ZReferenceProcessorTask 中铃慷,由所有 GC 線程來一起并發(fā)執(zhí)行這個(gè) Task。

void ZReferenceProcessor::work() {
  // Process discovered references
  oop* const list = _discovered_list.addr();
  oop* p = list;

  // 循環(huán)遍歷 _discovered_list蜕该,檢查之前收集到的 Reference 對(duì)象的 referent 是否存活
  while (*p != NULL) {
    const oop reference = *p;
    const ReferenceType type = reference_type(reference);
    // 如果該 reference 已經(jīng)被應(yīng)用程序處理過了 -> referent == NULL, 那么就不需要再被處理了犁柜,直接丟棄
    // 如果 referent 依然存活,那么也要丟棄堂淡,不能放入 _discovered_list 中
    if (should_drop(reference, type)) {
      // 如果 referent 是 alive 的或者在上一輪 GC 中已經(jīng)被處理過馋缅,則將 reference 從 _discovered_list 中刪除
      *p = drop(reference, type);
    } else {
      // 如果 referent 不是 alive 的,在并發(fā)標(biāo)記階段沒有被標(biāo)記绢淀,那么就讓它繼續(xù)留在 _discovered_list 中
      // 這里會(huì)調(diào)用 reference 的 clear 方法 -> referent 置為 null
      // 返回 reference 在 _discovered_list 中的下一個(gè)對(duì)象萤悴,繼續(xù) while 循環(huán)
      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 是一個(gè) ZPerWorker 類型的皆的,每一個(gè) GC 線程對(duì)應(yīng)一個(gè)覆履,用于在 Concurrent Mark 階段并發(fā) discover Reference。

循環(huán)遍歷 _discovered_list费薄,挨個(gè)獲取鏈表中收集到的臨時(shí) Reference硝全,通過 should_drop 方法判斷是否需要將 Reference 對(duì)象從 _discovered_list 中移除。移除條件有兩個(gè):

  1. 如果 Reference 對(duì)象的 referent 被置為 null , 那么就需要將這里的 Reference 對(duì)象移除掉楞抡。因?yàn)樵?Reference 被放入到 _pending_list 之前伟众,JVM 會(huì)主動(dòng)調(diào)用 Reference 對(duì)象的 clear 方法,將 referent 置空凳厢。referent = null 代表的語義是這個(gè) Reference 之前已經(jīng)被添加到 _discovered_list 中了柱恤,比如在上一輪 GC 中就已經(jīng)被處理了数初,本輪 GC 直接將 Reference 對(duì)象回收掉就好了,不需要再重復(fù)添加到 _discovered_list梗顺。

  2. 如果 Reference 對(duì)象在前幾輪 GC 沒有被處理過泡孩,是在本輪 GC 中新發(fā)現(xiàn)的,那么就繼續(xù)判斷它的 referent 是否還存活寺谤,如果仍然存活的話仑鸥,就將 Reference 對(duì)象移除吮播,因?yàn)?referent 還活著,自然也不需要被 ReferenceHandler 線程處理

bool ZReferenceProcessor::should_drop(oop reference, ReferenceType type) const {
  // 獲取 Reference 所引用的 referent
  const oop referent = reference_referent(reference);
  // 如果 Reference 對(duì)象在上一輪 GC 中被處理過或者已經(jīng)被應(yīng)用線程自己處理了眼俊,那么本輪 GC 直接回收掉
  // 不會(huì)再將 Reference 對(duì)象重復(fù)添加到 _discovered_list
  if (referent == NULL) {
    return true;
  }
  
  // 如果 referent 仍然存活意狠,那么也會(huì)將 Reference 對(duì)象移除,不需要被 ReferenceHandler 線程處理
  if (type == REF_PHANTOM) {
    // 針對(duì) PhantomReference 對(duì)象的特殊處理疮胖,后面在專門的小節(jié)中講解环戈,這里先忽略
    return ZBarrier::is_alive_barrier_on_phantom_oop(referent);
  } else {
    // 本小節(jié)我們重點(diǎn)關(guān)注這個(gè)分支,主要就是判斷這個(gè) referent 是否被標(biāo)記為 alive
    return ZBarrier::is_alive_barrier_on_weak_oop(referent);
  }
}

這里大家可能就有點(diǎn)懵了澎灸,因?yàn)楣P者前面介紹過院塞,在 ZGC 的 Concurrent Mark 階段, Reference 對(duì)象被 JVM 添加到 _discovered_list 中的條件就是這個(gè) Reference 對(duì)象的 referent 沒有被標(biāo)記過汹族。那為什么這里又要判斷一下呢 顶瞒?我們來看一個(gè)這樣的場景:

image.png

上圖中展示的這個(gè)場景是法绵,一個(gè) object 對(duì)象在 JVM 堆中同時(shí)被一個(gè) StrongReference 對(duì)象和一個(gè) WeakReference 對(duì)象所引用。

假設(shè)在 ZGC 的 Concurrent Mark 階段,GC 線程先遍歷到 WeakReference 對(duì)象狡赐,注意此時(shí)還沒有遍歷到 StrongReference 對(duì)象。由于還沒有遍歷到 StrongReference 搀擂,所以這個(gè) object 對(duì)象還沒有被標(biāo)記為 alive喷市。

而對(duì)于 WeakReference 對(duì)象來說,GC 線程并不會(huì)遍歷標(biāo)記它的 referent腹备,對(duì)吧,這是我們第四小節(jié)中的內(nèi)容了。這時(shí)這個(gè) WeakReference 對(duì)象就會(huì)被 JVM 添加到 _discovered_list 中喊儡。

好的,我們繼續(xù) Concurrent Mark匆赃,后面 GC 線程最終是要遍歷到 StrongReference 對(duì)象的,對(duì)吧。當(dāng) GC 線程遍歷到 StrongReference 對(duì)象的時(shí)候首先會(huì)標(biāo)記這個(gè) StrongReference 對(duì)象為 alive囱淋,隨后開始遍歷它的所有非靜態(tài)成員變量,逐個(gè)進(jìn)行標(biāo)記 alive涮较,在這個(gè)過程中 object 對(duì)象最終也會(huì)被標(biāo)記為 alive。

當(dāng) Concurrent Mark 結(jié)束之后闺属,我們來到了本小節(jié)的 Concurrent Process Non-Strong References 階段俱箱,那么對(duì)于此時(shí)被添加到 _discovered_list 中的這個(gè) WeakReference 對(duì)象是不是就不對(duì)了乃摹,因?yàn)樗?referent 后面又被標(biāo)記為 alive 了,所以在 should_drop 函數(shù)的最后還是要通過 is_alive_barrier_on_weak_oop 判斷一下 referent 是否被標(biāo)記,如果被標(biāo)記過了蹈集,那么就需要將這個(gè) WeakReference 對(duì)象從 _discovered_list 中移除。

了解了這個(gè)背景辩蛋,我們再來看 ZReferenceProcessor::work 中的處理邏輯就很清晰了伤为。首先 GC 線程會(huì)在 while (*p != NULL) 循環(huán)中不停的遍歷 _discovered_list 中臨時(shí)存放的這些 Reference 對(duì)象。

然后通過 should_drop 判斷這個(gè) Reference 對(duì)象是否應(yīng)該從 _discovered_list 中移除裆蒸,如果 should_drop 返回 true ,那么 JVM 就會(huì)通過 drop 方法將 Reference 對(duì)象移除。很簡單的鏈表操作装哆,這里筆者就不展開了。

如果 should_drop 返回 false, JVM 就會(huì)讓這個(gè) Reference 對(duì)象繼續(xù)保留在 _discovered_list 中号醉,并調(diào)用 keep 方法獲取該 Reference 對(duì)象在 _discovered_list 中的下一個(gè)元素线椰,繼續(xù)進(jìn)行 while 循環(huán)重復(fù)上述的判斷邏輯。

oop* ZReferenceProcessor::keep(oop reference, ReferenceType type) {

  // 入隊(duì)計(jì)數(shù)加 1
  _enqueued_count.get()[type]++;

  // 將 referent 置為 null ,此后 Reference 就變?yōu)榱?inactive
  make_inactive(reference, type);

  // 從 _discovered_list 中獲取下一個(gè) Reference 繼續(xù)循環(huán)
  return reference_discovered_addr(reference);
}

keep 方法中會(huì)調(diào)用一個(gè) make_inactive 方法,JVM 在這里會(huì)調(diào)用 Reference 對(duì)象的 clear 方法將 referent 置為 null 植袍。

void ZReferenceProcessor::make_inactive(oop reference, ReferenceType type) const {
  if (type == REF_FINAL) {
      ..... 省略 FinalReference 的處理 ....
  } else {
    // 這里調(diào)用 Reference 對(duì)象的 clear 方法將,referent 置為 null
    reference_clear_referent(reference);
  }
}

那么此時(shí)如果我們在應(yīng)用線程中調(diào)用這個(gè) Reference 對(duì)象的 get() 方法的時(shí)候就會(huì)得到一個(gè) null 值,referent 對(duì)象被 JVM 置為 null 的時(shí)機(jī)就是這個(gè) Reference 對(duì)象確定要被添加到 _pending_list 的時(shí)候贷笛。

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

當(dāng) _discovered_list 中的那些所有需要被移除的 Reference 對(duì)象都已經(jīng)被移除之后,JVM 就會(huì)將終態(tài)的 _discovered_list 原子地添加到 _pending_list 中。

Concurrent Process Non-Strong References 階段的最后旬蟋,ZGC 就會(huì)調(diào)用 enqueue_references 方法將 _pending_list 中的 Reference 對(duì)象轉(zhuǎn)移到 _reference_pending_list 中。最后重置 pending list拦惋,為下一輪 GC 做準(zhǔn)備匆浙。

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 做準(zhǔn)備
        _pending_list.set(NULL);
        _pending_list_tail = _pending_list.addr();
    }

這里我們看到 ZGC 在更新完 _reference_pending_list 之后厕妖,會(huì)調(diào)用一個(gè) ml.notify_all()首尼,那么這個(gè)操作是要喚醒誰呢 言秸?或者說誰會(huì)在 Heap_lock 上等待呢 合是?

還記不記得筆者在第三小節(jié)中為大家介紹的 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 對(duì)象蛾茉,那么當(dāng)前線程在 Heap_lock 上 wait
    ml.wait();
  }
JVM_END

那么誰會(huì)在這個(gè)方法上阻塞等待呢 看蚜?答案就是 —— ReferenceHandler 線程纽竣。

private static class ReferenceHandler extends Thread {

  private static void processPendingReferences() {

        waitForReferencePendingList();

        ........ 省略 .....
  }
}

至于 ReferenceHandler 線程被喚醒之后干了什么 港令? 這不就是筆者在第三小節(jié)中詳細(xì)為大家介紹的內(nèi)容么朵纷,這樣一來是不是就和前面的內(nèi)容遙相呼應(yīng)起來了~~~

image.png

6. SoftReference 具體在什么時(shí)候被回收 ? 如何量化內(nèi)存不足 呛谜?

大家在網(wǎng)上或者在其他講解 JVM 的書籍中多多少少會(huì)看到這樣一段關(guān)于 SoftReference 的描述 —— “當(dāng) SoftReference 所引用的 referent 對(duì)象在整個(gè)堆中沒有其他強(qiáng)引用的時(shí)候对妄,發(fā)生 GC 的時(shí)候,如果此時(shí)內(nèi)存充足,那么這個(gè) referent 對(duì)象就和其他強(qiáng)引用一樣,不會(huì)被 GC 掉漓库,如果此時(shí)內(nèi)存不足怠蹂,系統(tǒng)即將 OOM 之前,那么這個(gè) referent 對(duì)象就會(huì)被當(dāng)做垃圾回收掉”妒峦。

image.png

當(dāng)然了胚鸯,如果僅從概念上理解的話昔期,這樣描述就夠了十艾,但是如果我們從 JVM 的實(shí)現(xiàn)角度上來說抵代,那這樣的描述至少是不準(zhǔn)確的,為什么呢 忘嫉? 筆者先提兩個(gè)問題出來荤牍,大家可以先思考下:

  1. 內(nèi)存充足的情況下,SoftReference 所引用的 referent 對(duì)象就一定不會(huì)被回收嗎 庆冕?

  2. 什么是內(nèi)存不足 参淫?這個(gè)概念如何量化,SoftReference 所引用的 referent 對(duì)象到底什么時(shí)候被回收 愧杯?

下面筆者繼續(xù)以 ZGC 為例,帶大家深入到 JVM 內(nèi)部去探尋下這兩個(gè)問題的精確答案~~

6.1 JVM 無條件回收 SoftReference 的場景

經(jīng)過前面第五小節(jié)的介紹鞋既,我們知道 ZGC 在 Concurrent Mark 以及 Concurrent Process Non-Strong References 階段中處理 Reference 對(duì)象的關(guān)鍵邏輯都封裝在 ZReferenceProcessor 中力九。

在 ZReferenceProcessor 中有一個(gè)關(guān)鍵的屬性 —— _soft_reference_policy,在 ZGC 的過程中邑闺,處理 SoftReference 的策略就封裝在這里跌前,本小節(jié)開頭提出的那兩個(gè)問題的答案就隱藏在 _soft_reference_policy 中。

class ZReferenceProcessor : public ReferenceDiscoverer {
  // 關(guān)于 SoftReference 的處理策略
  ReferencePolicy*     _soft_reference_policy;
}

那下面的問題就是如果我們能夠知道 _soft_reference_policy 的初始化邏輯陡舅,那是不是關(guān)于 SoftReference 的一切疑惑就迎刃而解了 抵乓?我們來一起看下 _soft_reference_policy 的初始化過程。

在 ZGC 開始的時(shí)候靶衍,首先會(huì)創(chuàng)建一個(gè) ZDriverGCScope 對(duì)象灾炭,這里主要進(jìn)行一些 GC 的準(zhǔn)備工作,比如更新 GC 的相關(guān)統(tǒng)計(jì)信息颅眶,設(shè)置并行 GC 線程個(gè)數(shù)蜈出,以及本小節(jié)的重點(diǎn),初始化 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 之前铡原,會(huì)調(diào)用一個(gè)重要的方法 —— should_clear_soft_references,本小節(jié)的答案就在這里商叹,該方法就是用來判斷燕刻,ZGC 是否需要無條件清理 SoftReference 所引用的 referent 對(duì)象。

  • 返回 true 表示剖笙,在 GC 的過程中只要遇到 SoftReference 對(duì)象卵洗,那么它引用的 referent 對(duì)象就會(huì)被當(dāng)做垃圾清理,SoftReference 對(duì)象也會(huì)被 JVM 加入到 _reference_pending_list 中等待 ReferenceHandler 線程去處理枯途。這里就和 WeakReference 的語義一樣了忌怎。

  • 返回 false 表示籍滴,內(nèi)存充足的時(shí)候,JVM 就會(huì)把 SoftReference 當(dāng)做普通的強(qiáng)引用一樣處理榴啸,它所引用的 referent 對(duì)象不會(huì)被回收孽惰,但內(nèi)存不足的時(shí)候,被 SoftReference 所引用的 referent 對(duì)象就會(huì)被回收鸥印,SoftReference 也會(huì)被加入到 _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 過程中就會(huì)無條件地清理 SoftReference 。

  1. 引起 GC 的原因是 —— _wb_full_gc潜的,也就是由 WhiteBox 相關(guān) API 觸發(fā)的 Full GC骚揍,就會(huì)無條件清理 SoftReference。

  2. 引起 GC 的原因是 —— _metadata_GC_clear_soft_refs啰挪,也就是在元數(shù)據(jù)分配失敗的時(shí)候觸發(fā)的 Full GC信不,元空間內(nèi)存不足,情況就很嚴(yán)重了亡呵,所以要無條件清理 SoftReference抽活。

  3. 引起 GC 的原因是 —— _z_allocation_stall,在 ZGC 采用阻塞模式分配 Zpage 頁面的時(shí)候锰什,如果內(nèi)存不足無法分配下硕,那么就會(huì)觸發(fā)一次 GC,這時(shí) GC 的觸發(fā)原因就是 _z_allocation_stall汁胆,這種情況下就會(huì)無條件清理 SoftReference梭姓。

ZGC 非阻塞模式分配 Zpage 的時(shí)候如果內(nèi)存不足、就直接拋出 OutOfMemoryError嫩码,不會(huì)啟動(dòng) GC 糊昙。

ZPage* ZPageAllocator::alloc_page(uint8_t type, size_t size, ZAllocationFlags flags) {
  EventZPageAllocation event;

retry:
  ZPageAllocation allocation(type, size, flags);
  // 判斷是否進(jìn)行阻塞分配 ZPage
  if (!alloc_page_or_stall(&allocation)) {
    // 如果非阻塞分配  ZPage 失敗,直接 Out of memory
    return NULL;
  }
}

在我們了解了這個(gè)背景之后谢谦,在回頭來看下 _soft_reference_policy 的初始化過程 :

參數(shù) clear 就是 should_clear_soft_references 函數(shù)的返回值

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 : 當(dāng) clear 為 true 的時(shí)候释牺,ZGC 就會(huì)采用這種策略,在 GC 的過程中只要遇到 SoftReference回挽,就會(huì)無條件回收其引用的 referent 對(duì)象没咙,SoftReference 對(duì)象也會(huì)被 JVM 加入到 _reference_pending_list 中等待 ReferenceHandler 線程去處理。

  2. lru_max_heap_policy :當(dāng) clear 為 false 的時(shí)候千劈,ZGC 就會(huì)采用這種策略祭刚,這種情況下 SoftReference 的存活時(shí)間取決于 JVM 堆中剩余可用內(nèi)存的總大小,也是我們下一小節(jié)中討論的重點(diǎn)。

下面我們就來看一下 lru_max_heap_policy 的初始化過程涡驮,看看 JVM 是如何量化內(nèi)存不足的 ~~

6.2 JVM 如何量化內(nèi)存不足

LRUMaxHeapPolicy 的 setup() 方法主要用來確定被 SoftReference 所引用的 referent 對(duì)象最大的存活時(shí)間暗甥,這個(gè)存活時(shí)間是和堆的剩余空間大小有關(guān)系的,也就是堆的剩余空間越大 SoftReference 的存活時(shí)間就越長捉捅,堆的剩余空間越小 SoftReference 的存活時(shí)間就越短撤防。

void LRUMaxHeapPolicy::setup() {
  size_t max_heap = MaxHeapSize;
  // 獲取最近一次 gc 之后,JVM 堆的最大剩余空間
  max_heap -= Universe::heap()->used_at_last_gc();
  // 轉(zhuǎn)換為 MB
  max_heap /= M;
  //  -XX:SoftRefLRUPolicyMSPerMB 默認(rèn)為 1000 棒口,單位毫秒
  // 表示每 MB 的剩余內(nèi)存空間中允許 SoftReference 存活的最大時(shí)間
  _max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
  assert(_max_interval >= 0,"Sanity check");
}

JVM 首先會(huì)獲取我們通過 -Xmx 參數(shù)指定的最大堆 —— MaxHeapSize寄月,然后在通過 Universe::heap()->used_at_last_gc() 獲取上一次 GC 之后 JVM 堆占用的空間,兩者相減无牵,就得到了當(dāng)前 JVM 堆的最大剩余內(nèi)存空間漾肮,并將單位轉(zhuǎn)換為 MB

現(xiàn)在 JVM 堆的剩余空間我們計(jì)算出來了茎毁,那如何根據(jù)這個(gè) max_heap 計(jì)算 SoftReference 的最大存活時(shí)間呢 克懊?

這里就用到了一個(gè) JVM 參數(shù) —— SoftRefLRUPolicyMSPerMB,我們可以通過 -XX:SoftRefLRUPolicyMSPerMB 來指定七蜘,默認(rèn)為 1000 保檐, 單位為毫秒。

它表達(dá)的意思是每 MB 的堆剩余內(nèi)存空間允許 SoftReference 存活的最大時(shí)長崔梗,比如當(dāng)前堆中只剩余 1MB 的內(nèi)存空間,那么 SoftReference 的最大存活時(shí)間就是 1000 ms垒在,如果剩余內(nèi)存空間為 2MB蒜魄,那么 SoftReference 的最大存活時(shí)間就是 2000 ms 。

現(xiàn)在我們剩余 max_heap 的空間场躯,那么在本輪 GC 中谈为,SoftReference 的最大存活時(shí)間就是 —— _max_interval = max_heap * SoftRefLRUPolicyMSPerMB

從這里我們可以看出 SoftReference 的最大存活時(shí)間 _max_interval踢关,取決于兩個(gè)因素:

  1. 當(dāng)前 JVM 堆的最大剩余空間伞鲫。

  2. 我們指定的 -XX:SoftRefLRUPolicyMSPerMB 參數(shù)值,這個(gè)值越大 SoftReference 存活的時(shí)間就越久签舞,這個(gè)值越小秕脓,SoftReference 存活的時(shí)間就越短。

在我們得到了這個(gè) _max_interval 之后儒搭,那么 JVM 是如何量化內(nèi)存不足呢 吠架?被 SoftReference 引用的這個(gè) referent 對(duì)象到底什么被回收 ?讓我們再次回到 JDK 中搂鲫,來看一下 SoftReference 的實(shí)現(xiàn):

public class SoftReference<T> extends Reference<T> {
    // 由 JVM 來設(shè)置傍药,每次 GC 發(fā)生的時(shí)候,JVM 都會(huì)記錄一個(gè)時(shí)間戳到這個(gè) clock 字段中
    private static long clock;
    // 表示應(yīng)用線程最近一次訪問這個(gè) SoftReference 的時(shí)間戳(當(dāng)前的 clock 值)
    // 在 SoftReference 的 get 方法中設(shè)置
    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 發(fā)生時(shí)間設(shè)置到 timestamp 中
            // 用這個(gè)表示當(dāng)前 SoftReference 最近被訪問的時(shí)間戳
            // 注意這里的時(shí)間戳語義是 最近一次的 gc 時(shí)間
            this.timestamp = clock;
        return o;
    }
}

SoftReference 中有兩個(gè)非常重要的字段,一個(gè)是 clock 拐辽,另一個(gè)是 timestamp拣挪。clock 字段是由 JVM 來設(shè)置的,在每一次發(fā)生 GC 的時(shí)候俱诸,JVM 都會(huì)去更新這個(gè)時(shí)間戳菠劝。具體一點(diǎn)的話,就是在 ZGC 的 Concurrent Process Non-Strong References 階段處理完所有 Reference 對(duì)象之后乙埃,JVM 就會(huì)來更新這個(gè) clock 字段闸英。

void ZReferenceProcessor::process_references() {
  ZStatTimer timer(ZSubPhaseConcurrentReferencesProcess);

  // Process discovered lists
  ZReferenceProcessorTask task(this);
  // gc _workers 一起運(yùn)行 ZReferenceProcessorTask
  _workers->run(&task);

  // Update SoftReference clock
  soft_reference_update_clock();
}

soft_reference_update_clock() 中 ,JVM 會(huì)將 SoftReference 類中的 clock 字段更新為當(dāng)前時(shí)間戳介袜,單位為毫秒甫何。

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

而 timestamp 字段用來表示這個(gè) SoftReference 對(duì)象有多久沒有被訪問到了,應(yīng)用線程越久沒有訪問 SoftReference遇伞,JVM 就越傾向于回收它的 referent 對(duì)象辙喂。這也是 LRUMaxHeapPolicy 策略中 LRU 的語義體現(xiàn)。

應(yīng)用線程在每次調(diào)用 SoftReference 的 get 方法時(shí)候鸠珠,都會(huì)將最近一次的 GC 時(shí)間戳 clock 更新到 timestamp 中巍耗,這樣一來,如果一個(gè) SoftReference 被頻繁的訪問渐排,那么 clock 和 timestamp 的值一直是相等的炬太。

image.png

如果一個(gè) SoftReference 已經(jīng)很久沒有被訪問了,timestamp 就會(huì)遠(yuǎn)遠(yuǎn)落后于 clock驯耻,因?yàn)樵跊]有被訪問的這段時(shí)間內(nèi)可能已經(jīng)發(fā)生好幾次 GC 了亲族。

image.png

在我們了解了這些背景之后,再來看一下 JVM 對(duì)于 SoftReference 的回收過程可缚,在本文 5.1 小節(jié)中介紹的 ZGC Concurrent Mark 階段中霎迫,當(dāng) GC 遍歷到一個(gè) Reference 類型的對(duì)象的時(shí)候,會(huì)在 should_discover 方法中判斷一下這個(gè) Reference 對(duì)象所引用的 referent 是否被標(biāo)記過帘靡。如果 referent 沒有被標(biāo)記為 alive , 那么接下來就會(huì)將這個(gè) Reference 對(duì)象放入 _discovered_list 中知给,等待后續(xù)被 ReferenHandler 處理,referent 也會(huì)在本輪 GC 中被回收掉描姚。

bool ZReferenceProcessor::should_discover(oop reference, ReferenceType type) const {

  // 此時(shí) Reference 的狀態(tài)就是 inactive涩赢,那么這里將不會(huì)重復(fù)將 Reference 添加到 _discovered_list 重復(fù)處理
  if (is_inactive(reference, referent, type)) {
    return false;
  }
  // referent 還被強(qiáng)引用關(guān)聯(lián),那么 return false 也就是說不能被加入到 discover list 中
  if (is_strongly_live(referent)) {
    return false;
  }
  // referent 現(xiàn)在只被軟引用關(guān)聯(lián)轩勘,那么就需要通過 LRUMaxHeapPolicy
  // 來判斷這個(gè) SoftReference 所引用的 referent 是否應(yīng)該存活
  if (is_softly_live(reference, type)) {
    return false;
  }

  return true;
}

如果當(dāng)前遍歷到的 Reference 對(duì)象是 SoftReference 類型的谒主,那么就需要在 is_softly_live 方法中根據(jù)前面介紹的 LRUMaxHeapPolicy 來判斷這個(gè) SoftReference 引用的 referent 對(duì)象是否滿足存活的條件。

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 的時(shí)間戳
  const jlong clock = java_lang_ref_SoftReference::clock();
  // 判斷是否應(yīng)該清除這個(gè) 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 最大存活時(shí)間 —— _max_interval观游,那么這個(gè) SoftReference 所引用的 referent 對(duì)象在本輪 GC 中就不會(huì)被回收搂捧,SoftReference 對(duì)象也不會(huì)被放到 _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) {
  // 相當(dāng)于 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 的最大存活時(shí)間)
  if(interval <= _max_interval) {
    // SoftReference 所引用的 referent 對(duì)象在本輪 GC 中就不會(huì)被回收
    return false;
  }
  // interval 大于 _max_interval懂缕,這個(gè) SoftReference 所引用的 referent 對(duì)象就會(huì)被回收
  // SoftReference 也會(huì)被放到 _reference_pending_list 中等待 ReferenceHandler 線程去處理
  return true;
}

如果 interval 大于 _max_interval允跑,那么這個(gè) SoftReference 所引用的 referent 對(duì)象在本輪 GC 中就會(huì)被回收,SoftReference 對(duì)象也會(huì)被 JVM 放到 _reference_pending_list 中等待 ReferenceHandler 線程處理搪柑。

從以上過程中我們可以看出聋丝,SoftReference 被 ZGC 回收的精確時(shí)機(jī)是,當(dāng)一個(gè) SoftReference 對(duì)象已經(jīng)很久很久沒有被應(yīng)用線程訪問到了工碾,那么發(fā)生 GC 的時(shí)候這個(gè) SoftReference 就會(huì)被回收掉弱睦。

具體多久呢 ? 就是 _max_interval 指定的 SoftReference 最大存活時(shí)間,這個(gè)時(shí)間由當(dāng)前 JVM 堆的最大剩余空間和 -XX:SoftRefLRUPolicyMSPerMB 共同決定渊额。

比如况木,發(fā)生 GC 的時(shí)候,當(dāng)前堆的最大剩余空間為 1MB旬迹,SoftRefLRUPolicyMSPerMB 指定的是 1000 ms 火惊,那么當(dāng)一個(gè) SoftReference 對(duì)象超過 1000 ms 沒有被應(yīng)用線程訪問的時(shí)候,就會(huì)被 ZGC 回收掉奔垦。

《以 ZGC 為例屹耐,談一談 JVM 是如何實(shí)現(xiàn) Reference 語義的(下)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市椿猎,隨后出現(xiàn)的幾起案子惶岭,更是在濱河造成了極大的恐慌,老刑警劉巖鸵贬,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異脖捻,居然都是意外死亡阔逼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門地沮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗜浮,“玉大人,你說我怎么就攤上這事摩疑∥H冢” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵雷袋,是天一觀的道長吉殃。 經(jīng)常有香客問我,道長后众,這世上最難降的妖魔是什么集乔? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任鉴嗤,我火速辦了婚禮翠语,結(jié)果婚禮上悄窃,老公的妹妹穿的比我還像新娘狗热。我一直安慰自己饵沧,他們只是感情好孩灯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布巧娱。 她就那樣靜靜地躺著碉怔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪禁添。 梳的紋絲不亂的頭發(fā)上撮胧,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音上荡,去河邊找鬼趴樱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛酪捡,可吹牛的內(nèi)容都是我干的叁征。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼逛薇,長吁一口氣:“原來是場噩夢啊……” “哼捺疼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起永罚,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤啤呼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后呢袱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體官扣,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年羞福,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惕蹄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡治专,死狀恐怖卖陵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情张峰,我是刑警寧澤泪蔫,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站喘批,受9級(jí)特大地震影響撩荣,放射性物質(zhì)發(fā)生泄漏铣揉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一婿滓、第九天 我趴在偏房一處隱蔽的房頂上張望老速。 院中可真熱鬧,春花似錦凸主、人聲如沸橘券。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旁舰。三九已至,卻和暖如春嗡官,著一層夾襖步出監(jiān)牢的瞬間箭窜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工衍腥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留磺樱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓婆咸,卻偏偏與公主長得像竹捉,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子尚骄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容