有關(guān) java 內(nèi)存方面的教程 請(qǐng)看 http://www.tudou.com/programs/view/hN_4sQJMoFQ/
今天看了一下java的內(nèi)存分配,分享一下:
基礎(chǔ)數(shù)據(jù)類型直接在棧空間分配, 方法的形式參數(shù),直接在棧空間分配,當(dāng)方法調(diào)用完成后從棧空間回收。 引用數(shù)據(jù)類型,需要用new來創(chuàng)建,既在棧空間分配一個(gè)地址空間,又在堆空間分配對(duì)象的類變量 。 方法的引用參數(shù),在棧空間分配一個(gè)地址空間,并指向堆空間的對(duì)象區(qū),當(dāng)方法調(diào)用完成后從棧空間回收。局部變量 new 出來時(shí),在棧空間和堆空間中分配空間,當(dāng)局部變量生命周期結(jié)束后,棧空間立刻被回收,堆空間區(qū)域等待GC回收。 方法調(diào)用時(shí)傳入的 literal 參數(shù),先在棧空間分配,在方法調(diào)用完成后從棧空間分配。字符串常量在 DATA 區(qū)域分配 ,this 在堆空間分配 。數(shù)組既在棧空間分配數(shù)組名稱, 又在堆空間分配數(shù)組實(shí)際的大小!
哦 對(duì)了,補(bǔ)充一下static在DATA區(qū)域分配。
其實(shí)是有規(guī)律的,只要你理解了這些個(gè)基本的原理:
堆空間的話: 操作系統(tǒng)有一個(gè)記錄空閑內(nèi)存地址的鏈表,當(dāng)系統(tǒng)收到程序的申請(qǐng)時(shí),會(huì)遍歷該鏈表,尋找第一個(gè)空間大于所申請(qǐng)空間的堆結(jié)點(diǎn),然后將該結(jié)點(diǎn)從空閑結(jié)點(diǎn)鏈表中刪除,并將該結(jié)點(diǎn)的空間分配給程序,另外,對(duì)于大多數(shù)系統(tǒng),會(huì)在這塊內(nèi)存空間中的首地址處記錄本次分配的大小,這樣代碼中的delete語句才能正確的釋放本內(nèi)存空間。另外由于找到的堆結(jié)點(diǎn)的大小不一定正好等于申請(qǐng)的大小,系統(tǒng)會(huì)自動(dòng)的將多余的那部分重新放入空閑鏈表中。是由new分配的內(nèi)存,一般速度比較慢,而且容易產(chǎn)生內(nèi)存碎片,不過用起來最方便,另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內(nèi)存,他不是在堆,也不是在棧是直接在進(jìn)程的地址空間中保留一快內(nèi)存,雖然用起來最不方便。但是速度快,也最靈活。是向高地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是不連續(xù)的內(nèi)存區(qū)域。這是由于系統(tǒng)是用鏈表來存儲(chǔ)的空閑內(nèi)存地址的,自然是不連續(xù)的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存。由此可見,堆獲得的空間比較靈活,也比較大。
棧空間的話:在Windows下, 棧是向低地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是一塊連續(xù)的內(nèi)存的區(qū)域。這句話的意思是棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的,在WINDOWS下,棧的大小是固定的(是一個(gè)編譯時(shí)就確定的常數(shù)),如果申請(qǐng)的空間超過棧的剩余空間時(shí),將提示overflow。因此,能從棧獲得的空間較小。只要棧的剩余空間大于所申請(qǐng)空間,系統(tǒng)將為程序提供內(nèi)存,否則將報(bào)異常提示棧溢出。 由系統(tǒng)自動(dòng)分配,速度較快。但程序員是無法控制的。
ok,頭會(huì)不會(huì)有點(diǎn)小暈,不會(huì)的話繼續(xù)吧:
JVM中的堆和棧
JVM是基于堆棧的虛擬機(jī).JVM為每個(gè)新創(chuàng)建的線程都分配一個(gè)堆棧.也就是說,對(duì)于一個(gè)Java程序來說,它的運(yùn)行就是通過對(duì)堆棧的操作來完成的。堆棧以幀為單位保存線程的狀態(tài)。JVM對(duì)堆棧只進(jìn)行兩種操作:以幀為單位的壓棧和出棧操作。
我們知道,某個(gè)線程正在執(zhí)行的方法稱為此線程的當(dāng)前方法.我們可能不知道,當(dāng)前方法使用的幀稱為當(dāng)前幀。當(dāng)線程激活一個(gè)Java方法,JVM就會(huì)在線程的Java堆棧里新壓入一個(gè)幀。這個(gè)幀自然成為了當(dāng)前幀.在此方法執(zhí)行期間,這個(gè)幀將用來保存參數(shù),局部變量,中間計(jì)算過程和其他數(shù)據(jù).這個(gè)幀在這里和編譯原理中的活動(dòng)紀(jì)錄的概念是差不多的.
從Java的這種分配機(jī)制來看,堆棧又可以這樣理解:堆棧(Stack)是操作系統(tǒng)在建立某個(gè)進(jìn)程時(shí)或者線程(在支持多線程的操作系統(tǒng)中是線程)為這個(gè)線程建立的存儲(chǔ)區(qū)域,該區(qū)域具有先進(jìn)后出的特性。
每一個(gè)Java應(yīng)用都唯一對(duì)應(yīng)一個(gè)JVM實(shí)例,每一個(gè)實(shí)例唯一對(duì)應(yīng)一個(gè)堆。應(yīng)用程序在運(yùn)行中所創(chuàng)建的所有類實(shí)例或數(shù)組都放在這個(gè)堆中,并由應(yīng)用所有的線程共享.跟C/C++不同,Java中分配堆內(nèi)存是自動(dòng)初始化的。Java中所有對(duì)象的存儲(chǔ)空間都是在堆中分配的,但是這個(gè)對(duì)象的引用卻是在堆棧中分配,也就是說在建立一個(gè)對(duì)象時(shí)從兩個(gè)地方都分配內(nèi)存,在堆中分配的內(nèi)存實(shí)際建立這個(gè)對(duì)象,而在堆棧中分配的內(nèi)存只是一個(gè)指向這個(gè)堆對(duì)象的指針(引用)而已。
PS: 相關(guān)內(nèi)容
方法區(qū)
在一個(gè)jvm實(shí)例的內(nèi)部,類型信息被存儲(chǔ)在一個(gè)稱為方法區(qū)的內(nèi)存邏輯區(qū)中。類型信息是由類加載器在類加載時(shí)從類文件中提取出來的。類(靜態(tài))變量也存儲(chǔ)在方法區(qū)中。
jvm實(shí)現(xiàn)的設(shè)計(jì)者決定了類型信息的內(nèi)部表現(xiàn)形式。如,多字節(jié)變量在類文件是以big-endian存儲(chǔ)的,但在加載到方法區(qū)后,其存放形式由jvm根據(jù)不同的平臺(tái)來具體定義。
jvm在運(yùn)行應(yīng)用時(shí)要大量使用存儲(chǔ)在方法區(qū)中的類型信息。在類型信息的表示上,設(shè)計(jì)者除了要盡可能提高應(yīng)用的運(yùn)行效率外,還要考慮空間問題。根據(jù)不同的需求,jvm的實(shí)現(xiàn)者可以在時(shí)間和空間上追求一種平衡。
因?yàn)榉椒▍^(qū)是被所有線程共享的,所以必須考慮數(shù)據(jù)的線程安全。假如兩個(gè)線程都在試圖找lava的類,在lava類還沒有被加載的情況下,只應(yīng)該有一個(gè)線程去加載,而另一個(gè)線程等待。
方法區(qū)的大小不必是固定的,jvm可以根據(jù)應(yīng)用的需要?jiǎng)討B(tài)調(diào)整。同樣方法區(qū)也不必是連續(xù)的。方法區(qū)可以在堆(甚至是虛擬機(jī)自己的堆)中分配。jvm可以允許用戶和程序指定方法區(qū)的初始大小,最小和最大尺寸。
方法區(qū)同樣存在垃圾收集,因?yàn)橥ㄟ^用戶定義的類加載器可以動(dòng)態(tài)擴(kuò)展java程序,一些類也會(huì)成為垃圾。jvm可以回收一個(gè)未被引用類所占的空間,以使方法區(qū)的空間最小。
類型信息
對(duì)每個(gè)加載的類型,jvm必須在方法區(qū)中存儲(chǔ)以下類型信息:
一 這個(gè)類型的完整有效名
二 這個(gè)類型直接父類的完整有效名(除非這個(gè)類型是interface或是
java.lang.Object,兩種情況下都沒有父類)
三 這個(gè)類型的修飾符(public,abstract, final的某個(gè)子集)
四 這個(gè)類型直接接口的一個(gè)有序列表
類型名稱在java類文件和jvm中都以完整有效名出現(xiàn)。在java源代碼中,完整有效名由類的所屬包名稱加一個(gè)".",再加上類名
組成。例如,類Object的所屬包為java.lang,那它的完整名稱為java.lang.Object,但在類文件里,所有的"."都被
斜杠“/”代替,就成為java/lang/Object。完整有效名在方法區(qū)中的表示根據(jù)不同的實(shí)現(xiàn)而不同。
除了以上的基本信息外,jvm還要為每個(gè)類型保存以下信息:
類型的常量池( constant pool)
域(Field)信息
方法(Method)信息
除了常量外的所有靜態(tài)(static)變量
常量池
jvm為每個(gè)已加載的類型都維護(hù)一個(gè)常量池。常量池就是這個(gè)類型用到的常量的一個(gè)有序集合,包括實(shí)際的常量(string,
integer, 和floating point常量)和對(duì)類型,域和方法的符號(hào)引用。池中的數(shù)據(jù)項(xiàng)象數(shù)組項(xiàng)一樣,是通過索引訪問的。
因?yàn)槌A砍卮鎯?chǔ)了一個(gè)類型所使用到的所有類型,域和方法的符號(hào)引用,所以它在java程序的動(dòng)態(tài)鏈接中起了核心的作用。
域信息
jvm必須在方法區(qū)中保存類型的所有域的相關(guān)信息以及域的聲明順序,
域的相關(guān)信息包括:
域名
域類型
域修飾符(public, private, protected,static,final volatile, transient的某個(gè)子集)
方法信息
jvm必須保存所有方法的以下信息,同樣域信息一樣包括聲明順序
方法名
方法的返回類型(或 void)
方法參數(shù)的數(shù)量和類型(有序的)
方法的修飾符(public, private, protected, static, final, synchronized, native, abstract的一個(gè)子集)除了abstract和native方法外,其他方法還有保存方法的字節(jié)碼(bytecodes)操作數(shù)棧和方法棧幀的局部變量區(qū)的大小
異常表
類變量(
Class Variables
譯者:就是類的靜態(tài)變量,它只與類相關(guān),所以稱為類變量
)
類變量被類的所有實(shí)例共享,即使沒有類實(shí)例時(shí)你也可以訪問它。這些變量只與類相關(guān),所以在方法區(qū)中,它們成為類數(shù)據(jù)在邏輯上的一部分。在jvm使用一個(gè)類之前,它必須在方法區(qū)中為每個(gè)non-final類變量分配空間。
常量(被聲明為final的類變量)的處理方法則不同,每個(gè)常量都會(huì)在常量池中有一個(gè)拷貝。non-final類變量被存儲(chǔ)在聲明它的
類信息內(nèi),而final類被存儲(chǔ)在所有使用它的類信息內(nèi)。
對(duì)類加載器的引用
jvm必須知道一個(gè)類型是由啟動(dòng)加載器加載的還是由用戶類加載器加載的。如果一個(gè)類型是由用戶類加載器加載的,那么jvm會(huì)將這個(gè)類加載器的一個(gè)引用作為類型信息的一部分保存在方法區(qū)中。
jvm在動(dòng)態(tài)鏈接的時(shí)候需要這個(gè)信息。當(dāng)解析一個(gè)類型到另一個(gè)類型的引用的時(shí)候,jvm需要保證這兩個(gè)類型的類加載器是相同的。這對(duì)jvm區(qū)分名字空間的方式是至關(guān)重要的。
對(duì)Class類的引用
jvm為每個(gè)加載的類型(譯者:包括類和接口)都創(chuàng)建一個(gè)java.lang.Class的實(shí)例。而jvm必須以某種方式把Class的這個(gè)實(shí)例和存儲(chǔ)在方法區(qū)中的類型數(shù)據(jù)聯(lián)系起來。
你可以通過Class類的一個(gè)靜態(tài)方法得到這個(gè)實(shí)例的引用// A method declared in class java.lang.Class:
public static Class forName(String className);
假如你調(diào)用forName("java.lang.Object"),你會(huì)得到與java.lang.Object對(duì)應(yīng)的類對(duì)象。你甚至可以通過這個(gè)函數(shù)
得到任何包中的任何已加載的類引用,只要這個(gè)類能夠被加載到當(dāng)前的名字空間。如果jvm不能把類加載到當(dāng)前名字空間,
forName就會(huì)拋出ClassNotFoundException。
(譯者:熟悉COM的朋友一定會(huì)想到,在COM中也有一個(gè)稱為 類對(duì)象(Class Object)的東東,這個(gè)類對(duì)象主要 是實(shí)現(xiàn)一種工廠模式,而java由于有了jvm這個(gè)中間 層,類對(duì)象可以很方便的提供更多的信息。這兩種類對(duì)象 都是Singleton的)
也可以通過任一對(duì)象的getClass()函數(shù)得到類對(duì)象的引用,getClass被聲明在Object類中:
// A method declared in class java.lang.Object:
public final Class getClass();
例如,假如你有一個(gè)java.lang.Integer的對(duì)象引用,可以激活getClass()得到對(duì)應(yīng)的類引用。
通過類對(duì)象的引用,你可以在運(yùn)行中獲得相應(yīng)類存儲(chǔ)在方法區(qū)中的類型信息,下面是一些Class類提供的方法:
// Some of the methods declared in class java.lang.Class:
public String getName();
public Class getSuperClass();
public boolean isInterface();
public Class[] getInterfaces();
public ClassLoader getClassLoader();
這些方法僅能返回已加載類的信息。getName()返回類的完整名,getSuperClass()返回父類的類對(duì)象,isInterface()判斷是否是接口。getInterfaces()返回一組類對(duì)象,每個(gè)類對(duì)象對(duì)應(yīng)一個(gè)直接父接口。如果沒有,則返回一個(gè)長(zhǎng)度為零的數(shù)組。
getClassLoader()返回類加載器的引用,如果是由啟動(dòng)類加載器加載的則返回null。所有的這些信息都直接從方法區(qū)中獲得。
方法表
為了提高訪問效率,必須仔細(xì)的設(shè)計(jì)存儲(chǔ)在方法區(qū)中的數(shù)據(jù)信息結(jié)構(gòu)。除了以上討論的結(jié)構(gòu),jvm的實(shí)現(xiàn)者還可以添加一些其他的數(shù)據(jù)結(jié)構(gòu),如方法表。jvm對(duì)每個(gè)加載的非虛擬類的類型信息中都添加了一個(gè)方法表,方法表是一組對(duì)類實(shí)例方法的直接引用(包括從父類繼承的方法)。jvm可以通過方法表快速激活實(shí)例方法。(譯者:這里的方法表與C++中的虛擬函數(shù)表一樣,但java方法全都 是virtual的,自然也不用虛擬二字了。正像java宣稱沒有 指針了,其實(shí)java里全是指針。更安全只是加了更完備的檢查機(jī)制,但這都是以犧牲效率為代價(jià)的,個(gè)人認(rèn)為java的設(shè)計(jì)者 始終是把安全放在效率之上的,所有java才更適合于網(wǎng)絡(luò)開發(fā))
一個(gè)例子
為了顯示jvm如何使用方法區(qū)中的信息,我們據(jù)一個(gè)例子,我們
看下面這個(gè)類:
class Lava {
private int speed = 5; // 5 kilometers per hour
void flow() {
}
}
class Volcano {
public static void main(String[] args) {
Lava lava = new Lava();
lava.flow();
}
}
下面我們描述一下main()方法的第一條指令的字節(jié)碼是如何被執(zhí)行的。不同的jvm實(shí)現(xiàn)的差別很大,這里只是其中之一。
為了運(yùn)行這個(gè)程序,你以某種方式把“Volcano"傳給了jvm。有了這個(gè)名字,jvm找到了這個(gè)類文件(Volcano.class)并讀入,它從
類文件提取了類型信息并放在了方法區(qū)中,通過解析存在方法區(qū)中的字節(jié)碼,jvm激活了main()方法,在執(zhí)行時(shí),jvm保持了一個(gè)指向當(dāng)前類(Volcano)常量池的指針。
注意jvm在還沒有加載Lava類的時(shí)候就已經(jīng)開始執(zhí)行了。正像大多數(shù)的jvm一樣,不會(huì)等所有類都加載了以后才開始執(zhí)行,它只會(huì)在需要的時(shí)候才加載。
main()的第一條指令告知jvm為列在常量池第一項(xiàng)的類分配足夠的內(nèi)存。jvm使用指向Volcano常量池的指針找到第一項(xiàng),發(fā)現(xiàn)是一個(gè)對(duì)Lava類的符號(hào)引用,然后它就檢查方法區(qū)看lava是否已經(jīng)被加載了。
這個(gè)符號(hào)引用僅僅是類lava的完整有效名”lava“。這里我們看到為了jvm能盡快從一個(gè)名稱找到一個(gè)類,一個(gè)良好的數(shù)據(jù)結(jié)構(gòu)是多么重要。這里jvm的實(shí)現(xiàn)者可以采用各種方法,如hash表,查找樹等等。同樣的算法可以用于Class類的forName()的實(shí)現(xiàn)。
當(dāng)jvm發(fā)現(xiàn)還沒有加載過一個(gè)稱為"Lava"的類,它就開始查找并加載類文件"Lava.class"。它從類文件中抽取類型信息并放在了方法區(qū)中。
jvm于是以一個(gè)直接指向方法區(qū)lava類的指針替換了常量池第一項(xiàng)的符號(hào)引用。以后就可以用這個(gè)指針快速的找到lava類了。而這個(gè)替換過程稱為常量池解析(constant pool resolution)。在這里我們替換的是一個(gè)native指針。
jvm終于開始為新的lava對(duì)象分配空間了。這次,jvm仍然需要方法區(qū)中的信息。它使用指向lava數(shù)據(jù)的指針(剛才指向volcano常量池第一項(xiàng)的指針)找到一個(gè)lava對(duì)象究竟需要多少空間。
jvm總能夠從存儲(chǔ)在方法區(qū)中的類型信息知道某類型對(duì)象需要的空間。但一個(gè)對(duì)象在不同的jvm中可能需要不同的空間,而且它的空間分布也是不同的。(譯者:這與在C++中,不同的編譯器也有不同的對(duì)象模型是一個(gè)道理)
一旦jvm知道了一個(gè)Lava對(duì)象所要的空間,它就在堆上分配這個(gè)空間并把這個(gè)實(shí)例的變量speed初始化為缺省值0。假如lava的父對(duì)象也有實(shí)例變量,則也會(huì)初始化。
當(dāng)把新生成的lava對(duì)象的引用壓到棧中,第一條指令也結(jié)束了。下面的指令利用這個(gè)引用激活java代碼把speed變量設(shè)為初始值,5。另外一條指令會(huì)用這個(gè)引用激活Lava對(duì)象的flow()方法。
本博客為學(xué)習(xí)交流用,凡未注明引用的均為本人作品,轉(zhuǎn)載請(qǐng)注明出處,如有版權(quán)問題請(qǐng)及時(shí)通知。由于博客時(shí)間倉促,錯(cuò)誤之處敬請(qǐng)諒解,有任何意見可給我留言,愿共同學(xué)習(xí)進(jìn)步。