一、概述
我們知道java代碼先編譯為.class文件,然后再將.class文件交由jvm執行。在程序運行的這一過程中,jvm會將其管理的內存空間划分為不同的區域,這些區域各有各的用途,我們將其分為五類:
- 方法區
- 堆
- 虛擬機棧
- 本地方法棧
- 程序計數器
其中方法區和堆是線程共享的,隨jvm啟動和停止而創建和銷毀;
而虛擬機棧、本地方法棧和程序計數器則是線程私有的,隨線程的創建和結束而創建和銷毀。
二、線程隔離數據區
包括程序計數器,虛擬機棧,本地方法棧三部分,是線程私有的數據區。
1.程序計數器
程序計數器用於記錄當前線程執行的字節碼指令的地址。
我們知道cup實現多線程操作是根據每個線程分配是時間片來決定處的,每一個時間片cup都只處理搶到那個時間片的線程,因此很可能出現線程1指令執行到一半,結果下一個時間片又去處理另一個線程了。
為了能夠在線程切換后依然能恢復到正確的指令位置,每一個線程都需要一個獨立的計數器去記錄正在執行的字節碼指令地址,我們可以簡單的理解為一個記錄執行到的指令行數的一個指示器。
如果指向的是java方法,計數器記錄執行的字節碼的地址,如果是非java代碼的Native方法,這計數器為空。
計數器是唯一一個沒有規定OutOfMemoryError
的區域。
2.虛擬機棧
虛擬機棧是描述java方法執行的一個內存模型。
每個方法執行的時候會常見一個棧幀,棧幀中會儲存局部變量表。操作數棧、動態鏈接、方法出口信息等。比如方法的局部變量會插入局部遍歷表,對局部變量的運算和傳遞則通過數棧等等。
每個方法從調用到完成就是一個棧幀在虛擬機棧中入棧到出棧的一個過程。我們使用遞歸時提到的棧就是虛擬機棧。
虛擬機棧規定有兩種異常:StackOverflowError
和 OutOfMemoryError
我們知道方法調用實際就是棧幀入棧,如果棧的深度超過規定,就會拋出StackOverflowError
異常;
棧的大小可以規定也可以動態擴展,如果棧擴展大小時申請不到足夠的內存,就會拋出OutOfMemoryError
異常.
3.本地方法棧
本地方法棧是描述j非java的方法執行的一個內存模型。
它與虛擬機棧功能一樣,但是不同的是本地方法棧用於存放實現方法非java代碼的方法。當一個java方法要調用的的時候,會將java棧幀入虛擬機棧,而當非java方法要調用的時候就會入本地方法棧。
實際上兩種棧之間往往會互相調用對方的方法,比如java方法A調用了java方法B,java方法B調用了C++方法C,這個C++方法又調用了java方法D,描述一下過程就會是:
A =》虛擬機棧,B =》虛擬機棧,C =》本地方法棧,D =》虛擬機棧
二、線程共享數據區
1.堆
堆用於存放對象實例、數組和字符串常量池
堆用於存放類的實例對象、數組和字符串常量池、另外,由於實例對象存儲於此區域,所以也是垃圾收集器管理的主要區域,故又稱GC堆。
java對可以是固定大小,也可以是動態大小,如果堆中沒有內存分配給新的實例對象的時候,就會拋出OutOfMemoryError
異常。
1.2字符串常量池
這里稍微提一下字符串常量池,正由於字符串常量池的存在,當創建字符串常量時,首先檢查字符串常量池是否存在該字符串,存在該字符串,返回引用實例,不存在,實例化該字符串並放入池中。
這也是為什么字符串明明是對象卻可以直接使用 == 比較,因為同樣的字符指向的都是常量池里同一個字符串對象。
1.3 垃圾回收策略
另外值得一提的是,堆往往和垃圾回收問題一起出現,所以這里也簡單的介紹一下內存分配和回收的策略:
由於jvm內存回收機制采用了分代收集算法,所以java堆中還分為新生代和老年代,新生代中又分為占大部分控件的eden
區域和占較小空間的survivorSpace0
和survivorSpace1
。
根據分代收集算法,對在給新實例進行內存分配時一般遵循以下原則:
-
對象優先分配給
eden
區域。當eden區域沒有足夠弓箭時,發起一次GC。當垃圾回收時,根據復制算法:eden
和一個survivorSpace
中還存活的對象會復制到另一個survivorSpace
中,然后清理原先的空間 -
需要大量連續內存空間的大對象直接進入老年代。比如巨長的數組或者字符串,還有非常高的樹之類的。
-
長期存活的對象會進入老年代。對象在新生代活過一定次數GC后會移入老年代。
當然,不同的垃圾收集器和不同的垃圾收集算法適應不同的程序運行情況,實際的內存回收機制要復雜的多,這里以后會在新隨筆里另外再展開,這里就不再贅述了。
2.方法區
方法區主要用來存放類信息、類的靜態變量、常量、運行時常量池等
方法區和堆功能類似,主要用與存放類信息,常量和即時編譯器編譯后的代碼等數據。
2.2永久代
很多文章提到方法區的時候都會涉及到這個“永久代”這個詞。實際上,方法區是jvm的一個規范,永久代是這種規范的另一種實現,類似的還有元空間,這也是方法區的一種實現。
jvm虛擬機分為很多種,比如HotSpot ,JRockit(Oracle)、J9(IBM)等等,但是只有HotSpot才有永久代這個說法。硬要說的話,方法區可以理解為一個接口,永久區是這個接口的實現類。
2.3 運行時常量池
類似的問題還有運行時常量池。運行時常量池是方法區的一部分,用於存儲各種編譯時以及運行時產生的新常量,類加載以后的數據就存放於此,還有字符串手動入池方法intern()
。
這里的 “運行時常量池”同上文提到的方法區和永久代的關系一樣,也是jvm的規范而不是實現,運行時常量必然會有一個專門的儲存空間,但是放在哪就得看虛擬機各自的實現了。
不過這里要額外理解一下字符串常量池:
常量池分為兩塊,一塊是堆中的字符串常量池,一塊是方法區中的常量池。實際上JDK8之前字符串常量池也在方法區中的常量池里邊,而在JDK8之后被單獨分離出來放到了堆里。