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é).
也就是說一個(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)存布局如下:
如果按照Little-Endian方法,其內(nèi)存布局如下:
可以看出,對(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
:
此處內(nèi)嵌了一段匯編代碼來實(shí)現(xiàn)大小端的轉(zhuǎn)換.至此,我們已經(jīng)清楚JVM是如何統(tǒng)一成大端模式的.最新文章見浮游.