帶着問題閱讀
- 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的首字母要大寫一樣。
參考資料
- 《深入理解Java虛擬機》 周志明
- java-memory-model
- memorymanagement-whitepaper