今天閑來無事來,看一下java中的內存模型和垃圾回收機制的原理,關于這個方面的知識,網上已經有很多現成的資料可以供我們參考,但是知識還是比較雜的,在這部分知識點中有一本書不得不推薦:《深入理解Java虛擬機》,現在已經是第二版了。這本書就從頭開始詳細介紹了Java整個虛擬機的模型以及Java的類文件結構,加載機制等。這里大部分的知識點都是可以在這本書中找到的,當然我是主要還是借鑒這本書中的很多內容的。下面就不多說了,進入主題吧。
首先來看一下Java中的內存模型圖:
第一、程序計數器(PC)
程序計數器(PRogram Counter Register)是一塊較小的內存空間,它可以看做當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時就是通過改變這個計數器的值來取下一條需要執行的字節碼指令,分支、跳轉、循環、異常處理、線程恢復等基礎功能都需要這個計數器來完成
注:程序計數器是線程私有的,每條線程都會有一個獨立的程序計數器
第二、Java棧(虛擬機棧)
Java棧就是Java中的方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀(關于棧幀后面介紹),這個棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,每個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
注:Java棧也是線程私有的。
異常可能性:對于棧有兩種異常情況:如果線程請求的棧深度大于棧所允許的深度,將拋出StackOverflowError異常,如果虛擬機棧可以動態拓展,在拓展的時無法申請到足夠的內存,將會拋出OutOfMemoryError異常
棧幀的概念:
棧幀用于支持虛擬機進行方法調用和執行的數據結構。
1) 局部變量表
局部變量表(Local Variable Table)是一組 變量值存儲空間,用于存放 方法參數和方法內部定義的局部變量.局部變量表的容量以變量槽(Variable Slot,下稱Slot)為最小單位. 一個Slot可以存放一個32位以內的數據類型,Java中占用32位以內的數據類型有boolean、byte、char、short、int、float、reference[3]和returnAddress 8種類型,對于 64位的數據類型,虛擬機會以高位對齊的方式為其 分配兩個連續的Slot空間(long double).
2) 操作數棧
操作數棧(Operand Stack)也常稱為操作棧,它是一個后入先出(Last In First Out,LIFO)棧,當一個方法剛剛執行的時候,這個方法的操作數棧是空的,在方法的執行過程中,會有各種字節碼指令往操作數棧中寫入和提取內容,也就是出棧/入棧操作,例如,在做算術運算的時候通過操作數棧來進行的,又或者在調用其他方法的時候是通過操作數棧來進行參數傳遞的。
舉個例子:整數假發的字節碼指令iadd在運行的時候操作數棧中最接近棧頂的兩個元素已經存入了兩個int類型的數值,當執行這個指令時,會將這兩個int值出棧并相加,然后將相加的結果入棧。
3) 方法返回地址
一個方法開始執行后,只有 兩種方式可以退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱為調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱為 正常完成出口(Normal Method Invocation Completion)。另外一種退出方式是,在方法執行過程中 遇到了異常,并且這個異常沒有在方法體內得到處理,無論是Java虛擬機內部產生的異常,還是代碼中使用athrow字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,這種退出方法的方式稱為 異常完成出口(Abrupt Method Invocation Completion)。 一個方法使用異常完成出口的方式退出,是不會給它的上層調用者產生任何返回值的
4) 附加信息
虛擬機規范允許具體的虛擬機實現增加一些規范里沒有描述的信息到棧幀之中,例如與調試相關的信息,這部分信息完全取決于具體的虛擬機實現,這里不再詳述。在實際開發中,一般會把動態連接、方法返回地址與其他附加信息全部歸為一類,稱為棧幀信息。
第三、本地方法棧
本地方法棧與Java棧所發揮的作用是非常相似的,它們之間的區別不過是Java棧執行Java方法,本地方法棧執行的是本地方法。
注:本地方法棧也是線程私有的
異??赡苄裕汉蚃ava棧一樣,可能拋出StackOverflowError和OutOfMemeryError異常
第四、Java堆
對于大多數應用來說,Java堆是Java虛擬機所管理的內存中最大的一塊,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存,當然我們后面說到的垃圾回收器的內容的時候,其實Java堆就是垃圾回收器管理的主要區域。
注:堆是線程共享的
異??赡苄裕喝绻阎袥]有內存完成實例分配,并且堆也無法再拓展時,將會拋出OutOfMemeryError異常
第五、方法區
方法區它用于存儲已被虛擬機加載的類信息、常量、靜態常量、即時編譯器編譯后的代碼等數據。
注:方法區和堆一樣是線程共享的
異??赡苄裕寒敺椒▍^無法滿足內存分配需求時,將拋出OutOfMemeryError異常
1)運行時常量池
運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載器后進入方法區的運行時異常常量池存放。
上面就介紹了Java的內存的幾個模塊的相關概念,其實我們需要知道這些知識,最主要的目的是不要在項目中寫那些OOM的代碼,因為我們如果知道了內存模型之后,即使代碼中出現了OOM的問題,我們可以定位到哪里出了問題。
下面也來看一下上面說到的幾個內存模塊導致的內存溢出異常問題:
(這個也是面試的時候經常會被問到:比如叫你寫一段讓堆內存溢出的代碼,或者是問你如果如果修改堆大小)
第一、堆溢出
[java] view plain copy
我們上面看到堆主要是存放對象的,所以我們如果想讓堆出現OOM的話,可以開一個死循環,然后產生新的對象就可以了。然后在將堆的大小調小點。
加上JVM參數
-verbose:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError,
就能很快報出OOM:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
第二、棧溢出
[java] view plain copy
我們知道棧中存放的方法執行的過程中需要的空間,所以我們可以下一個循環遞歸,這樣方法棧就會出現OOM的異常了。
設置JVM參數:-Xss128k,報出異常:
Exception in thread "main" java.lang.StackOverflowError
打印出Stack length:1007,這里可以看出,在我的機器上128k的棧容量能承載深度為1007的方法調用。當然報這樣的錯很少見,一般只會出現無限循環的遞歸中,另外,線程太多也會占滿棧區域:
[java] view plain copy
這個棧的溢出,就是我們上面說到棧的時候的兩種異常情況。
報出異常:Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread
第三、方法區溢出
[java] view plain copy
我們知道方法區是存放一些類的信息等,所以我們可以使用類加載無限循環加載class,這樣就會出現方法區的OOM異常。
手動將棧的大小調小點
加上JVM參數:-XX:PermSize=10M -XX:MaxPermSize=10M,運行后會報如下異常:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
第四、常量池溢出
[java] view plain copy
我們知道常量池中存放的是運行過程中的常量,同時我們知道String類型的intern方法是將字符串的值放到常量池中的。所以上面弄可以開一個死循環將字符串的值都放到常量池中,這樣常量池就會出現OOM異常了。因為常量池本身就是方法區的一部分,所以我們也可以手動的調節一下棧的大小。
總結:上面只是從宏觀的角度介紹了一下內存模型,具體關于內存中每個區域的詳細信息,可以閱讀開頭說到的那本很不錯的書籍。當然我們在學習Java的時候可以分為四大模塊:Java的Api、Java虛擬機(內存模型和垃圾回收器)、Java的Class文件、設計模式,關于Api的知識我們在工作的過程中用到的比較多,而且這部分內容完全是靠使用度,你用多了,api你自然就知道了。Java虛擬機和Java的Class文件的相關知識在工作中可能不一定能用到,但是這方面的知識能夠讓你更了解Java的整個體系結構。至于設計模式這個就是修煉的過程,也是最難的過程。得慢慢的體會其的強大之處。
新聞熱點
疑難解答