類變量解析
1 java中private 字段可以被繼承嗎
有一種說法是凡是父類中被定義為private的字段,都是“老子的”私有財產(chǎn),即便是兒子,也繼承不了
如果沒有繼承的話,為什么父類和子類的大小都是一樣呢?所以子類繼承了父類的私有字段,只是沒法訪問而已。而當我們調(diào)用父類get XXX方法時,就拿到了父類的字段。也就是說雖然兒子繼承了父類的私有財產(chǎn),但是卻沒有直接權(quán)力支配,除非老子給兒子開放了特有的接口訪問,否則兒子不能動老子的私有財產(chǎn)。
我們可以接著進行驗證,首先我們定義一個Myclass類,里面定義4個字段。接著我們定義一個Test類,去繼承它,然后我們?nèi)ヲ炞C這個結(jié)論。
咦,為什么debug的時候a,b,c,d字段居然在test實例中呢?哈哈,這就說明子類確實是可以繼承到父類的私有字段的。
我們可以使用jdk自帶工具HSDB進行驗證。在jdk/lib目錄下,使用如下命令:java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB,去查看大小。具體如何使用HSDB可以參考下面這個鏈接。https://www.cnblogs.com/alinainai/p/11070923.html
我們可以看到nonstatic_fied_size:5至于為什么是5呢?因為我們最后定義一個為long類型的大小為2個slot槽所以size為3+2=5
以下所有代碼引用open-jdk1.8 hotspot源碼
在調(diào)用parse_fields()函數(shù)之前,先定義了一個變量fac,這里的FieldAllocationCount是一個class類型,聲明如下:
通過聲明可知FieldAllocationCount的變量實例將會記錄5種靜態(tài)(static)類
型變量的數(shù)量和5種對應的非靜態(tài)類型的變量的數(shù)量,這5種變量類型分別是:
Oop,引用類型
Byte字節(jié)類型
Short短整型
Word,雙字類型
Double,浮點型
layout?_fields()函數(shù)里面會分別統(tǒng)計static和非static種變量的數(shù)量,后面JVM為
Java類分配內(nèi)存空間時,會根據(jù)這些變量的數(shù)量計算所占內(nèi)存大小。可能有小伙伴會問Java
語言所支持的實際數(shù)據(jù)類型遠不止這5種呀,例如boolean float int等等,為什么JVM
統(tǒng)計這5種呢?這是因為在JVM部,除了引用類型,所有內(nèi)置的基本類型都使用剩余的
4種類型來表示,因此想知道Java類的域變量需要占用多少內(nèi)存,只需要分別統(tǒng)計這幾種類型的數(shù)量即可
這個Array類就是用于元數(shù)據(jù)分配的數(shù)組
上面對ClassFileParser: :parse_ fields()的主要邏輯進行了注釋,通過注釋可以知道JVM??Java類域變量的邏輯是:
獲取Java類中的變量數(shù)量
為字段數(shù)據(jù)分配臨時資源數(shù)組
讀取變量的訪問標識
讀取變量名稱索引
讀取變量類型索引
讀取常量索引
讀取簽名表索引,
對字段表屬性進行解析
讀取變量屬性
判斷變量類型
統(tǒng)計各類型數(shù)量,并分配字段數(shù)據(jù)大小
從臨時資源數(shù)組獲取字段的數(shù)據(jù)
將字段值保存到元數(shù)組
2?偏移量
解析完字節(jié)碼文件中Java類的全部域變量信息后,JVM計算出5種數(shù)據(jù)類型各自的數(shù)量。依舊在 ClassFileParser::parseClassFile()中,具體實現(xiàn)在layout_fields()函數(shù)中
計算全部靜態(tài)變量所占的字節(jié)數(shù)。
這段邏輯很簡單,先拿到起始偏移量,接著分別根據(jù)各個靜態(tài)類型變量的偏移量計算總偏移量。計算順序是:
static_oop
static_double
static_word
static_short
static_byte
每一種“下游”數(shù)據(jù)類型的偏移量都依賴其“上游”數(shù)據(jù)類型所占的字節(jié)寬度以及數(shù)量。
JVM為何要記錄每一個變量的偏移量呢?其實。這與靜態(tài)變量的存儲機制和訪問機制有關(guān)。對于JDK1.8而言,靜態(tài)變量存儲在Java類在JVM所對應的鏡像類instanceMirrorKlass.cpp 中,當Java代碼訪問靜態(tài)變量時,最終JVM也是通過設(shè)置偏移量進行訪問。
3?非靜態(tài)變量偏移量
相比于靜態(tài)變量偏移量,非靜態(tài)變量的偏移量計算稍顯復雜。
獲取完非靜態(tài)字段起始偏移量之后就獲取double和oop這 兩種非靜態(tài)變量的起始偏移量,,分配策略不同,2種變量的起始偏移量也不同。下面分別為處理非壓縮類型,以及處理壓縮類型
緊接著計算剩余三種類型變量的起始偏移量
5大類型變量的偏移量都計算完,現(xiàn)在開始遍歷所有字段,分別計算各類型具體變量的偏移值,這里包括靜態(tài)的和非靜態(tài)的。
如上面代碼所注釋的那樣,非靜態(tài)類型變量的偏移量計算邏輯可以分為5個步驟
(1) 計算非靜態(tài)變量起始偏移量,這一步需要首先獲取父類的非靜態(tài)字段大小。
(2) 計算non_static_double_offset和ono_static_oop_offset這兩種非靜態(tài)變量的起始偏移量。
(3) 計算剩余三種類型變量起始偏移量:non_static_word_offset,non_static_short_offset和non_static_byte_offset
(4) 計算對齊補白空間。
(5) 計算補白后非靜態(tài)變量所需要的內(nèi)存空間總大小。
3.1什么是偏移量?
對于非靜態(tài)變量類型的變量,其偏移量是相對于未來即將new出來的Java對象實例在JVM內(nèi)部所對應的instanceOop對象實例首地址的偏移位置。我們知道常量池對象采用oop-klass這種一分為二的模型來描述。對于Java類,JVM內(nèi)部同樣適用這種一分為二的模型來描述,對應的oop類是instanceOopDesc,對應的klass類是instanceKlass。在oop-klass模型中,oop模型主要存儲對象實例的實際數(shù)據(jù),而klass模型則主要存儲對象的結(jié)構(gòu)信息和虛函數(shù)方法表,說白了就是描述類的結(jié)構(gòu)和行為。
當JVM加載一個Java類,會首先構(gòu)建對應的instanceKlass對象,而當new一個Java對象實例時,則會構(gòu)建出對應的instanceOop對象。InstanceOop對象主要由Java類的成員變量組成,而這些成員變量在instanceOop中的排列順序,便由各種變量類型的偏移量決定。
在Hotspot內(nèi)部,任何oop對象都包含對象頭,因此實際上非靜態(tài)變量的偏移量要從對象頭的末尾開始計算。Java類實例在堆內(nèi)存的起始偏移量如下
知道了偏移量的含義,現(xiàn)在來看看JVM的實現(xiàn),源代碼邏輯如下
起始位置等于base_offset_in_bytes + nonstatic_field_size*heapOopSize;也就是要獲父類的非靜態(tài)字段大小,這里說明父類的字段是可以讓子類繼承的,因為計算子類的時候,需要獲取父類的字段大小。而父類的這個大小定義在instanceKlass.hpp
而base_offset_in_bytes大小定義在instanceOop.hpp中,如果啟用壓縮指針,那么則調(diào)用klass_gap_offset_in_bytes方法,否則為實例instanceOopDesc大小。instanceOopDesc繼承自oopDesc,而oopDesc類型大小在前面文章已經(jīng)計算出來了,是兩個指針寬度。假設(shè)JVM運行在64位架構(gòu)上,則這個值是16,而啟用了壓縮指針,則在64位架構(gòu)上,該值為12,這是因為無論是否開啟壓縮策略,oop._mark作為一個指針是不會被壓縮的,任何時候都會占用8字節(jié),而oop._klass則會受壓縮策略的影響,若開啟壓縮策略,則_klass僅會占用4字節(jié),所以在64位架構(gòu)上開啟壓縮策略的情況下,oop對象頭總共占用12字節(jié)的內(nèi)存空間。 ?
Java類是面向?qū)ο蟮模^承是面向?qū)ο缶幊痰娜筇匦灾唬?java.lang. Object 類以外,所有的 Java 類都顯式或隱式地繼承了某個父類,而字段繼承和方法繼承則構(gòu)成了繼承的全部。
如果說繼承是目標,那么字段在子類中的內(nèi)存占用則是技術(shù)手段子類必須將父類中定
義的非靜態(tài)字段信息全部復一遍,才能實現(xiàn)字段繼承的目標因此,在計算子類非靜態(tài)字段的起始偏移量時,必須將父類可被繼承字段的內(nèi)存大小考慮在內(nèi)。具體而言,子類的非靜態(tài)字段起始偏移量,在計算完oop對象頭的大小后,還需要為父類的可被繼承的字段預留空間。
Hotspot將父類可被繼承的字段的內(nèi)存空間安排在子類所對應的oop對象頭的后面,因此最終一個Java類中非靜態(tài)字段的起始偏移位置在父類被繼承的字段域的末尾,所下所示
內(nèi)存對齊與字段重排
Java類字段的偏移地址與內(nèi)存對齊有脫不開的關(guān)系,JVM為了處理內(nèi)存對齊,頗費了一番心思,不惜將字段進行重排。
什么是內(nèi)存對齊?
內(nèi)存對齊與數(shù)據(jù)在內(nèi)存中的位置有關(guān)如果一個變量的內(nèi)存起始地址正好等于其長度的整數(shù)倍,則這種內(nèi)存分配就被稱作自然對齊。
(1)在32位x86平臺上,基本的內(nèi)存對齊規(guī)則如下所示
舉個例子,在32位CPU下,假設(shè)一個int整形變量的內(nèi)存地址為0x00000008,那么這個變量就是自然對齊的。
(2)為什么需要字節(jié)對齊
現(xiàn)代計算機中內(nèi)存空間都是按照字節(jié)劃分的,也即內(nèi)存的力度細分到存儲單元,而一個存儲單元恰恰包含8個比特,正好是一字節(jié),因此從理論上講似乎對任何類型的變量的訪問可以從任何地址開始。
但實際情況恰恰相反,各個硬件平臺在對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數(shù)據(jù)只能從某些特定的地址開始存取。例如有些架構(gòu)的CPU在訪問一個沒有進行對齊的變量時會反生錯誤,那么在這種架構(gòu)下進行編程就需要保證字節(jié)對齊。
3.2內(nèi)存分配總結(jié)
規(guī)則1:任何對象都是以8字節(jié)為粒度進行對齊的。
規(guī)則2:類屬性按照如下優(yōu)先級進行排列:長整型和雙精度類型;整型和浮點型; 字符和短整型;字節(jié)類型和布爾類型;最后是引用類型。這些屬性都按照各自類 型寬度對齊。 。
規(guī)則3:不同類繼承關(guān)系中的成員不能混合排列。首先按照規(guī)則2處理父類中的成員,接著才是子類的成員 。
規(guī)則4:當父類最后一個屬性和子類第一個屬性之間間隔不足4字節(jié)時,必須擴展到4字節(jié)的基本單位。 ?
規(guī)則5:如果子類第一個成員是一個雙精度或長整型,并且父類沒有用完 8 字節(jié) (沒有顯式的父類,并且JVM開啟了指針壓縮策略,oop對象頭只占用 12 字節(jié)時), JVM 會破壞規(guī)則 2,按整型(int)、短整型(short)、字節(jié)型(byte)、引用類型 (reference)的順序向未填滿的空間填充
4.Java類在堆內(nèi)存中的內(nèi)存空間,主要由Java類非靜態(tài)字段占據(jù)。Hotspot解析Java類非靜態(tài)字段和分配堆內(nèi)存空間的主要邏輯總結(jié)為如下幾步:
(1)解析常量池,統(tǒng)計Java 類中非靜態(tài)字段的總數(shù)量,按照5大類型(oops、 longs/doubles、ints、horts/chars、bytes)分別統(tǒng)計。
(2)計算Java類字段的起始偏移量,起始偏移位置從父類繼承的字段域的末尾開始
(3)按照分配策略,計算5大類型中的每一個類型的起始偏移量。
(4) 以5大類型各個類型的起始偏移量為基準,計算每一個大類型下各個具體字段的偏移量。
(5)計算Java類在堆內(nèi)存中所需要的內(nèi)存空間。
經(jīng)過上面5步,HotSpot便能確定一個Java類所需要的堆內(nèi)存空間。 當全部解析完Java 類之后,Java 類的全部字段信息及其偏移量將會保存到Hotspot 所構(gòu)建出來的instanceKlass 中,至此,一個Java類的字段結(jié)構(gòu)信息便全部解析完成。當Java程序中使用new關(guān)鍵字創(chuàng)建Java類的實例對象時,Hotspot便會從instanceKlass中讀取Java類所需要的堆內(nèi)存大小并分配對應的內(nèi)存空間。