一步搞清楚多态与类初始化的底层原理
首先我們先看一個段非常有代表性的代碼,里面一口氣牽扯到了多態(tài)和類初始化順序知識。
public class Test {public static void main(String[] args) {A test = new B();} }class A {int value = 10;A() {System.out.println("父類構(gòu)造器");process();}public void process() {System.out.println("父類的process");value++;System.out.println(value);} }class B extends A {int value = 12;{value++;}B() {System.out.println("子類構(gòu)造器");process();}public void process() {System.out.println("子類的process");System.out.println(value);value++;System.out.println(value);} }它的輸出是:
父類構(gòu)造器
子類的process
0
1
子類構(gòu)造器
子類的process
13
14
我想現(xiàn)在你一定很困惑,不要慌上車!帶你了解底層的原理
為什么會調(diào)用子類的process()方法?
這里的底層原理是Java的動態(tài)分派機(jī)制
對于方法重寫,Java采用的是動態(tài)分派機(jī)制,也就是說在運行的時候才確定調(diào)用哪個方法。由于A的實際類型是B,因此調(diào)用的就是B的process()法。
原理在底層字節(jié)碼中的invokevirtual指令的多態(tài)查找過程,分為以下幾個步驟:
從這個過程可以發(fā)現(xiàn),在第一步的時候就在運行期確定接收對象(執(zhí)行方法的所有者程稱為接受者)的實際類型,所以當(dāng)調(diào)用invokevirtual指令就會把運行時常量池中符號引用解析為直接引用,這就是方法重寫的本質(zhì)。
相信到這你還是迷迷糊糊的,那是因為缺少對類加載過程中解析知識的了解
解析是類加載的過程之一
解析階段時虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。
- 符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義的地位到目標(biāo)即可。
- 直接引用可以是直接指向目標(biāo)的指針、相對偏移量或是一個能間接定位到目標(biāo)的句柄。
- 我們知道Class文件的常量池中存有大量的符號引用(字節(jié)碼中方法調(diào)用指令就以常量池中指向方法的符號引用作為參數(shù))。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。
- 另一部分將在每一次運行期間轉(zhuǎn)化為直接引用,這部分稱為動態(tài)連接。
通俗點說,所有方法調(diào)用中的目標(biāo)方法在Class文件里面都是一個常量池中的符號引用,在類加載的解析階段,會將其中的一部分符號引用轉(zhuǎn)化為直接引用,這種解析能成立的前提是——>方法在程序真正運行之前就有一個可確定的調(diào)用版本(主要是靜態(tài)方法和私有方法),它們的調(diào)用版本在運行期是不可變的。因為靜態(tài)方法和私有方法不可能通過繼承或別的方式重寫成其他版本!!劃重點——>其他版本,因此他們都在類加載階段解析完成了。
綜上可知,在動態(tài)分派的機(jī)制下,因為子類繼承父類重寫了process()方法,只有在程序運行時才能確定的調(diào)用版本,將符號引用轉(zhuǎn)化成了直接引用,指向了實例的process()方法。
這種在運行期根據(jù)實際類型確定方法執(zhí)版本的分派過程就是動態(tài)分派。
為什么打印出來的是0和1?
這是因為在對象實例化的時候,劃分內(nèi)存后會直接賦零值。
對象的創(chuàng)建
- 虛擬機(jī)遇到一條new指令時,首先將會去檢查這個指令的參數(shù)能否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。
- 在類加載檢查通過后,虛擬機(jī)將為新生對象分配內(nèi)存。對象所需內(nèi)存的大小在類加載完成后便可完全確定,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。
- 如果Java堆中的內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內(nèi)存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種叫做指針碰撞。
- 如果Java堆中的內(nèi)存不是規(guī)整的,虛擬機(jī)就必須維護(hù)一個列表,記錄哪塊內(nèi)存塊是可用的,在分配的時候從列表中到找一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這鐘叫做空閑列表。
- 并發(fā)分配對象內(nèi)存有兩種解決方案->方案一:虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性;方案二:把內(nèi)存分配的動作按照線程劃分在不同的空間之中進(jìn)行,即每個線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩存(Thread Local Allocation Buffer,TLAB)。哪個線程要分配內(nèi)存就在哪個線程的TLAB上分配,只有TLAB用完并分配新的TLAB時,才需要同步鎖定。是否開啟TLAB:-XX:+/-UseTLAB
- 內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值,這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值。
- 虛擬機(jī)要對對象進(jìn)行設(shè)置,例如對象是哪個實例,如何找到類的元數(shù)據(jù)信息、對象的哈希碼、對象GC分代年齡,將這些信息存放在對象頭之中。
- 知執(zhí)行new指令之后會接著執(zhí)行init方法,把對象按照程序員的意愿進(jìn)行初始化。
可以知道,當(dāng)父類調(diào)用子類的process()方法時,子類并沒有初始化完成,僅僅是分配了內(nèi)存,這里有個實例變量初始化順序:
遵循的原則是:
(1)按照代碼中的順序依次執(zhí)行實例變量定義語句和實例變量代碼塊;
(2)如果創(chuàng)建該類的對象時該類的類變量尚未初始化,則先初始化類變量,再初始化實例變量;
(3)如果該類有父類的話,則先創(chuàng)建一個父類對象;并且,如果父類類變量沒被初始化時,先初始化父類的類變量,再初始化父類的實例變量,再調(diào)用父類的默認(rèn)構(gòu)造器;
相信到這你理解了為什么會打出0和1了,是因為父類的構(gòu)造函數(shù)是在子類的實例變量初始化之前執(zhí)行的。所以當(dāng)輸出value時,其值為0。
轉(zhuǎn)載于:https://www.cnblogs.com/keeya/p/9380107.html
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的一步搞清楚多态与类初始化的底层原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .net core datatable
- 下一篇: centos7搭建SVN+Apache+