很多人問,作為一個Android開發(fā)有必要了解Java內(nèi)存分配機制嗎严嗜?答案是肯定的。
java的內(nèi)存區(qū)域劃分實際上遠比這復(fù)雜:java虛擬機在執(zhí)行Java
程序的過程中會把所有的內(nèi)存劃分為不同的數(shù)據(jù)區(qū)域洲敢,下面這張圖
描述了一個HelloWorld.java文件被JVM加載到內(nèi)存中的過程:
1.HelloWorld.java 文件首先需要經(jīng)過編譯器編譯漫玄,生成HelloWorld.class 字節(jié)碼文件。
2.Java程序中訪問HelloWorld這個類時压彭,需要通過ClassLoader將HelloWorld.class加載到JVM內(nèi)存中睦优。
3.JVM中的內(nèi)存可以劃分為若干個不同的數(shù)據(jù)區(qū)域,包括:
程序計數(shù)器壮不,虛擬機棧汗盘,本地方法棧,堆询一,方法區(qū)隐孽。
1.1 程序計數(shù)器
Java是多線程的,CPU可以在多個線程中分配執(zhí)行時間片段健蕊。當一個線程被CPU掛起時菱阵,需要記錄代碼已經(jīng)執(zhí)行到的位置,方便CPU重新執(zhí)行此線程時缩功,知道從那行執(zhí)行開始執(zhí)行晴及,這就是程序計數(shù)器的組作用。
程序計數(shù)器是虛擬機中一塊較小的內(nèi)存空間嫡锌,主要用于記錄當前線程
執(zhí)行的位置虑稼。
如上圖所示:每個線程都會記錄當前方法執(zhí)行到的一個位置,當CPU切換到某一個線程上時势木,根據(jù)程序計數(shù)器記錄的數(shù)字动雹,繼續(xù)向下執(zhí)行指令。
關(guān)于程序計數(shù)器還有幾個需要格外注意:
1.在Java虛擬機規(guī)范中跟压,對程序計數(shù)器這一區(qū)域規(guī)定沒有任何OutOfMemoryError情況。
2.線程私有歼培,每個線程內(nèi)部都有一個私有的程序計數(shù)器震蒋,他的生命周期隨著線程創(chuàng)建而創(chuàng)建茸塞,結(jié)束而死亡。
3.當一個線程正在執(zhí)行一個Java方法時查剖,這個程序計數(shù)器記錄正在執(zhí)行虛擬機字節(jié)指令的地址钾虐,如果正在執(zhí)行的是Native方法,這個程序計數(shù)器的數(shù)值為空
1.2本地方法棧
本地方法棧和下面即將要講的虛擬機椝褡基本相同效扫,只不過是針對本地方法,在Android開發(fā)涉及JNI可能接觸本地方法多一些直砂,有一些虛擬機的實現(xiàn)已經(jīng)將本地方法棧和虛擬機棧合二為一了(HotSpot)菌仁。
1.3虛擬機棧
虛擬機棧也是線程私有的,與線程的生命周期同步静暂,在Java虛擬機中济丘,規(guī)定了兩種異常:
1.StackOverflowError :當前線程請求棧深度超出虛擬機棧所允許的深度時拋出。
2.OutOfMemoryError:當JVM動態(tài)擴展到無法申請足夠內(nèi)存時拋出洽蛀。
在我們看一些博客和書的時候摹迷,經(jīng)常看到一句話:JVM是基于棧解釋器執(zhí)行的郊供,DVM是基于寄存器解釋器執(zhí)行的峡碉。
上面那句話基于棧值得就是虛擬機棧,虛擬機棧的初衷是用來描述Java方法執(zhí)行的內(nèi)存模型驮审,每個方法執(zhí)行的時候鲫寄,JVM都會在虛擬機棧中創(chuàng)建一個棧幀,讓我們看一下棧幀是什么头岔?
1.3.1棧幀
每一個線程在執(zhí)行某一個方法時塔拳,都會為這個方法創(chuàng)建一個棧幀,
我們可以這樣理解:一個線程包含多個棧幀峡竣,每個棧幀內(nèi)部包含:局部變量表靠抑,操作數(shù)棧,動態(tài)鏈接适掰,返回地址等颂碧。
1.3.1.1 局部變量表
局部變量表是變量值的存儲空間,我們調(diào)用方法傳遞的參數(shù)类浪,以及在方法內(nèi)部創(chuàng)建的變量都會保存在局部變量表中载城。
1.3.1.2 操作數(shù)棧
操作數(shù)棧也常稱為操作棧,他是一個后入先出棧费就。
當一個方法剛剛開始執(zhí)行的時候诉瓦,這個方法的操作數(shù)棧是空的,在方法執(zhí)行的過程中,會把各種指令壓入和彈出操作數(shù)棧睬澡。
1.3.1.3 動態(tài)鏈接
動態(tài)鏈接的主要目的是為了支持方法在調(diào)用過程中的動態(tài)鏈接固额。
在一個class文件中,一個方法要調(diào)用其他方法煞聪,需要將這些方法的符號引用轉(zhuǎn)化為其所在內(nèi)存地址中的直接引用斗躏,而符號引用存在于方法區(qū)中。
1.3.1.4 返回地址
當一個方法開始執(zhí)行后昔脯,只有兩種方式可以退出這個方法:
1.正常退出:方法中的代碼正常完成啄糙,或者遇到任意一個返回的指令。
2.異常退出:方法執(zhí)行過程中遇到異常云稚,并且內(nèi)部沒有處理隧饼。
正常退出時,棧幀中可能保存此數(shù)值作為返回地址碱鳞。方法異常退出時桑李,棧幀中一般不會保存部分信息。
1.4 堆
Java堆是JVM所管理的內(nèi)存中最大的一塊窿给,該區(qū)的唯一目的就是存放對象實例贵白,幾乎所有的對象的實例都在堆里分配,因此他是GC管理的主要區(qū)域崩泡,有時候也叫GC堆同時他是所有線程的內(nèi)存區(qū)域禁荒,因此被分配在此區(qū)域的對象如果被多個線程訪問的話,需要考慮線程安全角撞。
按照對象存儲時間的不同呛伴,可以分為新生代,老年代谒所,其中新生代又被分為Eden和Survivor區(qū)热康。
圖中不同的區(qū)域具有不同的生命周期,可以根據(jù)不同區(qū)域使用不同的垃圾回收算法劣领,進而提高垃圾回收率姐军。
1.5 方法區(qū)
方法區(qū)也是JVM規(guī)范里規(guī)定的一塊運行時數(shù)據(jù)區(qū)。主要存儲已經(jīng)被JVM加載的類信息(版本尖淘,字段奕锌,方法,接口)村生,常量惊暴,靜態(tài)變量,即時編譯器后的代碼和數(shù)據(jù)趁桃,該區(qū)域也是被各個線程共享辽话。
1.6 異常再現(xiàn)
StackOverflowError 棧溢出:
在method方法中肄鸽,遞歸調(diào)用了自身,并且沒有設(shè)置遞歸結(jié)束條件屡穗,所以出現(xiàn)了StackOverflowError異常贴捡。
OutOfMemoryError 內(nèi)存溢出:
理論上,虛擬機棧村砂,方法去,堆都有可能發(fā)生OutOfMemoryError屹逛,但在實際過程中础废,大多數(shù)發(fā)生于堆中。
在一個無線循環(huán)中罕模,動態(tài)向List添加HeapError對象评腺,這會不斷的占用堆中的內(nèi)存,當堆內(nèi)存不夠時淑掌,必然會產(chǎn)生OutOfMemoryError蒿讥,也就是內(nèi)存溢出異常。
1.7 總結(jié)
對于 JVM 運行時內(nèi)存布局抛腕,我們需要始終記住一點:上面介紹的這 5 塊內(nèi)容都是在 Java 虛擬機規(guī)范中定義的規(guī)則芋绸,這些規(guī)則只是描述了各個區(qū)域是負責做什么事情、存儲什么樣的數(shù)據(jù)担敌、如何處理異常摔敛、是否允許線程間共享等。千萬不要將它們理解為虛擬機的“具體實現(xiàn)”全封,虛擬機的具體實現(xiàn)有很多马昙,比如 Sun 公司的 HotSpot、JRocket刹悴、IBM J9行楞、以及我們非常熟悉的 Android Dalvik 和 ART 等。這些具體實現(xiàn)在符合上面 5 種運行時數(shù)據(jù)區(qū)的前提下土匀,又各自有不同的實現(xiàn)方式子房。
最后我們借助一張圖來概括一下本課時所介紹的內(nèi)容:
總結(jié)來說,JVM 的運行時內(nèi)存結(jié)構(gòu)中一共有兩個“椇阆鳎”和一個“堆”池颈,分別是:Java 虛擬機棧和本地方法棧,以及“GC堆”和方法區(qū)钓丰。除此之外還有一個程序計數(shù)器躯砰,但是我們開發(fā)者幾乎不會用到這一部分,所以并不是重點學(xué)習(xí)內(nèi)容携丁。 JVM 內(nèi)存中只有堆和方法區(qū)是線程共享的數(shù)據(jù)區(qū)域琢歇,其它區(qū)域都是線程私有的兰怠。并且程序計數(shù)器是唯一一個在 Java 虛擬機規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。