首先是概念層面的幾個問題:
然后是運(yùn)用層面:
好了,問題提完了,如果您都能一眼看出答案,那么,沒有必要再浪費(fèi)您寶貴的時間看下去了。
如果您還不太明白,請跟隨我一路走下去。
Java中運(yùn)行時內(nèi)存結(jié)構(gòu)
1.1 方法區(qū):
方法區(qū)是系統(tǒng)分配的一個內(nèi)存邏輯區(qū)域,是JVM在裝載類文件時,用于存儲類型信息的(類的描述信息)。
方法區(qū)存放的信息包括:
1.1.1類的基本信息:
1.1.2已裝載類的詳細(xì)信息:
在方法區(qū)中,每個類型都對應(yīng)一個常量池,存放該類型所用到的所有常量,常量池中存儲了諸如文字字符串、final變量值、類名和方法名常量。它們以數(shù)組形式通過索引被訪問,是外部調(diào)用與類聯(lián)系及類型對象化的橋梁。(存的可能是個普通的字符串,然后經(jīng)過常量池解析,則變成指向某個類的引用)
字段信息存放類中聲明的每一個字段的信息,包括字段的名、類型、修飾符。
字段名稱指的是類或接口的實(shí)例變量或類變量,字段的描述符是一個指示字段的類型的字符串,如private A a=null;則a為字段名,A為描述符,private為修飾符
類中聲明的每一個方法的信息,包括方法名、返回值類型、參數(shù)類型、修飾符、異常、方法的字節(jié)碼。
(在編譯的時候,就已經(jīng)將方法的局部變量、操作數(shù)棧大小等確定并存放在字節(jié)碼中,在裝載的時候,隨著類一起裝入方法區(qū)。)
在運(yùn)行時,JVM從常量池中獲得符號引用,然后在運(yùn)行時解析成引用項的實(shí)際地址,最后通過常量池中的全限定名、方法和字段描述符,把當(dāng)前類或接口中的代碼與其它類或接口中的代碼聯(lián)系起來。 |
這個沒什么好說的,就是類變量,類的所有實(shí)例都共享,我們只需知道,在方法區(qū)有個靜態(tài)區(qū),靜態(tài)區(qū)專門存放靜態(tài)變量和靜態(tài)塊。
由此我們可以知道反射的基礎(chǔ):
在裝載類的時候,加入方法區(qū)中的所有信息,最后都會形成Class類的實(shí)例,代表這個被裝載的類。方法區(qū)中的所有的信息,都是可以通過這個Class類對象反射得到。我們知道對象是類的實(shí)例,類是相同結(jié)構(gòu)的對象的一種抽象。同類的各個對象之間,其實(shí)是擁有相同的結(jié)構(gòu)(屬性),擁有相同的功能(方法),各個對象的區(qū)別只在于屬性值的不同。 同樣的,我們所有的類,其實(shí)都是Class類的實(shí)例,他們都擁有相同的結(jié)構(gòu)-----Field數(shù)組、Method數(shù)組。而各個類中的屬性都是Field屬性的一個具體屬性值,方法都是Method屬性的一個具體屬性值。 |
在運(yùn)行時,JVM從常量池中獲得符號引用,然后在運(yùn)行時解析成引用項的實(shí)際地址,最后通過常量池中的全限定名、方法和字段描述符,把當(dāng)前類或接口中的代碼與其它類或接口中的代碼聯(lián)系起來。
1.2 Java棧
JVM棧是程序運(yùn)行時單位,決定了程序如何執(zhí)行,或者說數(shù)據(jù)如何處理。
在Java中,一個線程就會有一個線程的JVM棧與之對應(yīng),因?yàn)椴贿^的線程執(zhí)行邏輯顯然不同,因此都需要一個獨(dú)立的JVM棧來存放該線程的執(zhí)行邏輯。
對方法的調(diào)用:
Java棧內(nèi)存,以幀的形式存放本地方法的調(diào)用狀態(tài),包括方法調(diào)用的參數(shù)、局部變量、中間結(jié)果等(方法都是以方法幀的形式存放在方法區(qū)的),每調(diào)用一個方法就將對應(yīng)該方法的方法幀壓入Java 棧,成為當(dāng)前方法幀。當(dāng)調(diào)用結(jié)束(返回)時,就彈出該幀。
這意味著:
在方法中定義的一些基本類型的變量和引用變量都在方法的棧內(nèi)存中分配。當(dāng)在一段代碼塊定義一個變量時,Java 就在棧中為這個變量分配內(nèi)存空間,當(dāng)超過變量的作用域后(方法執(zhí)行完成后),Java 會自動釋放掉為該變量所分配的內(nèi)存空間,該內(nèi)存空間可以立即被另作它用。--------同時,因?yàn)樽兞勘会尫?,該變量對?yīng)的對象,也就失去了引用,也就變成了可以被gc對象回收的垃圾。
因此我們可以知道成員變量與局部變量的區(qū)別:
局部變量,在方法內(nèi)部聲明,當(dāng)該方法運(yùn)行完時,內(nèi)存即被釋放。 成員變量,只要該對象還在,哪怕某一個方法運(yùn)行完了,還是存在。 從系統(tǒng)的角度來說,聲明局部變量有利于內(nèi)存空間的更高效利用(方法運(yùn)行完即回收)。 成員變量可用于各個方法間進(jìn)行數(shù)據(jù)共享。 |
Java 棧內(nèi)存的組成:
局部變量區(qū)、操作數(shù)棧、幀數(shù)據(jù)區(qū)組成。
(1):局部變量區(qū)為一個以字為單位的數(shù)組,每個數(shù)組元素對應(yīng)一個局部變量的值。調(diào)用方法時,將方法的局部變量組成一個數(shù)組,通過索引來訪問。若為非靜態(tài)方法,則加入一個隱含的引用參數(shù)this,該參數(shù)指向調(diào)用這個方法的對象。而靜態(tài)方法則沒有this參數(shù)。因此,對象無法調(diào)用靜態(tài)方法。
由此,我們可以知道,方法什么時候設(shè)計為靜態(tài),什么時候?yàn)榉庆o態(tài)?
前面已經(jīng)說過,對象是類的一個實(shí)例,各個對象結(jié)構(gòu)相同,只是屬性不同。 而靜態(tài)方法是對象無法調(diào)用的。 所以,靜態(tài)方法適合那些工具類中的工具方法,這些類只是用來實(shí)現(xiàn)一些功能,也不需要產(chǎn)生對象,通過設(shè)置對象的屬性來得到各個不同的個體。 |
(2):操作數(shù)棧也是一個數(shù)組,但是通過棧操作來訪問。所謂操作數(shù)是那些被指令操作的數(shù)據(jù)。當(dāng)需要對參數(shù)操作時如a=b+c,就將即將被操作的參數(shù)壓棧,如將b 和c 壓棧,然后由操作指令將它們彈出,并執(zhí)行操作。虛擬機(jī)將操作數(shù)棧作為工作區(qū)。
(3):幀數(shù)據(jù)區(qū)處理常量池解析,異常處理等
1.3 java堆
java的堆是一個運(yùn)行時的數(shù)據(jù)區(qū),用來存儲數(shù)據(jù)的單元,存放通過new關(guān)鍵字新建的對象和數(shù)組,對象從中分配內(nèi)存。
在堆中聲明的對象,是不能直接訪問的,必須通過在棧中聲明的指向該引用的變量來調(diào)用。引用變量就相當(dāng)于是為數(shù)組或?qū)ο笃鸬囊粋€名稱,以后就可以在程序中使用棧中的引用變量來訪問堆中的數(shù)組或?qū)ο蟆?/font>
由此我們可以知道,引用類型變量和對象的區(qū)別:
聲明的對象是在堆內(nèi)存中初始化的, 真正用來存儲數(shù)據(jù)的。不能直接訪問。 引用類型變量是保存在棧當(dāng)中的,一個用來引用堆中對象的符號而已(指針)。 |
堆與棧的比較:
JAVA堆與棧都是用來存放數(shù)據(jù)的,那么他們之間到底有什么差異呢?既然棧也能存放數(shù)據(jù),為什么還要設(shè)計堆呢?
1.從存放數(shù)據(jù)的角度:
前面我們已經(jīng)說明:
棧中存放的是基本類型的變量or引用類型的變量
堆中存放的是對象or數(shù)組對象.
在棧中,引用變量的大小為32位,基本類型為1-8個字節(jié)。
但是對象的大小和數(shù)組的大小是動態(tài)的,這也決定了堆中數(shù)據(jù)的動態(tài)性,因?yàn)樗窃谶\(yùn)行時動態(tài)分配內(nèi)存的,生存期也不必在編譯時確定,Java 的垃圾收集器會自動收走這些不再使用的數(shù)據(jù)。
2.從數(shù)據(jù)共享的角度:
1).在單個線程類,棧中的數(shù)據(jù)可共享
例如我們定義:
int a=3;int b=3;
編譯器先處理int a = 3;首先它會在棧中創(chuàng)建一個變量為a 的引用,然后查找棧中是否有3 這個值,如果沒找到,就將3 存放進(jìn)來,然后將a 指向3。接著處理int b = 3;在創(chuàng)建完b 的引用變量后,因?yàn)樵跅V幸呀?jīng)有3這個值,便將b 直接指向3。這樣,就出現(xiàn)了a 與b 同時均指向3的情況。
而如果我們定義:
Integer a=new Integer(3);//(1)Integer b=new Integer(3);//(2)
這個時候執(zhí)行過程為:在執(zhí)行(1)時,首先在棧中創(chuàng)建一個變量a,然后在堆內(nèi)存中實(shí)例化一個對象,并且將變量a指向這個實(shí)例化的對象。在執(zhí)行(2)時,過程類似,此時,在堆內(nèi)存中,會有兩個Integer類型的對象。
2).在進(jìn)程的各個線程之間,數(shù)據(jù)的共享通過堆來實(shí)現(xiàn)
例:那么,在多線程開發(fā)中,我們的數(shù)據(jù)共享又是怎么實(shí)現(xiàn)的呢?
如圖所示,堆中的數(shù)據(jù)是所有線程棧所共享的,我們可以通過參數(shù)傳遞,將一個堆中的數(shù)據(jù)傳入各個棧的工作內(nèi)存中,從而實(shí)現(xiàn)多個線程間的數(shù)據(jù)共享
(多個進(jìn)程間的數(shù)據(jù)共享則需要通過網(wǎng)絡(luò)傳輸了。)
3.從程序設(shè)計的的角度:
從軟件設(shè)計的角度看,JVM棧代表了處理邏輯,而JVM堆代表了數(shù)據(jù)。這樣分開,使得處理邏輯更為清晰。分而治之的思想。這種隔離、模塊化的思想在軟件設(shè)計的方方面面都有體現(xiàn)。
4.值傳遞和引用傳遞的真相
有了以上關(guān)于棧和堆的種種了解后,我們很容易就可以知道值傳遞和引用傳遞的真相:
1.程序運(yùn)行永遠(yuǎn)都是在JVM棧中進(jìn)行的,因而參數(shù)傳遞時,只存在傳遞基本類型和對象引用的問題。不會直接傳對象本身。 但是傳引用的錯覺是如何造成的呢? 在運(yùn)行JVM棧中,基本類型和引用的處理是一樣的,都是傳值,所以,如果是傳引用的方法調(diào)用,也同時可以理解為“傳引用值”的傳值調(diào)用,即引用的處理跟基本類型是完全一樣的。 但是當(dāng)進(jìn)入被調(diào)用方法時,被傳遞的這個引用的值,被程序解釋(或者查找)到JVM堆中的對象,這個時候才對應(yīng)到真正的對象。 如果此時進(jìn)行修改,修改的是引用對應(yīng)的對象,而不是引用本身,即:修改的是JVM堆中的數(shù)據(jù)。所以這個修改是可以保持的了。 |
最后:
從某種意義上來說對象都是由基本類型組成的。
可以把一個對象看作為一棵樹,對象的屬性如果還是對象,則還是一顆樹(即非葉子節(jié)點(diǎn)),基本類型則為樹的葉子節(jié)點(diǎn)。程序參數(shù)傳遞時,被傳遞的值本身都是不能進(jìn)行修改的,但是,如果這個值是一個非葉子節(jié)點(diǎn)(即一個對象引用),則可以修改這個節(jié)點(diǎn)下面的所有內(nèi)容。 |
其實(shí),面向?qū)ο蠓绞降某绦蚺c以前結(jié)構(gòu)化的程序在執(zhí)行上沒有任何區(qū)別。
面向?qū)ο蟮囊耄皇?/span>改變了我們對待問題的思考方式,而更接近于自然方式的思考。
當(dāng)我們把對象拆開,其實(shí)對象的屬性就是數(shù)據(jù),存放在JVM堆中;而對象的行為(方法),就是運(yùn)行邏輯,放在JVM棧中。我們在編寫對象的時候,其實(shí)即編寫了數(shù)據(jù)結(jié)構(gòu),也編寫的處理數(shù)據(jù)的邏輯。
聯(lián)系客服