深入JVM系列(一)之內存模型與內存分配

一、JVM內存區域劃分


大多數 JVM 將內存區域劃分爲 Method Area(Non-Heap),Heap,Program Counter Register, Java Method Stack,Native Method StackDirect Memomry(注意 Directory Memory 並不屬於 JVM 管理的內存區域)。前三者一般譯爲:方法區、堆、程序計數器。但不同的資料和書籍上對於後三者的中文譯名不盡相同,這裏將它們分別譯作:Java 方法棧、原生方法棧和直接內存區。對於不同的 JVM,內存區域劃分可能會有所差異,比如 Hot Spot 就將 Java 方法棧和原生方法棧合二爲一,我們可以統稱爲方法棧(Method Stack)



首先我們熟悉一下一個一般性的 Java 程序的工作過程。一個 Java 源程序文件,會被編譯爲字節碼文件(以 class 爲擴展名),然後告知 JVM 程序的運行入口,再被 JVM 通過字節碼解釋器加載運行。那麼程序開始運行後,都是如何涉及到各內存區域的呢?

概括地說來,JVM 每遇到一個線程,就爲其分配一個程序計數器、Java 方法棧和原生方法棧。當線程終止時,兩者所佔用的內存空間也會被釋放掉。棧中存儲的是棧幀,可以說每個棧幀對應一個“運行現場”。在每個“運行現場”中,如果出現了一個局部對象,則它的實例數據被保存在堆中,而類數據被保存在方法區。

二、指令、方法與屬性


在講各部分之前,我們首先要搞清楚的是什麼是數據以及什麼是指令。然後要搞清楚對象的方法和對象的屬性分別保存在哪裏。

1)方法本身是指令的操作碼部分,保存在Stack中;

2)方法內部變量作爲指令的操作數部分,跟在指令的操作碼之後,保存在Stack中(實際上是簡單類型保存在Stack中,對象類型在Stack中保存地址,在Heap 中保存值);上述的指令操作碼和指令操作數構成了完整的Java 指令。

3)對象實例包括其屬性值作爲數據,保存在數據區Heap 中。

非靜態的對象屬性作爲對象實例的一部分保存在Heap 中,而對象實例必須通過Stack中保存的地址指針才能訪問到。因此能否訪問到對象實例以及它的非靜態屬性值完全取決於能否獲得對象實例在Stack中的地址指針。

非靜態方法和靜態方法的區別:


非靜態方法有一個和靜態方法很重大的不同:非靜態方法有一個隱含的傳入參數,該參數是JVM給它的,和我們怎麼寫代碼無關,這個隱含的參數就是對象實例在Stack中的地址指針。因此非靜態方法(在Stack中的指令代碼)總是可以找到自己的專用數據(在Heap 中的對象屬性值)。當然非靜態方法也必須獲得該隱含參數,因此非靜態方法在調用前,必須先new一個對象實例,獲得Stack中的地址指針,否則JVM將無法將隱含參數傳給非靜態方法。

靜態方法無此隱含參數,因此也不需要new對象,只要class文件被ClassLoader load進入JVM的Stack,該靜態方法即可被調用。當然此時靜態方法是存取不到Heap 中的對象屬性的。

總結一下該過程:當一個class文件被ClassLoader load進入JVM後,方法指令保存在Stack中,此時Heap 區沒有數據。然後程序技術器開始執行指令,如果是靜態方法,直接依次執行指令代碼,當然此時指令代碼是不能訪問Heap 數據區的;如果是非靜態方法,由於隱含參數沒有值,會報錯。因此在非靜態方法執行前,要先new對象,在Heap 中分配數據,並把Stack中的地址指針交給非靜態方法,這樣程序技術器依次執行指令,而指令代碼此時能夠訪問到Heap 數據區了。

靜態屬性和動態屬性:


前面提到對象實例以及動態屬性都是保存在Heap 中的,而Heap 必須通過Stack中的地址指針才能夠被指令(類的方法)訪問到。因此可以推斷出:靜態屬性是保存在Stack中的,而不同於動態屬性保存在Heap 中。正因爲都是在Stack中,而Stack中指令和數據都是定長的,因此很容易算出偏移量,也因此不管什麼指令(類的方法),都可以訪問到類的靜態屬性。也正因爲靜態屬性被保存在Stack中,所以具有了全局屬性。

在JVM中,靜態屬性保存在Stack指令內存區,動態屬性保存在Heap數據內存區。

三、Stack 棧


Stack(棧)是JVM的內存指令區。Stack管理很簡單,push一定長度字節的數據或者指令,Stack指針壓棧相應的字節位移;pop一定字節長度數據或者指令,Stack指針彈棧。Stack的速度很快,管理很簡單,並且每次操作的數據或者指令字節長度是已知的。所以Java 基本數據類型,Java 指令代碼,常量都保存在Stack中。

棧也叫棧內存,是 Java 程序的運行區,是在線程創建時創建,它的生命期是跟隨線程的生命
,線程結束棧內存也就釋放,對於棧來說不存在垃圾回收問題,只要線程一結束,該棧就 Over。

那麼棧中存的是那些數據呢?又什麼是格式呢?

棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是
一個有關方法(Method)和運行期數據的數據集,當一個方法 A 被調用時就產生了一個棧幀 F1,並
被壓入到棧中,A 方法又調用了 B 方法,於是產生棧幀 F2 也被壓入棧,執行完畢後,先彈出 F2
棧幀,再彈出 F1 棧幀,遵循“先進後出”原則。

那棧幀中到底存在着什麼數據呢?棧幀中主要保存 3 類數據:本地變量(Local Variables),
包括輸入參數和輸出參數以及方法內的變量;棧操作(Operand Stack),記錄出棧、入棧的操作;
棧幀數據(Frame Data),包括類文件、方法等等。光說比較枯燥,我們畫個圖來理解一下 Java
棧,如下圖所示:
 

四、Heap 堆


Heap(堆)是JVM的內存數據區。Heap 的管理很複雜,每次分配不定長的內存空間,專門用來保存對象的實例。在Heap 中分配一定的內存來保存對象實例,實際上也只是保存對象實例的屬性值,屬性的類型和對象本身的類型標記等,並不保存對象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的內存保存對象實例和對象的序列化比較類似。而對象實例在Heap 中分配好以後,需要在Stack中保存一個4字節的Heap 內存地址,用來定位該對象實例在Heap 中的位置,便於找到該對象實例。

Java中堆是由所有的線程共享的一塊內存區域。

4.1 Generation


JVM堆一般又可以分爲以下三部分:

Perm

Perm代主要保存class,method,filed對象,這部門的空間一般不會溢出,除非一次性加載了很多的類,不過在涉及到熱部署的應用服務器的時候,有時候會遇到java.lang.OutOfMemoryError : PermGen space 的錯誤,造成這個錯誤的很大原因就有可能是每次都重新部署,但是重新部署後,類的class沒有被卸載掉,這樣就造成了大量的class對象保存在了perm中,這種情況下,一般重新啓動應用服務器可以解決問題。

Tenured

Tenured區主要保存生命週期長的對象,一般是一些老的對象,當一些對象在Young複製轉移一定的次數以後,對象就會被轉移到Tenured區,一般如果系統中用了application級別的緩存,緩存中的對象往往會被轉移到這一區間。

Young

Young區被劃分爲三部分,Eden區和兩個大小嚴格相同的Survivor區,其中Survivor區間中,某一時刻只有其中一個是被使用的,另外一個留做垃圾收集時複製對象用,在Young區間變滿的時候,minor GC就會將存活的對象移到空閒的Survivor區間中,根據JVM的策略,在經過幾次垃圾收集後,任然存活於Survivor的對象將被移動到Tenured區間。

4.2 Sizing the Generations


JVM提供了相應的參數來對內存大小進行配置。正如上面描述,JVM中堆被分爲了3個大的區間,同時JVM也提供了一些選項對Young,Tenured的大小進行控制。

Total Heap

-Xms :指定了JVM初始啓動以後初始化內存

-Xmx:指定JVM堆得最大內存,在JVM啓動以後,會分配-Xmx參數指定大小的內存給JVM,但是不一定全部使用,JVM會根據-Xms參數來調節真正用於JVM的內存

-Xmx -Xms之差就是三個Virtual空間的大小

Young Generation

-XX:NewRatio=8意味着tenured 和 young的比值8:1,這樣eden+2*survivor=1/9

堆內存

-XX:SurvivorRatio=32意味着eden和一個survivor的比值是32:1,這樣一個Survivor就佔Young區的1/34.

-Xmn 參數設置了年輕代的大小

Perm Generation

-XX:PermSize=16M -XX:MaxPermSize=64M

Thread Stack

-XX:Xss=128K

五、The pc Register 程序計數器寄存器


JVM支持多個線程同時運行。每個JVM都有自己的程序計數器。在任何一個點,每個JVM線程執行單個方法的代碼,這個方法是線程的當前方法。如果方法不是native的,程序計數器寄存器包含了當前執行的JVM指令的地址,如果方法是 native的,程序計數器寄存器的值不會被定義。 JVM的程序計數器寄存器的寬度足夠保證可以持有一個返回地址或者native的指針。

六、Method Area 方法區


Object Class Data(類定義數據) 是存儲在方法區的。除此之外,常量、靜態變量、JIT 編譯後的代碼也都在方法區。正因爲方法區所存儲的數據與堆有一種類比關係,所以它還被稱爲 Non-Heap。方法區也可以是內存不連續的區域組成的,並且可設置爲固定大小,也可以設置爲可擴展的,這點與堆一樣。

方法區內部有一個非常重要的區域,叫做運行時常量池(Runtime Constant Pool,簡稱 RCP)。在字節碼文件中有常量池(Constant Pool Table),用於存儲編譯器產生的字面量和符號引用。每個字節碼文件中的常量池在類被加載後,都會存儲到方法區中。值得注意的是,運行時產生的新常量也可以被放入常量池中,比如 String 類中的 intern() 方法產生的常量。

6.1 常量池 (constant pool)


常量池指的是在編譯期被確定,並被保存在已編譯的.class文件中的一些數據。除了包含代碼中所定義的各種基本類型(如int、long等等)和對象型(如String及數組)的常量值(final)還包含一些以文本形式出現的符號引用,比如:

◆類和接口的全限定名;

◆字段的名稱和描述符;

◆方法和名稱和描述符。

虛擬機必須爲每個被裝載的類型維護一個常量池。常量池就是該類型所用到常量的一個有序集和,包括直接常量(string,integer和 floating point常量)和對其他類型,字段和方法的符號引用。

對於String常量,它的值是在常量池中的。而JVM中的常量池在內存當中是以表的形式存在的, 對於String類型,有一張固定長度的CONSTANT_String_info表用來存儲文字字符串值,注意:該表只存儲文字字符串值,不存儲符號引 用。說到這裏,對常量池中的字符串值的存儲位置應該有一個比較明瞭的理解了。
在程序執行的時候,常量池 會儲存在Method Area,而不是堆中。

七、Java Method Stack Java 方法棧 與 Native Method Stack 原生方法棧


第七章內容來源:http://blog.csdn.net/poechant/article/details/7289093

Java 方法棧也是線程私有的,每個 Java 方法棧都是由一個個棧幀組成的,每個棧幀是一個方法運行期的基礎數據結構,它存儲着局部變量表、操作數棧、動態鏈接、方法出口等信息。當線程調用調用了一個 Java 方法時,一個棧幀就被壓入(push)到相應的 Java 方法棧。當線程從一個 Java 方法返回時,相應的 Java 方法棧就彈出(pop)一個棧幀。


其中要詳細介紹的是局部變量表,它保存者各種基本數據類型和對象引用(Object reference)。基本數據類型包括 boolean、byte、char、short、int、long、float、double。對象引用,本質就是一個地址(也可以說是一個“指針”),該地址是堆中的一個地址,通過這個地址可以找到相應的 Object(注意是“找到”,原因會在下面解釋)。而這個地址找到相應 Object 的方式有兩種。一種是該地址存儲着 Pointer to Object Instance Data 和 Pointer to Object Class Data,另一種是該地址存儲着 Object Instance Data,其中又包含有 Pointer to Object Class Data。如下兩圖所示。


第一種方式,Java 方法棧中有 Handler Pool 和 Instance Pool。無論哪種方式,Object Class Data 都是存儲在方法區的,Object Instance Data 都是存儲在堆中的。


圖1 句柄方式


圖2 直接方式

原生方法棧與 Java 方法棧相類似,這裏不再贅述。


八、JVM運行原理 例子


以上都是純理論,我們舉個例子來說明 JVM 的運行原理,我們來寫一個簡單的類,代碼如下:
public class JVMShowcase {
//靜態類常量,
public final static String ClASS_CONST = "I'm a Const";
//私有實例變量
private int instanceVar=15;
public static void main(String[] args) {
//調用靜態方法
runStaticMethod();
//調用非靜態方法
JVMShowcase showcase=new JVMShowcase();
showcase.runNonStaticMethod(100);
}
//常規靜態方法
public static String runStaticMethod(){
return ClASS_CONST;
}
//非靜態方法
public int runNonStaticMethod(int parameter){
int methodVar=this.instanceVar * parameter;
return methodVar;
}
}


這個類沒有任何意義,不用猜測這個類是做什麼用,只是寫一個比較典型的類,然後我們來看
看 JVM 是如何運行的,也就是輸入 java JVMShow 後,我們來看 JVM 是如何處理的:

       向操作系統申請空閒內存。JVM 對操作系統說“給我 64M 空閒內存”,於是第 1 步,JVM 向操作系統申請空閒內存
作系統就查找自己的內存分配表,找了段 64M 的內存寫上“Java 佔用”標籤,然後把內存段的起始地址和終止地址給 JVM,JVM 準備加載類文件。

分配內存內存。第 2 步,JVM 分配內存。JVM 獲得到 64M 內存,就開始得瑟了,首先給 heap 分個內存,並
且是按照 heap 的三種不同類型分好的,然後給棧內存也分配好。

        文件。第 3 步,檢查和分析 class 文件。若發現有錯誤即返回錯誤。

    加載類。第 4 步,加載類。由於沒有指定加載器,JVM 默認使用 bootstrap 加載器,就把 rt.jar 下的所有
類都加載到了堆類存的永久存儲區,JVMShow 也被加載到內存中。我們來看看棧內存,如下圖:

Heap 是空,Stack 是空,因爲還沒有線程被執行。Class Loader 通知 Execution Enginer 已經加
載完畢。

    執行引擎執行方法。第 5 步,執行引擎執行 main 方法。執行引擎啓動一個線程,開始執行 main 方法,在 main 執
行完畢前,方法區如下圖所示:


在 Method Area 加入了 CLASS_CONST 常量,它是在第一次被訪問時產生的。堆內存中有兩個對象 object 和 showcase 對象,如下圖所示:

爲什麼會有 Object 對象呢?是因爲它是 JVMShowcase 的父類,JVM 是先初始化父類,然後再
初始化子類,甭管有多少個父類都初始化。在棧內存中有三個棧幀,如下圖所示:

於此同時,還創建了一個程序計數器指向下一條要執行的語句。
   
釋放內存。運第 6 步,釋放內存。運行結束,JVM 向操作系統發送消息,說“內存用完了,我還給你”
行結束。


九、JVM 相關問題


問:堆和棧有什麼區別堆和棧有什麼區別有什麼
答:堆是存放對象的,但是對象內的臨時變量是存在棧內存中,如例子中的 methodVar 是在運
行期存放到棧中的。
棧是跟隨線程的,有線程就有棧,堆是跟隨 JVM 的,有 JVM 就有堆內存。

問:堆內存中到底存在着什麼東西?堆內存中到底存在着什麼東西?
答:對象,包括對象變量以及對象方法。

問:類變量和實例變量有什麼區別?類變量和實例變量有什麼區別?有什麼區別
答:靜態變量是類變量,非靜態變量是實例變量,直白的說,有 static 修飾的變量是靜態變量,
沒有 static 修飾的變量是實例變量。靜態變量存在方法區中,實例變量存在堆內存中。
     啓動時就初始化好的,和你這說的不同呀!

問:我聽說類變量是在 JVM 啓動時就初始化好的,和你這說的不同呀!
答:那你是道聽途說,信我的,沒錯。
     的方法(函數)到底是傳值還是傳址值還是傳址?

問:Java 的方法(函數)到底是傳值還是傳址?
答:都不是,是以傳值的方式傳遞地址,具體的說原生數據類型傳遞的值,引用類型傳遞的地
址。對於原始數據類型,JVM 的處理方法是從 Method Area 或 Heap 中拷貝到 Stack,然後運行 frame
中的方法,運行完畢後再把變量指拷貝回去。
             產生?

問:爲什麼會產生 OutOfMemory 產生?
答:一句話:Heap 內存中沒有足夠的可用內存了。這句話要好好理解,不是說 Heap 沒有內存
了,是說新申請內存的對象大於 Heap 空閒內存,比如現在 Heap 還空閒 1M,但是新申請的內存需
要 1.1M,於是就會報 OutOfMemory 了,可能以後的對象申請的內存都只要 0.9M,於是就只出現
一次 OutOfMemory,GC 也正常了,看起來像偶發事件,就是這麼回事。 但如果此時 GC 沒有回
收就會產生掛起情況,系統不響應了。

問:我產生的對象不多呀,爲什麼還會產生 OutOfMemory?我產生的對象不多呀,?
答:你繼承層次忒多了,Heap 中 產生的對象是先產生 父類,然後才產生子類,明白不?
            錯誤分幾種?問:OutOfMemory 錯誤分幾種?
答:分兩種,分別是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen
space”,兩種都是內存溢出,heap size 是說申請不到新的內存了,這個很常見,檢查應用或調整
堆內存大小。
“PermGen space”是因爲永久存儲區滿了,這個也很常見,一般在熱發佈的環境中出現,是
因爲每次發佈應用系統都不重啓,久而久之永久存儲區中的死對象太多導致新對象無法申請內存,
一般重新啓動一下即可。

問:爲什麼會產生 StackOverflowError??
答:因爲一個線程把 Stack 內存全部耗盡了,一般是遞歸函數造成的。
         之間可以互訪嗎?

問:一個機器上可以看多個 JVM 嗎?JVM 之間可以互訪嗎?
答:可以多個 JVM,只要機器承受得了。JVM 之間是不可以互訪,你不能在 A-JVM 中訪問
B-JVM 的 Heap 內存,這是不可能的。在以前老版本的 JVM 中,會出現 A-JVM Crack 後影響到
B-JVM,現在版本非常少見。
      要採用垃圾回收機制,的顯式

問:爲什麼 Java 要採用垃圾回收機制,而不採用 C/C++的顯式內存管理?的顯 內存管理?
答:爲了簡單,內存管理不是每個程序員都能折騰好的。

問:爲什麼你沒有詳細介紹垃圾回收機制?爲什麼你沒有詳細介紹垃圾回收機制
答:垃圾回收機制每個 JVM 都不同,JVM Specification 只是定義了要自動釋放內存,也就是
說它只定義了垃圾回收的抽象方法,具體怎麼實現各個廠商都不同,算法各異,這東西實在沒必要
深入。
    中到底哪些區域是共享的?哪些是私有的?

問:JVM 中到底哪些區域是共享的?哪些是私有的?
答:Heap 和 Method Area 是共享的,其他都是私有的,

問:什麼是 JIT,你怎麼沒說?,你怎麼沒說?
答:JIT 是指 Just In Time,有的文檔把 JIT 作爲 JVM 的一個部件來介紹,有的是作爲執行引
擎的一部分來介紹,這都能理解。Java 剛誕生的時候是一個解釋性語言,別噓,即使編譯成了字
節碼(byte code)也是針對 JVM 的,它需要再次翻譯成原生代碼(native code)才能被機器執行,於
是效率的擔憂就提出來了。Sun 爲了解決該問題提出了一套新的機制,好,你想編譯成原生代碼,
沒問題,我在 JVM 上提供一個工具,把字節碼編譯成原生碼,下次你來訪問的時候直接訪問原生
碼就成了,於是 JIT 就誕生了,就這麼回事。
    還有哪些部分是你沒有提到的?

問:JVM 還有哪些部分是你沒有提到的?
答:JVM 是一個異常複雜的東西,寫一本磚頭書都不爲過,還有幾個要說明的:
常量池(constant pool)按照順序存放程序中的常量,:並且進行索引編號的區域。比如 int i =100,
這個 100 就放在常量池中。
安全管理器(Security Manager):提供 Java 運行期的安全控制,防止惡意攻擊,比如指定讀取
文件,寫入文件權限,網絡訪問,創建進程等等,Class Loader 在 Security Manager 認證通過後才
能加載 class 文件的。
方法索引表(Methods table),記錄的是每個 method 的地址信息,Stack 和 Heap 中的地址指針
其實是指向 Methods table 地址。

問:爲什麼不建議在程序中顯式的生命 System.gc()??
答:因爲顯式聲明是做堆內存全掃描,也就是 Full GC,是需要停止所有的活動的(Stop The
World Collection),你的應用能承受這個嗎?

問:JVM 有哪些調整參數?
答:非常多,自己去找,堆內存、棧內存的大小都可以定義,甚至是堆內存的三個部分、新生
代的各個比例都能調整。


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