Java內存區域剖析 —— 定位OutOfMemory異常之前的必修課

帶着問題閱讀

  • Java中都有哪些數據需要放進內存?
  • 如果你是JVM的設計者,你會怎樣將內存按功能進行劃分?
  • HotSpot是怎麼劃分內存的?


導語

上一講帶着大家踏入了Java虛擬機的大門,從這一講開始,進入專題的第一個版塊——Java虛擬機的自動內存管理機制。

說起內存,大家很容易就想到了內存溢出,的確,對於Java工程師來說,誰的一生不會經歷OutOfMemory呢,要麼是Heap Space家起火,要麼是Stack家淹水了,要麼就是PermGen被打劫了。在學習如何定位這些異常發生的原因並提出解決方案之前,我們必須瞭解一下,Java虛擬機是如何劃分自己的內存區域的。

本文是Effective Java專欄Java虛擬機專題的第二講,如果你覺得看完之後對你有所幫助,歡迎訂閱本專欄,也歡迎您將本專欄分享給你身邊的工程師同學。

在學習本節課程之前,建議您先了解一下以下知識點:


Java內存區域

一個Java進程啓動後,會被劃分一塊類似於疆土的內存區域,虛擬機將這塊大的內存,按照所存儲的數據類型,劃分爲不同的區域進行管理,Java虛擬機的運行時數據區,可以用下面這張圖來表示:



下面就來對這張內存區域模型圖作詳細的講解。


程序計數器

程序計數器在虛擬機內存中是一塊很小的區域,這個計數器記錄了當前線程執行到了哪一行的字節碼指令。每條線程都擁有一個獨立的程序計數器,有了這個計數器,才能在不停的線程切換中,讓線程記得下一條要執行的指令

程序計數器是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemory異常的區域。


Java虛擬機棧

和程序計數器一樣,Java虛擬機棧也是線程私有的。虛擬機棧主要存儲的內容是方法執行時的局部變量表,包括各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(指向一個對象地址的指針)和returnAddress類型(指向一條字節碼指令的地址)。

其中,64位長度的long和double會佔用2個局部變量空間,其餘數據類型只佔用1個。局部變量表所需的內存空間在編譯器就分配完成,也就是說,進入一個方法時,這個方法需要分配多大的局部變量空間是完全確定的

可能這樣講大家還有點模糊,這裏上個圖:


每個線程都擁有一個虛擬機棧,線程中每個方法執行時都會在棧中創建一個棧幀,棧幀中就包含了上面所說的數據,方法每遞歸一次,就新建一個棧幀,依此類推。後面講到虛擬機字節碼執行引擎時,會對這張圖作更詳細的介紹。

在Java虛擬機規範中,這個區域有兩種異常情況:

  • 如果線程運行時的棧幀總得大小超過虛擬機限制的大小,會拋出StackOverflow異常,這一點通常發生在遞歸運行時;
  • 如果虛擬機棧設置爲可以動態擴展,並且在擴展時無法申請到足夠內存,則會拋出OutOfMemory異常。


本地方法棧

本地方法棧發揮的作用和虛擬機棧是十分相似的,主要都是存儲着方法執行的局部變量的信息,不同的是虛擬機棧是爲執行Java方法(也就是字節碼)服務,而本地方法棧是爲虛擬機使用到的本地Native方法服務。

基於兩者的相似性,HotSpot虛擬機直接將兩者合二爲一。

和虛擬機棧一樣,本地方法棧也有StackOverflow和OutOfMemory異常。


Java堆

Java堆是Java虛擬機內存中最大的一塊,是被所有線程共享的一塊內存區域。Java堆負責存放Java對象實例,因此Java堆也是垃圾收集的主要區域。

Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可。

可以通過-Xmx和-Xms參數來控制堆的大小,當堆沒有內存完成實例分配,且無法繼續擴展時,就會拋出OutOfMemory異常。


方法區

前面提到了虛擬機棧是用來管理Java方法執行的內存信息的,因此,很明顯,這裏的方法區存儲的並不是方法執行的信息。

方法區用於存儲已被虛擬機加載的類信息、常量、靜態變量、JIT編譯器編譯後的代碼等數據,也是各個線程共享的內存區域。


直接內存

直接內存並不是虛擬機運行時數據區的一部分,但是Java可以對這部分的內存進行使用,比如JDK 1.4新加入的NIO,它可以通過Native函數庫直接在機器內存中獲取內存,然後通過存儲在Java堆中的對象,作爲這塊內存的引用進行操作,避免了在Java堆和Native之間來回複製數據,提高了操作的性能。

直接內存的大小只受到本機總內存的限制,因此,如果直接內存使用過多,超過了機器的物理內存限制,就會導致OutOfMemory異常。


總結

這一講,主要給大家介紹了Java虛擬機是如何劃分內存以及各個內存區域的功能和職責。瞭解了這些之後,我們就能夠理解爲什麼會發生各種內存異常的異常了,在下一講,我將在自己的機器上,演示各種內存異常是如何發生的。


課後思考

如果要你在自己的機器上模擬Java堆的內存溢出,你會怎麼做?歡迎在評論區寫下你的想法,O(∩_∩)O謝謝。


上一講課後思考題的答案

上一講的問題是——“我們經常看到有人說他掌握J2SE、J2ME、J2EE,也看過有人說他很懂Java SE、Java ME、Java EE,那麼到底應該叫是J2XX還是Java XX呢?”

這個問題涉及到Java發展的歷史,1998年12月4日,JDK1.2發佈,Sun將Java技術體系拆分爲3個方向——J2SE、J2ME、J2EE;而到了2006年12月11日,JDK1.6發佈,Sun終結了J2XX的命名方式,啓用Java SE、Java ME、Java EE的命名方式。因此,在面試或者平時寫作的時候,建議大家採用Java XX的方式吧,就像Java的首字母要大寫一樣。


參考資料


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章