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