類變量解析

類變量解析

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)存空間。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

  • 第二部分 自動內(nèi)存管理機制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運行數(shù)據(jù)區(qū)域 程序計數(shù)器:當前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,204評論 0 2
  • 《深入理解Java虛擬機》筆記_第一遍 先取看完這本書(JVM)后必須掌握的部分。 第一部分 走近 Java 從傳...
    xiaogmail閱讀 5,152評論 1 34
  • 一:java概述:1,JDK:Java Development Kit,java的開發(fā)和運行環(huán)境,java的開發(fā)工...
    ZaneInTheSun閱讀 2,686評論 0 11
  • 第6章類文件結(jié)構(gòu) 6.1 概述 6.2 無關(guān)性基石 6.3 Class類文件的結(jié)構(gòu) java虛擬機不和包括java...
    kennethan閱讀 967評論 0 2
  • 一、java執(zhí)行引擎工作原理:方法調(diào)用 1.進行方法調(diào)用 Java語言的原子指令是字節(jié)碼,java方法是對字節(jié)碼的...
    WANGGGGG閱讀 790評論 0 3