java 內(nèi)存區(qū)域
要進(jìn)行 java 虛擬機的深入學(xué)習(xí),首先要了解的是 java 的內(nèi)存劃分诱贿。大部分程序員一開始接觸 java 娃肿,對于內(nèi)存的劃分是印象是堆內(nèi)存和棧內(nèi)存,而這僅僅適合于入門的學(xué)習(xí)珠十,實際上 java 的內(nèi)存劃分料扰,遠(yuǎn)遠(yuǎn)復(fù)雜的多。
-
java 內(nèi)存劃分
java 虛擬機在執(zhí)行 java 程序時焙蹭,會把它所管理的內(nèi)存區(qū)域劃分為若干個不同的數(shù)據(jù)區(qū)域晒杈。這些區(qū)域各有用處、創(chuàng)建及銷毀時間孔厉。有的區(qū)域是隨虛擬機的啟動而啟動拯钻,屬于線程共有的,而有的是隨用戶線程的啟動而啟動烟馅,屬于線程私有的说庭。下圖給出了 java 虛擬機對于內(nèi)存的劃分關(guān)系:
-
各部分內(nèi)存詳解 :
-
程序計數(shù)器(Program Counter Register)
我們知道 Java 虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,在任何一個確定的時間郑趁,一個處理器(對于多核處理器來說就是一個內(nèi)核)都只會執(zhí)行一條線程的指令刊驴。那么對于每一個線程來說,我們就需要有一個記錄器,來保證線程切換后能恢復(fù)到正確的執(zhí)行位置寡润,而且這個記錄器必須是每個線程獨立的捆憎,這個記錄器就是我們的程序記錄器。
??程序記錄器是一塊較小的內(nèi)存梭纹,它可以看做當(dāng)前線程所執(zhí)行的字節(jié)碼的行號計數(shù)器躲惰。在虛擬機的實現(xiàn)原理中,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令变抽,分支础拨、循環(huán)氮块、跳轉(zhuǎn)、異常處理诡宗、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成滔蝉。
?? 如果線程正在執(zhí)行的是一個 java 方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址塔沃;如果執(zhí)行的是一個 Native 方法蝠引,這個計數(shù)器值則為空(Undefined)。此內(nèi)存區(qū)域是唯一一個在 Java 虛擬機中沒有定義 OutOfMemoryError 的區(qū)域蛀柴。 -
Java 虛擬機棧(Java Virtual Machine Stacks)
與程序計數(shù)器一樣螃概,虛擬機棧也是線程私有的內(nèi)存,其生命周期跟線程相同鸽疾。
??虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:學(xué)過數(shù)據(jù)結(jié)構(gòu)的朋友都應(yīng)該知道吊洼,每個方法在執(zhí)行時都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧肮韧、動態(tài)鏈接融蹂、方法出口等信息旺订,每一個方法的執(zhí)行過程弄企,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。
??局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型区拳、對象引用和 returnAddress 類型拘领,其所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個方法時樱调,這個方法需要在幀中分配多大的局部變量空間是完全確定的约素,在方法運行期間不會改變局部變量表的大小。
??在 Java 虛擬機規(guī)范中笆凌,對這個區(qū)域規(guī)定了兩種異常情況:如果線程請求的棧深度大于虛擬機所允許的深入圣猎,將拋出 StackOverflowError 異常;如果虛擬機椘蚨可以動態(tài) 擴展送悔,而擴展時無法申請到足夠的內(nèi)存,將會拋出 OutOfMemoryError 異常爪模。 -
本地方法棧(Native Method Stack)
本地方法棧與虛擬機棧發(fā)揮的作用是非常相似的欠啤,它們之間的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行 Java 方法服務(wù),而本地方法棧則為虛擬機使用到的 Native 方法服務(wù)屋灌。
??與虛擬機棧一樣洁段,本地方法區(qū)棧也會拋出 StackOverflowError 和 OutOfMemoryError 異常。 -
Java 堆(Java Heap)
就像大多數(shù)朋友了解的一樣共郭,Java 堆唯一的目的是存放對象實例祠丝,幾乎所有的對象實例都在這里分配內(nèi)存疾呻。其也基本是 Java 虛擬機管理的內(nèi)存中最大的一塊,它是被所有線程共享的一塊內(nèi)存區(qū)域写半,在虛擬機啟動時創(chuàng)建罐韩。
??Java 堆是垃圾回收器管理的主要區(qū)域,因此很多時候也被稱作“GC 堆”污朽。從內(nèi)存回收的角度看散吵,由于現(xiàn)在收集器基本都采用分代收集算法,所以 Java 堆中還可以細(xì)分為:新生代和老生代蟆肆,再細(xì)致一點的有 Eden 空間矾睦,F(xiàn)rom Survivor 空間,To Survivor 空間等炎功;從內(nèi)存分配的角度看枚冗,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。無論如何劃分蛇损,其目的都只是為了更好的回收內(nèi)存赁温,或者更快地分配內(nèi)存,而具體細(xì)節(jié)的回收跟分配細(xì)節(jié)淤齐,我們將在后面的文章討論股囊。
?? 根據(jù) Java 虛擬機規(guī)范的規(guī)定,Java 堆可以處于物理上不連續(xù)的內(nèi)存空間中更啄,只要邏輯上是連續(xù)的即可稚疹。在實現(xiàn)時,既可以實現(xiàn)為固定大小的祭务,也可以實現(xiàn)為可擴展的内狗,目前主流的虛擬機都是按照可擴展來實現(xiàn)的(通過 -Xms 和 -Xmx 控制)。如果在堆中沒有內(nèi)存完成實例分配义锥,并且堆也無法再擴展時柳沙,將會拋出 OutOfMemoryError 異常。 -
方法區(qū)(Method Area)
方法區(qū)與 Java 堆一樣拌倍,是各個線程共享的內(nèi)存區(qū)域赂鲤,它用于存儲已被虛擬機加載的類信息、常量贰拿、靜態(tài)變量蛤袒、即時編譯器編譯后的代碼等數(shù)據(jù)。雖然 Java 虛擬機把方法區(qū)描述為堆的一個邏輯部分膨更,但是它卻有一個別名叫做 Non-Heap(非堆)妙真,目的是與 Java 堆區(qū)分開來。
??Java 虛擬機規(guī)范對方法區(qū)的限制非常寬松荚守,除了和 Java 堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴展外珍德,還可以選擇不實現(xiàn)垃圾收集练般。這區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載,一般來說锈候,這個區(qū)域的回收成績比較難以令人滿意薄料,尤其是類型的卸載,條件相當(dāng)苛刻泵琳,但這一部分的回收確實是必要的摄职,具體的細(xì)節(jié)我們應(yīng)該后面的文章有所介紹。
??根據(jù) Java 虛擬機規(guī)范的規(guī)定获列,當(dāng)方法區(qū)無法滿足內(nèi)存 分配需求時谷市,將拋出 OutOfMemoryError 異常。 -
運行時常量池(Runtime Constant Pool)
運行時常量池實際上是方法區(qū)的一部分击孩。Class 文件中除了有類的版本迫悠、字段、方法巩梢、接口等描述信息外创泄,還要一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用括蝠,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運行時常量池存放鞠抑。
??Java 虛擬機對 Class 文件每一部分的格式都有嚴(yán)格規(guī)定,每一個字節(jié)用于存儲哪種數(shù)據(jù)都必須符合規(guī)范上的要求才會被虛擬機認(rèn)可又跛、裝載和執(zhí)行碍拆,但對于運行時常量池若治,Java 虛擬機規(guī)范并沒有做任何細(xì)節(jié)的要求慨蓝。不過一般來說,除了保存 Class 文件中描述的符號引用外端幼,還會把翻譯出來的直接引用也存儲在運行時常量池中礼烈。
??既然運行時常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制婆跑,當(dāng)常量池?zé)o法再申請到內(nèi)存時會拋出 OutOfMemoryError 異常此熬。 -
直接內(nèi)存(Direct Memory)
直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是 Java 規(guī)范中定義的內(nèi)存區(qū)域滑进。但是這部分內(nèi)存也被頻繁的使用犀忱,而且也可能導(dǎo)致 OutOfMemoryError 異常,所以我們放在這里一起講解扶关。
??在 JDK 1.4 中新加入了 NIO(New Input/Output) 類阴汇,引入了一種基于通道與緩沖區(qū)的 I/O 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存节槐,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作搀庶。
??顯然拐纱,本機直接內(nèi)存的分配不會受到 Java 堆大小的限制,但是哥倔,既然是內(nèi)存秸架,肯定還是會受到本機總內(nèi)存大小及處理器尋址空間的限制。服務(wù)器管理員在配置虛擬機參數(shù)時咆蒿,會根據(jù)實際內(nèi)存設(shè)置 -Xmx 等參數(shù)信息东抹,但經(jīng)常忽略直接內(nèi)存,使得各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制沃测,從而導(dǎo)致動態(tài)擴展時出現(xiàn) OutOfMemoryError 異常府阀。
-
-
OutOfMemoryError 異常測試
在 Java 虛擬機規(guī)范中,除了程序計數(shù)器之外芽突,另外幾個內(nèi)存區(qū)域都會出現(xiàn) OutOfMemoryError 異常试浙,現(xiàn)在我們通過幾個代碼段來測試一下這些異常發(fā)生的場景。這些測試的目的有兩個:1寞蚌、通過程序驗證 Java 虛擬機運行時各個內(nèi)存區(qū)域存儲的內(nèi)容田巴;2、希望讀者們在以后的編程過程中遇到內(nèi)存溢出異常時挟秤,能夠根據(jù)異常信息準(zhǔn)確快速地判斷哪個區(qū)域的內(nèi)存溢出壹哺,并作出處理。
??接下來的代碼段在執(zhí)行的時候艘刚,都需要在執(zhí)行設(shè)置虛擬機參數(shù)管宵,這些參數(shù)在代碼前注釋都會有,而且這些參數(shù)對實驗結(jié)果會有直接的影響攀甚,如果有讀者也想實驗試一下這些內(nèi)存溢出情況的話箩朴,請不要忽略這些參數(shù)。如果實在命令行執(zhí)行程序秋度,那么直接在 Java 命令之后帶上參數(shù)就可以了炸庞,如果是在 IDE 中執(zhí)行,那么就在虛擬機選項中加上荚斯,下圖為筆者在 IDEA 編輯器中的示例:
-
Java 堆溢出
Java 堆用于存儲對象示例埠居,只要不斷地創(chuàng)建對象,并且保證 GC Roots 到對象之間有可達(dá)路線來避免垃圾回收機制清理這些對象事期,那么在對象數(shù)量達(dá)到最大堆的容量限制后就會產(chǎn)生內(nèi)存溢出異常滥壕。
運行結(jié)果:/** * Created by LinSY on 2017/9/23. * VM args: -Xms20M, -Xmx20m */ public class HeapOOM { static class OOMObject{ } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>() ; while (true){ list.add(new OOMObject()) ; } } }
??
??Java 堆的內(nèi)出溢出異常時實際應(yīng)用中最常見到的情況,當(dāng)出現(xiàn) Java 堆溢出異常時兽泣,異常堆信息 "java.lang.OutOfMemoryError" 會跟著進(jìn)一步提示 "Java heap space" 绎橘。
??解決這個區(qū)域的異常,一般是通過內(nèi)存映像分析工具進(jìn)行分析撞叨,具體的分析方法我們看看后面有沒有時間再專門整理出一片文章來給大家介紹下金踪,這里先簡單說一下思路:1浊洞、我們先分析內(nèi)存中的對象是否必要存在,也就是判斷是內(nèi)存泄漏還是內(nèi)存溢出胡岔。2法希、如果是內(nèi)存溢出,我們可以通過工具進(jìn)一步查看泄漏對象到 GC Roots 的引用鏈靶瘸,找尋到垃圾回收機制無法回收這些對象的原因苫亦,然后就可以比較準(zhǔn)確地定位泄漏代碼的位置。3怨咪、如果不存在內(nèi)存泄漏屋剑,也就是說內(nèi)存中的對象是必要存在的,那么就應(yīng)該檢查虛擬機的堆參數(shù)诗眨,與機器的物理內(nèi)存比較看是否還可以調(diào)大唉匾,從代碼上檢查是否某些對象的聲明周期過長,嘗試減少程序運行期的內(nèi)存消耗等路徑去解決問題匠楚。 -
虛擬機棧和本地方法棧溢出
筆記目前環(huán)境使用的是 HopSpot 虛擬機巍膘,它是不區(qū)分虛擬機棧和本地方法棧的,因此對于 HopSopt 來說芋簿,使用 -Xoss 參數(shù)設(shè)置本法方法棧的方法是無效的峡懈,棧容量只能由 -Xss 參數(shù)設(shè)定。
??上文提到的与斤,虛擬機棧和本地方法棧規(guī)定了兩種異常 OutOfMemoryError 異常和 StackOverflowError 異常肪康,具體出現(xiàn)情況可以去上文查看。那么我們只需要定義虛擬機參數(shù) -Xss 減少棧內(nèi)存撩穿,再定義無限遞歸調(diào)用某一方法磷支,應(yīng)該就能獲得 StackOverflowError 異常,異常出現(xiàn)時輸出的堆棧深度相應(yīng)減少:
實驗結(jié)果:/** * VM Args: -Xss256k * Created by LinSY on 2017/9/23. */ public class StackOverflowError { private int stackDepth = 1 ; public void stackLeak(){ stackDepth++ ; stackLeak() ; } public static void main(String[] args) throws Throwable{ StackOverflowError stackOverflowError = new StackOverflowError() ; try { stackOverflowError.stackLeak(); } catch (Throwable e) { System.out.println("stack depth : " + stackOverflowError.stackDepth); throw e ; } } }
??
??我們知道,操作系統(tǒng)分配給每個進(jìn)程的內(nèi)存是有限的(32 位 Windows 限制為 2GB)冗锁,虛擬機提供了參數(shù)來控制控制 Java 堆和方法區(qū)這兩部分內(nèi)存的最大值齐唆,剩余的內(nèi)容為 2GB(操作系統(tǒng)限制) 減去 Xmx(最大堆容量),再減去 MaxPermSize(最大方法區(qū)容量)冻河,程序計數(shù)器消耗的內(nèi)存很小可以忽略,如果虛擬機本身消耗的內(nèi)存我們忽略了茉帅,那么剩下的內(nèi)存就有本地方法棧和虛擬機棧瓜分了叨叙,每個線程分配的棧容量越大,可以建立的線程數(shù)量自然就越小堪澎,建立線程時就越容易把剩下的內(nèi)存耗盡擂错。所以我們可以通過建立不斷建立線程的方式來產(chǎn)生 OutOfMemoryError 異常:
特別提醒:如果讀者想嘗試上面的代碼,記得先保存當(dāng)前的工作樱蛤。因為在 Windows 虛擬機上钮呀,Java 的線程時映射到操作系統(tǒng)的內(nèi)核線程上的剑鞍,因此上面這段代碼又較大的風(fēng)險,有可能導(dǎo)致系統(tǒng)假死爽醋。/** * VM Args: -Xss2M(也可以設(shè)置再大一點) * Created by LinSY on 2017/9/26. */ public class StackOOM { private void dontStop(){ while (true){ } } public void stackLeakByCreatThread(){ while (true){ Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }) ; thread.start(); } } public static void main(String[] args) { StackOOM stackOOM = new StackOOM() ; stackOOM.stackLeakByCreatThread(); } }
??出現(xiàn) StackOverflowError 異常時蚁署,我們有錯誤堆棧可以閱讀蚂四,相對來說光戈,比較容易找到問題所在。但是遂赠,如果是建立過多線程導(dǎo)致的內(nèi)存溢出久妆,在不能減少線程數(shù)或者更換 64 位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程跷睦,這是一種解決多線程內(nèi)存溢出的方法筷弦。 -
方法區(qū)和運行時常量池溢出
由于運行時常量池是方法區(qū)的一部分,因此這兩個區(qū)域的溢出就放在一起測試抑诸。
??String.intern() 方法是一個 Native 方法奸笤,它的作用是:如果字符串常量池中包含了一個等于此字符串的對象,則返回字符串的 String 對象哼鬓;否則將此字符串添加到常量池中监右,并返回此 String 對象的引用。因此异希,我們可以在設(shè)置方法區(qū)的最大容量之后健盒,通過死循環(huán)來執(zhí)行該方法,從而得到運行時常量池的 OOM 異常:
運行結(jié)果:import java.util.ArrayList; import java.util.List; /** * VM Args : -XX:PermSize=2M, -XX:MaxPermSize=2M * Created by LinSY on 2017/9/26. */ public class RuntimeConstantPoolOOM { public static void main(String[] args) { List<String> list = new ArrayList<>() ; int i = 0 ; while (true){ list.add(String.valueOf(i++).intern()) ; } } }
??
??這里需要注意的是這個實驗結(jié)果只有在JDK1.6及以下才能得到称簿,在 JDK1.7 或以上該程序代碼的循環(huán)會一直下去扣癣,這是受到 JDK1.7 逐步去除“永久帶”的影響。
??方法區(qū)用于存放 Class 的相關(guān)信息憨降,如類名父虑、訪問修飾符,常量池授药、字段描述士嚎、方法描述等。對于這類區(qū)域的測試悔叽,基本的思路是在運行時去產(chǎn)生大量的類去填滿方法區(qū)莱衩,直到溢出。雖然直接使用 Java SE API 也可以產(chǎn)生動態(tài)類娇澎,但在本次實驗中操作起來比較麻煩笨蚁,在下面的代碼中,筆者借助 CGLib 直接操作字節(jié)碼運行時產(chǎn)生了大量的動態(tài)類。
方法區(qū)溢出也是一種常見的內(nèi)存溢出括细,一個類要被垃圾收集器回收掉伪很,判定條件是比較苛刻的。在經(jīng)常生成大量 Class 的應(yīng)用中奋单,需要特別注意類的回收狀況锉试。import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M * Created by LinSY on 2017/9/26. */ public class JavaMethodAreaOOM { public static void main(String[] args){ while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } } static class OOMObject { } }
-
本機直接內(nèi)存溢出
本機直接內(nèi)存容量可以通過 -XX:MaxDirectMemorySize 指定,如果不指定辱匿,則默認(rèn)與 Java 堆一樣键痛。下面的代碼越過了 DirectByteBuffer 類,直接通過反射獲取 UnSafe 實例進(jìn)行內(nèi)存分配匾七。因為雖然使用 DirectByteBuffer 分配內(nèi)存也會拋出內(nèi)存異常絮短,但它拋出異常時并沒有真正向操作系統(tǒng)申請分配內(nèi)存,而是通過計算得知內(nèi)存無法分配昨忆,于是手動拋出異常丁频,真正分配內(nèi)存的方法是 unsafe.allocateMemory() 方法。
實驗結(jié)果:import org.omg.CORBA.TRANSACTION_MODE; import org.omg.CORBA.UNSUPPORTED_POLICY; import sun.misc.Unsafe; import java.lang.reflect.Field; /** * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M * Created by LinSY on 2017/9/26. */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024 ; public static void main(String[] args) throws IllegalAccessException { Field unsafeField = Unsafe.class.getDeclaredFields()[0] ; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true){ unsafe.allocateMemory(_1MB) ; } } }
??
??本地直接內(nèi)存溢出并沒有什么其他多于的提示信息邑贴,其主要標(biāo)志是在 Heap Dump 文件中不會看見明顯的異常席里,如果讀者發(fā)現(xiàn) OOM 之后的 Dump 文件很小,而程序中又直接或間接的使用了 NIO拢驾,那就可以考慮檢查一下是不是這方面的原因奖磁。
-
-
小結(jié)
在這篇文章中掠哥,我們明白了虛擬機中的內(nèi)存是如何劃分的誓酒,哪部分區(qū)域、什么樣的代碼和操作可能導(dǎo)致溢出異常并用代碼實踐了這些異常出現(xiàn)的結(jié)果帘皿。雖然 Java 有垃圾回收機制稠腊,但內(nèi)存溢出離我們并不遙遠(yuǎn)躁染,我們在實際應(yīng)用中仍需十分小心并掌握異常出現(xiàn)后的解決辦法。而下一篇文章架忌,我們將講解一下 Java 垃圾回收機制為了避免內(nèi)存溢出異常都做出了哪些努力吞彤。