OpenJDK系列(二):從ClassFileParser談Endian

Endian

Endian即所謂的字節(jié)序,通俗點(diǎn)說就是多于一個(gè)類型的數(shù)據(jù)在內(nèi)存中存取的順序目前有兩種字節(jié)序.

  • Big-Endian: 也稱為大端序:高位字節(jié)存放在內(nèi)存的低地址端,低位字節(jié)存放在內(nèi)存的高地址端.
  • Little-Endian: 也稱為小端序:高位字節(jié)存放在內(nèi)存的高地址端,低位字節(jié)存放在內(nèi)存的低地址端.

Endian與內(nèi)存單元

對(duì)于0x12345678而言,1234是高四位,5678是低四位.再以十進(jìn)制的98來說9是高位,8是低位.現(xiàn)在回顧下內(nèi)存的抽象模型:由不同的存儲(chǔ)單元的構(gòu)成,每個(gè)存儲(chǔ)單元容量為1個(gè)字節(jié).

image-20180905111953115

也就是說一個(gè)內(nèi)存單元可以存放C語言中一個(gè)char類型數(shù)據(jù),如果是short類型,則需要占用2個(gè)內(nèi)存單元,而int類型則需要占據(jù)4個(gè)內(nèi)存單元,比如int類型的305419896,其十六進(jìn)制為0x12345678,需要占據(jù)4個(gè)內(nèi)存單元,那這個(gè)四個(gè)內(nèi)存單元中到底該如何存放數(shù)據(jù)呢?此時(shí)就用到了剛才的Endian.

如果按照Big-Endian方式,其內(nèi)存布局如下:

image-20180905105723144

如果按照Little-Endian方法,其內(nèi)存布局如下:

image-20180905105758961

可以看出,對(duì)于超過一個(gè)字節(jié)類型的數(shù)據(jù)按照不同Endian會(huì)在內(nèi)存中呈現(xiàn)不同的存放順序,那為什么會(huì)出現(xiàn)大小端呢?

Endian起因

Endian產(chǎn)生根本原因在于CPU要想讀寫內(nèi)存中的數(shù)據(jù)必須借助于寄存器.內(nèi)存單元的容量一直保持1Byte不變,但寄存器卻隨著發(fā)展其容量不斷增加,比如現(xiàn)代計(jì)算機(jī)的寄存器的容量都是超過1Byte的.這種寄存器容量和內(nèi)存單元容量的差異最終導(dǎo)致字節(jié)序問題.寄存器如何保存超過一個(gè)字節(jié)數(shù)據(jù)必然涉及到某種順序,這種順序就體現(xiàn)在寄存器高低位的定義,而這種定義又會(huì)影響到數(shù)據(jù)在寄存器中的存放,最終在內(nèi)存的存儲(chǔ)順序上體現(xiàn)出來.

Endian與Class解析

Endian和字節(jié)流解析有什么聯(lián)系呢?在單機(jī)上采用同一種模式進(jìn)行存取操作時(shí),CPU會(huì)自動(dòng)處理這種變化,保證數(shù)據(jù)寫入和讀取之后的結(jié)果一致.但涉及到網(wǎng)絡(luò)傳輸或者跨平臺(tái)后,就無法保證雙方使用的是同一種模式,如果不一致則會(huì)導(dǎo)致數(shù)據(jù)問題,因此需要進(jìn)行大小端的轉(zhuǎn)換.

對(duì)于Java這種跨平臺(tái)語言而言,同樣需要關(guān)注這種差異.Java輸出的字節(jié)信息都是大端模式,但JVM是卻由C/C++編寫的.在默認(rèn)情況下C/C++的大小端模式與當(dāng)前計(jì)算機(jī)硬件平臺(tái)的大小端模式保持一致,如果JVM對(duì)此不做特殊處理,最終讀取的字節(jié)碼文件會(huì)有問題.在實(shí)際開發(fā)中,我們并不會(huì)關(guān)注該問題,這是因?yàn)镴VM在讀取字節(jié)碼文件時(shí)做了特殊處理:如果檢測(cè)到當(dāng)前平臺(tái)采用的是小端模式,會(huì)將其轉(zhuǎn)為大端模式,以保證字節(jié)碼文件的在JVM中的一致性.

整個(gè)流程可以簡單描述為:當(dāng)一個(gè)類需要被加載時(shí),最終會(huì)交給classload.cpp的load_class(),接下來由ClassFileParser.cpp的parse_stream()負(fù)責(zé)解析.class文件對(duì)應(yīng)ClassFileStream,在解析的過程中會(huì)根據(jù)平臺(tái)的Endian來決定是否要進(jìn)行轉(zhuǎn)換.

ClassFileStream

ClassFileStream是用于讀取.class文件的輸入流,其路徑為:/OpenJDK10/OpenJDK10/hotspot/src/share/vm/classfile/classFileParser.hpp

class ClassFileStream: public ResourceObj {
 private:
  const u1* const _buffer_start; // Buffer bottom
  const u1* const _buffer_end;   // Buffer top (one past last element)
  mutable const u1* _current;    // Current buffer position
  const char* const _source;     // Source of stream (directory name, ZIP/JAR archive name)
  bool _need_verify;             // True if verification is on for the class file
    
  .......  
}

_current指針指向Java字節(jié)流中當(dāng)前已經(jīng)讀取到的位置.當(dāng)class文件剛被加載時(shí),_current指向當(dāng)前字節(jié)流的第一個(gè)字節(jié)所在的位置,后續(xù)隨著解析操作的不斷進(jìn)行,_current指針不斷的往后移動(dòng),直至當(dāng)前字節(jié)流最后.

根據(jù)字節(jié)碼規(guī)范,該類中定義了用于讀取固定字節(jié)長度的方法:

class ClassFileStream: public ResourceObj {
    ......
        
    public: 
     ClassFileStream(const u1* buffer,
                  int length,
                  const char* source,
                  bool verify_stream = verify);
    
     u2 get_u2_fast() const {
        u2 res = Bytes::get_Java_u2((address)_current);
        _current += 2;
        return res;
    }   
    
    u4 get_u4_fast() const {
     u4 res = Bytes::get_Java_u4((address)_current);
     _current += 4;
     return res;
    }
    
   u8 get_u8_fast() const {
    u8 res = Bytes::get_Java_u8((address)_current);
    _current += 8;
    return res;
   }
   ......
}

除此之外也定義用于跳過固定字節(jié)碼長度的常用方法,比如:skip_u4_fast(int length)等.在后續(xù)的字節(jié)碼解析過程中,這幾個(gè)方法非常常見.

ClassFileParser

ClassFileParser負(fù)責(zé)class文件解析,并嘗試創(chuàng)建oops.創(chuàng)建ClassFileParser對(duì)象后會(huì)繼續(xù)調(diào)用其parse_stream()`對(duì)當(dāng)前類文件的字節(jié)碼流進(jìn)行解析.由于class文件解析相對(duì)復(fù)雜,因此這里只介紹magic number是如何被解析出來的.

void ClassFileParser::parse_stream(const ClassFileStream* const stream,
                                   TRAPS) {

  assert(stream != NULL, "invariant");
  assert(_class_name != NULL, "invariant");

  // BEGIN STREAM PARSING
  stream->guarantee_more(8, CHECK);  // magic, major, minor
  // Magic value
  const u4 magic = stream->get_u4_fast();
  guarantee_property(magic == JAVA_CLASSFILE_MAGIC,
                     "Incompatible magic value %u in class file %s",
                     magic, CHECK);

  // Version numbers
  _minor_version = stream->get_u2_fast();
  _major_version = stream->get_u2_fast();
    
  ......
      
}

按照字節(jié)碼規(guī)范,字節(jié)碼前三部分依次是magic number,minor_version及major_version,分別占用u4,u2,u2,即4個(gè)字節(jié),2個(gè)字節(jié),2個(gè)字節(jié),總共是8個(gè)字節(jié),guarantee_more(8, CHECK)中的參數(shù)8含義就是如此:比較當(dāng)前字節(jié)流文件剩余的長度是否大于想要讀取的字節(jié)長度,否則報(bào)錯(cuò).

校驗(yàn)通過后,調(diào)用stream的get_u4_fast()方法從字節(jié)碼流中讀取u4長度的字節(jié)序,即ClassFileStream中get_u4_fast():

  u4 get_u4_fast() const {
    u4 res = Bytes::get_Java_u4((address)_current);
    // 讀取完4個(gè)字節(jié)后,需要后移_current,因此需要對(duì)其進(jìn)行+4  
    _current += 4;
    return res;
  }

在該方法中,從字節(jié)流中讀取4個(gè)字節(jié)的操作由Bytes::get_Java_u4((address)_current)實(shí)現(xiàn).其中Bytes是與CPU架構(gòu)相關(guān)的類.我這邊CPU采用的是x86架構(gòu),因此調(diào)用的是/OpenJDK10/hotspot/src/cpu/x86/vm/bytes_x86.hpp`中Bytes類:

class Bytes: AllStatic {
    ......
    static inline u4 get_Java_u4(address p) {
        // 調(diào)用模板方法get_Java()
        return get_Java<u4>(p); 
    }
    
    ......
     
    template <typename T>
    static inline T get_Java(const address p) {
       // 1.讀取u4,即get_native<u4>(p) 
       T x = get_native<T>(p);
       // 2.如果當(dāng)前平臺(tái)的字節(jié)序和Java不一樣,即不是Big-Endian,需要進(jìn)行轉(zhuǎn)換
       // 也就是將Little_Endian轉(zhuǎn)為Big_Endian 
       if (Endian::is_Java_byte_ordering_different()) {
         //3.大小端轉(zhuǎn)換,即swap<u4>(x)  
         x = swap<T>(x);
       }
       return x;
    }       

}

在模板方法get_Java()先是調(diào)用與平臺(tái)相關(guān)的函數(shù)get_native<u4>()來讀取4個(gè)字節(jié):

class Bytes: AllStatic {
    
  template <typename T>
  static inline T get_native(const void* p) {
    assert(p != NULL, "null pointer");

    T x;
    // is_aligned()用于判斷當(dāng)前值是否對(duì)齊與給定值,未對(duì)齊則使用memcpy從p指針出拷貝u4數(shù)據(jù)到x
    if (is_aligned(p, sizeof(T))) {
      // 此處由于是讀取u4,因此最終將指針p強(qiáng)轉(zhuǎn)為u4*類型的指針.  
      x = *(T*)p;
    } else {
      memcpy(&x, p, sizeof(T));
    }

    return x;
  }   
  
  ......
      
}

讀取完成后判斷當(dāng)前平臺(tái)的模式是否和Java中的一致,即當(dāng)前是否是大端模式,如果不是則繼續(xù)調(diào)用swap<u4>()實(shí)現(xiàn)小端到大端的轉(zhuǎn)換.

class Bytes: AllStatic {
  ......
      
  // Efficient swapping of byte ordering
  template <typename T>
  static T swap(T x) {
    switch (sizeof(T)) {
    case sizeof(u1): return x;
    case sizeof(u2): return swap_u2(x);
    case sizeof(u4): return swap_u4(x);
    case sizeof(u8): return swap_u8(x);
    default:
      guarantee(false, "invalid size: " SIZE_FORMAT "\n", sizeof(T));
      return 0;
    }
  }

  static inline u2   swap_u2(u2 x);                   // compiler-dependent implementation
  static inline u4   swap_u4(u4 x);                   // compiler-dependent implementation
  static inline u8   swap_u8(u8 x);
}

需要注意swap_u4()是夸平臺(tái),為了兼容,可以看到在/OpenJDK10/OpenJDK10/hotspot/src/os_cpu根據(jù)平臺(tái)進(jìn)行了不同的實(shí)現(xiàn),比如我這邊用的是/OpenJDK10/hotspot/src/os_cpu/bsd_x86/vm/bytes_bsd_x86.inline.hpp:

image-20180905154857898

此處內(nèi)嵌了一段匯編代碼來實(shí)現(xiàn)大小端的轉(zhuǎn)換.至此,我們已經(jīng)清楚JVM是如何統(tǒng)一成大端模式的.最新文章見浮游.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末诊沪,一起剝皮案震驚了整個(gè)濱河市嗤疯,隨后出現(xiàn)的幾起案子蝶柿,更是在濱河造成了極大的恐慌,老刑警劉巖赋朦,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異砂客,居然都是意外死亡甩十,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門坪郭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脉幢,你說我怎么就攤上這事歪沃。” “怎么了嫌松?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵沪曙,是天一觀的道長。 經(jīng)常有香客問我萎羔,道長液走,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任贾陷,我火速辦了婚禮缘眶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘髓废。我一直安慰自己巷懈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布慌洪。 她就那樣靜靜地躺著顶燕,像睡著了一般凑保。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涌攻,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天欧引,我揣著相機(jī)與錄音,去河邊找鬼恳谎。 笑死芝此,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惠爽。 我是一名探鬼主播癌蓖,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼婚肆!你這毒婦竟也來了租副?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤较性,失蹤者是張志新(化名)和其女友劉穎用僧,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赞咙,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡责循,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了攀操。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片院仿。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖速和,靈堂內(nèi)的尸體忽然破棺而出歹垫,到底是詐尸還是另有隱情,我是刑警寧澤颠放,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布排惨,位于F島的核電站,受9級(jí)特大地震影響碰凶,放射性物質(zhì)發(fā)生泄漏暮芭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一欲低、第九天 我趴在偏房一處隱蔽的房頂上張望辕宏。 院中可真熱鬧,春花似錦砾莱、人聲如沸匾效。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽面哼。三九已至野宜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間魔策,已是汗流浹背匈子。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闯袒,地道東北人虎敦。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像政敢,于是被迫代替她去往敵國和親其徙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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