天天看點

jvm中類和對象定義存儲基礎知識

作者:京東雲開發者

1.1 類檔案資料結構類型

Class檔案結構主要有兩種資料結構:無符号數和表

•無符号數:用來表述數字,索引引用、數量值以及字元串等,比如 圖1中類型為u1,u2,u4,u8分别代表1個位元組,2個位元組,4個位元組,8個位元組的無符号數

•表:表是有由多個無符号數以及其它的表組成的複合結構,比如圖1中類型以_info結尾的項為表類型。

1.2 類結構定義

Class類檔案是緊湊、順序、無空隙的,魔數(MagicNumber)、Class檔案版本(Version)、常量池(Constant_Pool)、通路标記(Access_flag)、本類(This_class)、父類(Super_class)、接口(Interfaces)、字段集合(Fields)、方法集合(Methods )、屬性集合(Attributes)。其中因為java多繼承是以interfaces接口類型為數組;attribute_info則是方法表中定義的code索引,指向具體的方法體位元組碼。如圖1所示。

jvm中類和對象定義存儲基礎知識

下面用一段程式做說明,此類有接口,有方法、類變量和執行個體變量,機器是如何識别位元組碼然後按照上面的規則來定義此class類呢?

package com.jd.crm.Logback;

public class TestClass implements Super{

    private static final int staticVar = 0;

    private int instanceVar=0;

    public int instanceMethod(int param) throws  Exception{
        return param ++;
    }
}
interface Super{ }

           

通過javap幫助解析class檔案格式如下:

Classfile /D:/spm-workspace/test/target/classes/com/jd/crm/Logback/TestClass.class
  Last modified 2023-4-14; size 597 bytes
  MD5 checksum 9d5dd9fc2145ac17393fee7a707d3b9c
  Compiled from "TestClass.java"
public class com.jd.crm.Logback.TestClass implements com.jd.crm.Logback.Super
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#26         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#27         // com/jd/crm/Logback/TestClass.instanceVar:I
   #3 = Class              #28            // com/jd/crm/Logback/TestClass
   #4 = Class              #29            // java/lang/Object
   #5 = Class              #30            // com/jd/crm/Logback/Super
   #6 = Utf8               staticVar
   #7 = Utf8               I
   #8 = Utf8               ConstantValue
   #9 = Integer            0
  #10 = Utf8               instanceVar
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/jd/crm/Logback/TestClass;
  #18 = Utf8               instanceMethod
  #19 = Utf8               (I)I
  #20 = Utf8               param
  #21 = Utf8               Exceptions
  #22 = Class              #31            // java/lang/Exception
  #23 = Utf8               MethodParameters
  #24 = Utf8               SourceFile
  #25 = Utf8               TestClass.java
  #26 = NameAndType        #11:#12        // "<init>":()V
  #27 = NameAndType        #10:#7         // instanceVar:I
  #28 = Utf8               com/jd/crm/Logback/TestClass
  #29 = Utf8               java/lang/Object
  #30 = Utf8               com/jd/crm/Logback/Super
  #31 = Utf8               java/lang/Exception
{
  public com.jd.crm.Logback.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #2                  // Field instanceVar:I
         9: return
      LineNumberTable:
        line 3: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/jd/crm/Logback/TestClass;

  public int instanceMethod(int) throws java.lang.Exception;
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iload_1
         1: iinc          1, 1
         4: ireturn
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jd/crm/Logback/TestClass;
            0       5     1 param   I
    Exceptions:
      throws java.lang.Exception
    MethodParameters:
      Name                           Flags
      param
}
SourceFile: "TestClass.java"


           

以上是javap幫助我們生成的class檔案解析結果,隻是給人看,而非機器。

通過編譯後生成class檔案格式如下,因為class檔案是以8位作為一個位元組的二進制流。為了友善計算,用16進制表示二進制(1個位元組=2個十六進制的數,故下面每2個數就代表1個位元組)

jvm中類和對象定義存儲基礎知識

1.2.1 魔法數

前四個位元組cafebabe是固定值,任何語言編譯成jvm認識的二進制流,前四位必須是固定的cafebabe位元組。

1.2.2 版本号

緊接着2個位元組00表示次版本号為0 ;0034代表主版本為52(jdk版本号對應的jdk版本為1.8)參考jdk版本和class位元組版本的對應關系

1.2.3 常量個數

常量個數const_pool_count位元組碼為00 20對應的說明常量個數為32,實際為31個,因為首位jvm作為保留位使用。

1.2.4 常量池

常量池存放兩大常量:字面量和符号引,字面量如文本字元串,被生命的final常量值等,而符号引用則包含類、接口的全限名稱、字段、方法名稱和描述符号等等。參考javap生成的類檔案資訊。

這裡隻分析下其中一個常量,在上面常量個數2個位元組後面緊接着一個位元組0a十進制為10,參考常量池類型10代表類中方法的符号引用。繼續參考方法類型MethodRef_info個格式定義:前兩個位元組0004代表方法所在類名稱的索引,後兩個位元組0001a代表一個NameAndType類型的索引。

jvm中類和對象定義存儲基礎知識

1.2.5 類通路标志

緊接常量池定義完後的u2辨別通路标志,本例辨別為0x0021和下圖示志位按位或計算,如0x0001為真,0x0020也為真,其他為否 最終确認通路标志位ACC_PUBLIC、ACC_SUPER

jvm中類和對象定義存儲基礎知識

1.2.6 本類、父類、接口索引集合

根據圖1的規則,u2兩個位元組0003辨別目前類名的引用到,引用常量池數組下标為#3,根據圖3所示子項的類名為com/jd/crm/Logback/TestClass;0004代表父類類名的引用常量池數組下标為#4,根據圖4所示引用的父類類名為java/lang/Object;緊接着0001辨別接口個數,指明數量為1,0005辨別第一個接口數組中接口的名稱,指向常量池中下标為5的名稱為com/jd/crm/Logback/Super;

比如查找目前類索引如下圖

jvm中類和對象定義存儲基礎知識

1.2.7 字段表集合

字段表以數組的形式定義存儲在常量表中

jvm中類和對象定義存儲基礎知識

以上圖說明,0002辨別域個數為2個域辨別,在本類中有兩個,一個類的域字段staticVar 一個是執行個體對象的域字段instanceVar,如字段結構定義(下圖)定義,前2個位元組001a為通路辨別,和類通路辨別一樣,分别用001a的二進制和下圖字段域通路辨別類型做位或運算,得出通路類型為ACC_PRIVATE類型。name_index的占用兩個位元組0006,指向常量表下标為6的引用,descriptor_index=0007指向常量表下标為7的引用,此處為I辨別為資料類型為int,attributes_count=0001為1個,值為0008指向常量表下标為#8的引用常量ConstantValue,辨別為靜态變量,最終依次類推第二個域辨別引用

jvm中類和對象定義存儲基礎知識

字段結構定義

字段域的通路标志請參考類通路标志,邏輯計算一緻,隻是規則不一樣而已 如下圖

jvm中類和對象定義存儲基礎知識

1.2.8 方法表集合

和域字段集合表定義類似 也是數組方式定義在常量池中 ,其中方法的結構體第四個字段attributes_count代表方法的屬性數量,attribute_info就是屬性的集合參考屬性表集合

jvm中類和對象定義存儲基礎知識
jvm中類和對象定義存儲基礎知識

方法表通路辨別類型

通過上面方法的通路标志、名稱索引和描述索引定義方法的基本資訊,方法的代碼塊則存放于類型為Code的屬性表中。

1.2.9 屬性表集合

類、字段表、方法表本身可包含屬性表,屬性表格結構體如下,屬性表結構類型較多,比如有Code類型、Exception類型、MethodParameters類型等等,具體參考屬性表類型。所有的屬性都是引用常量池中的屬性類型名稱。然後根據屬性的長度指定該屬性的内容,根據屬性的不同類型解析不同的屬性值。格式定義如下

jvm中類和對象定義存儲基礎知識

以Code屬性舉例,Code屬性結構如下所示

jvm中類和對象定義存儲基礎知識

jvm按屬性擷取attribute_name_index指向常量池一個字元串常量Code,緊接着attribute_length辨別Code類型Info資訊長度,這個info内容包括:max_stack 最大棧深,max_locals局部變量槽數量,code_length辨別機器位元組碼長度,往後查詢位元組碼如下圖所示,其實就是0/1/4/5/6/9的指令集。Code類型又嵌套異常屬性表、行号表LineNumberTable、LocaVariableTable 局部變量表等等資訊。如下圖javap生成的類定義資訊

jvm中類和對象定義存儲基礎知識

1.Code1方法執行過程:

構造方法:descriptor ()V辨別無參無傳回值為Void的方法索引,flags可見性修飾符;

程式運作時,先将常量池、方法位元組碼、字元串常量池,靜态變量加載到中繼資料區(1.8後字元串常量池,靜态變量放入了堆);main線程開始運作,配置設定棧幀記憶體,其中操作數棧stack=2表示運作該方法所需要的最大操作數棧的深度是2;locals=1表示該運作方法所需要的最大局部方法表的最大slot資料是1;args_size是該方法的形參個數,如果是執行個體方法 第一個形參是this引用。此例正是this引用。是以args_size=1+實際的參數

aload_0: 加載 slot0的局部變量,即this,作為下面的invokespecial 構造方法調用的參數

invokespecial: 調用構造方法,常量池第#1項,即【Method java/lang/Object."<init>":()V】

aload_0 :再次加載 slot0的局部變量,即this

iconst0: 将int類型為0的數值壓入棧頂(為什麼要再放入棧頂,我個人人為可能是下面初始化執行個體會需要指定到目前的執行個體對象)

putfileld: 将常量池中#2 也就是com/jd/crm/Logback/TestClass.instanceVar 執行個體變量指派為0,并彈出棧。

通過以上指令操作,對象已經初始化,可發現在執行個體變量初始化之前是先調用的構造器方法,後才初始化執行個體變量。

1.Code2方法instanceMethod執行過程:

descriptor辨別為int類型入參、int類型出參

flags辨別方法問public類型

statck=2代表棧深度為2,locals=2辨別預留兩個局部變量槽;args_size=2辨別兩個參數,分别為隐藏的this和方法的形式參數,下标[0]=this、 [1]=param 如下所示

LocalVariableTable:

Start Length Slot Name Signature

0 4 0 this Lcom/jd/crm/Logback/TestClass;

0 4 1 param I

0:iload_1 辨別将上面局部變量槽LocalVariableTable下标為1的param參數壓入棧

1:iconst_1 将int類型為1的常量數字壓入棧

2: iadd 将目前棧頂的兩個元素 param和1相加

3: ireturn 傳回

LineNumberTable:

line 10: 0

辨別實際java源代碼的行數

1.2.10位元組碼指令簡介

•加載和存儲指令:

•運算指令

•類型轉換指令

•對象建立和通路指令

•操作數棧管理指令

•控制轉移指令

•異常處理指令

•同步指令

•方法調用和傳回執行

invokervirtual:調用對象的執行個體方法 invokerinterface 調用接口方法,自動運作期搜尋一個實作接口的對象進行方法調用;invokerspeical:調用init、私有和父類調用的特殊方法調用;invokedynamic:運作時動态解析

1.3 類檔案加載

jvm中類和對象定義存儲基礎知識

1.3.1 加載

jvm通過classLoader(雙親委派)将class類檔案二進制流加載到中繼資料區記憶體,

将位元組流所辨別的靜态存儲結構轉換為中繼資料區的動态存儲

在堆記憶體建立一個Class對象,堆中的Class并不存儲靜态變量、常量、方法等實際資訊(實際存儲元空間),可以看做隻是一個句柄,通過對象頭的類指針指向元空間類資訊。這樣在強制轉換或者InstanceOf判斷時,會根據對象中的類指針指向元空間的類常量池進行判斷是否為同一個類。

1.3.2 驗證

1、檔案格式驗證

2、中繼資料驗證

3、位元組碼驗證

4、符号引用驗證

1.3.3 準備

準備階段是為類變量(靜态變量)配置設定記憶體并設定類變量初始值的階段,配置設定這些記憶體是在中繼資料區裡面進行的,但是類變量(無final修飾的靜态變量)、字元串常量在1.8及以後都放入了堆區間。這個階段有兩點需要重點介紹以下的:

1、隻有類變量(被static修飾的變量指派初始值,static final修飾的指派為程式指定值)會配置設定記憶體,不包括執行個體變量,執行個體變量是在對象執行個體化的時候在堆中配置設定記憶體的。

2、設定類變量的初始值是數量類型對應的預設值,而不是代碼中設定的預設值。例如public static int number=111,這類變量number在準備階段之後的初始值是0而不是111。而給number指派為111是在類的初始化階段。

1.3.4 解析

解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符号引用進行。

符号引用:常量池中類、字段的常量字元串表示方式

類和接口的解析舉例:假如類A引用了類B,加載階段是靜态解析,這時候B還沒有被放到JVM記憶體中,這時候A引用的隻是代表B的符号,這是符号引用。

直接引用: 指向目标的指針或者相對偏移量

類和接口的解析舉例:類A在解析階段發現自己符号引用了B,如果這個時候B還沒被加載。就是直接觸發B的類加載,加載後會在運作常量池存儲B的有效類資訊位址,并且直接引用。

•類和接口的解析

•字段解析根據常量池字段filedrf_info中的符号進行解析,首先在符号引用的類中根據簡單名稱和字段描述符查找,如果查到則傳回這個字段的直接引用并結束,否則從下往上地櫃各個父類查找,如果還未查到則抛出NoSuckFieldError異常

•方法解析

•接口方法解析

1.4 類執行個體初始化

初始化,為類的靜态變量賦予正确的初始值,JVM負責對類進行初始化,主要對類變量進行初始化clinit方法。在Java中對類變量進行初始值設定有兩種方式:定義靜态變量并指定值、使用靜态代碼塊

對象初始化

jvm中類和對象定義存儲基礎知識

1.4.1 初始化對象前檢查

jvm碰到一個new指令,首先判斷改指令指向的常量池的類全名是否被加載、解析初始化過,如果沒有則進行類加載,參考類檔案加載

1.4.2 記憶體配置設定

通過jvm記憶體配置設定機制,此配置設定機制取決回收機制,通過指針碰撞方法或者空閑清單方式進行堆記憶體配置設定;

1.指針碰撞法 假設Java堆中記憶體是完整的,已配置設定的記憶體和空閑記憶體分别在不同的一側,通過一個指針作為分界點,需要配置設定記憶體時,僅僅需要把指針往空閑的一端移動與對象大小相等的距離。使用的GC收集器:Serial、ParNew,适用堆記憶體規整(即沒有記憶體碎片)的情況下。這兩種都是新生代垃圾收集器,是以都是使用複制算法,可以得到比較完整的記憶體區域。

2.空閑清單法 事實上,Java堆的記憶體并不是完整的,已配置設定的記憶體和空閑記憶體互相交錯,JVM通過維護一個清單,記錄可用的記憶體塊資訊,當配置設定操作發生時,從清單中找到一個足夠大的記憶體塊配置設定給對象執行個體,并更新清單上的記錄。使用的GC收集器:CMS,适用堆記憶體不規整的情況下。從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種“标記-清除”算法實作的,是以會得到很多碎片是以和空閑清單配合使用。

記憶體配置設定并發問題

在建立對象的時候有一個很重要的問題,就是線程安全,因為在實際開發過程中,建立對象是很頻繁的事情,作為虛拟機來說,必須要保證線程是安全的,通常來講,虛拟機采用兩種方式來保證線程安全:

•CAS: CAS 是樂觀鎖的一種實作方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。虛拟機采用 CAS 配上失敗重試的方式保證更新操作的原子性。

•TLAB(本地現成緩沖區): 為每一個線程預先配置設定一塊堆記憶體,JVM在給線程中的對象配置設定記憶體時,首先在TLAB配置設定,當對象大于TLAB中的剩餘記憶體或TLAB的記憶體已用盡時,再采用上述的CAS進行記憶體配置設定。

1.4.3 初始化0值

記憶體配置設定完成後,虛拟機需要将配置設定到的記憶體空間都初始化為零值(不包括對象頭),這一步操作保證了對象的執行個體字段在 Java 代碼中可以不賦初始值就直接使用,程式能通路到這些字段的資料類型所對應的零值。

1.4.4 對象頭設定

初始化零值完成之後,虛拟機要對對象進行必要的設定,例如這個對象是哪個類的執行個體、如何才能找到類的中繼資料資訊、對象的哈希碼、對象的 GC 分代年齡等資訊。這些資訊存放在對象頭中。另外,根據虛拟機目前運作狀态的不同,如是否啟用偏向鎖等,對象頭會有不同的設定方式。

1.4.5 執行個體構造器初始化

1.4.6 對象的記憶體布局

對象在對中的存儲布局主要分為三部分,對象頭、執行個體資料、對齊填充

jvm中類和對象定義存儲基礎知識

對象頭:

主要兩類:其主要包括兩部分資料:Mark Word、Class對象指針。特别地對于數組對象而言,其還包括了數組長度資料。在64位的HotSpot虛拟機下,Mark Word占8個位元組,其記錄了Hash Code、GC資訊、鎖資訊等相關資訊;而Class對象指針則指向該執行個體的Class對象。

jvm中類和對象定義存儲基礎知識

HotSpot對象頭

執行個體資料:對象定義的執行個體變量,這部分資料存儲受到虛拟機配置設定政策參數(-XX:FieldsAllocationStype)和字段定義的順序影響。HotSpot預設配置設定的政策是将相同寬度字段一起存放,父類的變量會出現在子類變量之前。

對齊填充:jvm存儲任何大小必須是8個位元組的整數倍,不夠補齊。這個和類二級制位元組流一緻。下面是個無鎖狀态的對象執行個體化後的資料結構,使用jol工具列印出的執行個體布局如下

jvm中類和對象定義存儲基礎知識

1.5 對象的通路

jvm中類和對象定義存儲基礎知識

1.5.1 句柄通路

Java堆中将會劃分出一塊記憶體來作為句柄池,reference中 存儲的就是對象

的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址信 息

1.5.2 直接通路

直接通路是reference中直接存儲的執行個體對象的位址,執行個體對象中包含了類對象的通路指針,也就是如果通路類對象需要多一層引用

優缺點

這兩種對象通路方式各有優勢,使用句柄來通路的最大好處就是reference中存儲的是穩定的句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。 使用直接指針通路方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷, 由于對象的通路在Java中非常頻繁,是以這類開銷積少成多後也是一項非常可觀的執行成本。就本書讨論的主要虛拟機Sun HotSpot而言,它是使用第二種方式進行對象通路的,但從整個軟體開發的範圍來看,各種語言和架構使用句柄來通路的情況也十分常見

1.6 虛拟機位元組碼執行引擎

1.6.1 運作時棧幀結構

1.局部變量表:在class檔案被編譯時,就已知某個方法的局部變量槽有幾個,主要存放方法參數和方法内部定義的局部變量

2.操作數棧:和局部變量表相似,編譯時就明确了操作數棧的深度

3.動态連結:大部分類在類加載解析過程中,會将符号引用轉為直接引用,也就是在類加載階段清楚調用哪個類的哪個方法(這些方法調用參考位元組碼指令簡介中invoke*指令),但是有一部分必須在運作期間才能确定目标的方法的直接引用。

4.方法傳回位址

1.6.2 方法調用

1.解析:在内解析階段,會将符号引用轉換為直接引用,這種在解析階段就能确定的調用方法版本稱為解析,比如invokesatic invokespecial invokevirtual等等指令訓示的方法調用

2.靜态分派:方法的重載,虛拟機需要根據方法的入參個數和類型方能定位到某個具體方法,發生在編譯階段,故也屬于一種解析方式

3.重載方法比對優先級:方法重載過程中,涉及方法的入參和個數,而入參存在自動類型轉換,比如重載方法入參為char類型,如果不存在入參為char類型的方法比對,則char進行自動類型轉換為int類型,在最終比對了Int入參類型的方法。方法重載的本質

4.動态配置設定:如下圖所示,man和women和重新man引用指向women然後方法調用sayHello,此時位元組碼顯示的符号引用都是Human#sayHello,但是實際執行結果和指令碼不一緻,這是因為invokevirtual指令,在指令調用之前都會aload_x來加載實際的資料類型,這就是方法重寫的本質

jvm中類和對象定義存儲基礎知識

5.invokedynamic指令:為了解決其他invok*指令方法配置設定規則完全固化在虛拟機中的問題,jvm支援設計者更高的靈活度,将動态調用可以以api的方式直接使用。參考java.lang.invoke包的使用方式。

1.6.3 基于棧的位元組碼解釋執行引擎

jvm是基于棧的指令集合,這種指令自身不帶參數,使用操作數棧的輸入輸出作為指令本身的參數。實體機一般是基于寄存器的指令集,指令本身攜帶參數并存放在寄存器。

下面是一個基于棧來展示在虛拟機中位元組碼是如何執行的。

jvm中類和對象定義存儲基礎知識

以上位元組碼執行過程如下

jvm中類和對象定義存儲基礎知識

1.7 容易混淆點

1.7.1 檔案常量池

類加載後,類的域字段、方法和類描述資訊會加載到中繼資料區,既屬于類的靜态常量池

1.7.2 運作時常量池

我們上面說的class檔案中的常量池,它會在類加載後進入方法區中的運作時常量池。并非隻有Class定義的檔案常量合并處理後放入運作時常量池,在運作期間也可以将新的常量放入池中,比如String類的intern方法

jvm中類和對象定義存儲基礎知識

1.7.3 字元串常量池

字元串常量池存放在堆記憶體(>=1.8)中,堆裡邊的字元串常量池存放的是字元串的引用或者字元串(兩者都有),如下圖描述字元串建立的堆分布

jvm中類和對象定義存儲基礎知識

上圖說明:

引用初始化初始化s、s2是先看常量池,有就傳回對象引用,否則建立abc對象,然後建立s1/s2Ref常量引用傳回

字元串相加:先建立StringBuilder對象,然後apend字元串a、apend字元串b 然後toString(new方法)生成字元串ab對象并在字元串常量池生成引用傳回,為什麼不要字元串相加,就是因為會生成大量StringBuilder對象

String s = "a"+"b";//傳回的是常量池的ab字元串的引用
String s1 ="ab";
System.out.println(s == s1);//因兩個最終都指向字元串常量池,是以為true

           

new 字元串相當于堆建立兩個對象,一個String對象,然後建立字元串堆存儲,然後String對象引用到字元串的堆存儲,

String s1 ="a";
String s = new String ("a").intern();//強制生成字元串常量池引用
System.out.println(s == s1);//傳回true

           
String s1 ="a";
String s = new String ("a");
System.out.println(s == s1);//傳回false

           

1.8 附件

jvm常量池類型和結構體定義

jvm中類和對象定義存儲基礎知識

常量池類型

jvm中類和對象定義存儲基礎知識

常量池類型結構定義

jvm中類和對象定義存儲基礎知識

常見的屬性類型

jvm中類和對象定義存儲基礎知識

jdk版本好class位元組版本号對應關系

jvm中類和對象定義存儲基礎知識

屬性表類型

作者:京東物流 王北永

來源:京東雲開發者社群

繼續閱讀