天天看點

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

看到深入兩字,相信很多的JAVA初學者都會直接忽略這樣的文章,其實關于JVM記憶體區域的知識對于初學者來說其實是很重要的,了解Java記憶體配置設定的原理,這對于以後JAVA的學習會有更深刻的了解,這是我個人的看法。

先來看看JVM運作時候的記憶體區域

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

大多數 JVM 将記憶體區域劃分為 Method Area(Non-Heap)(方法區),Heap(堆),Program Counter Register(程式計數器), VM Stack(虛拟機棧,也有翻譯成JAVA 方法棧的),Native Method Stack (本地方法棧),其中Method Area和Heap是線程共享的,VMStack,Native Method Stack 和Program Counter Register是非線程共享的。為什麼分為線程共享和非線程共享的呢?請繼續往下看。

首先我們熟悉一下一個一般性的 Java 程式的工作過程。一個 Java 源程式檔案,會被編譯為位元組碼檔案(以 class 為擴充名),每個java程式都需要運作在自己的JVM上,然後告知 JVM 程式的運作入口,再被 JVM 通過位元組碼解釋器加載運作。那麼程式開始運作後,都是如何涉及到各記憶體區域的呢?

概括地說來,JVM初始運作的時候都會配置設定好Method Area(方法區)和Heap(堆),而JVM 每遇到一個線程,就為其配置設定一個Program Counter Register(程式計數器), VM Stack(虛拟機棧)和Native Method Stack (本地方法棧),當線程終止時,三者(虛拟機棧,本地方法棧和程式計數器)所占用的記憶體空間也會被釋放掉。這也是為什麼我把記憶體區域分為線程共享和非線程共享的原因,非線程共享的那三個區域的生命周期與所屬線程相同,而線程共享的區域與JAVA程式運作的生命周期相同,是以這也是系統垃圾回收的場所隻發生線上程共享的區域(實際上對大部分虛拟機來說知發生在Heap上)的原因。

1.程式計數器

程式計數器是一塊較小的記憶體區域,作用可以看做是目前線程執行的位元組碼的位置訓示器。分支、循環、跳轉、異常處理和線程恢複等基礎功能都需要依賴這個電腦來完成,不多說。

2.VM Strack

先來了解下JAVA指令的構成:

JAVA指令由 操作碼 (方法本身)和 操作數 (方法内部變量) 組成。

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

2)方法内部變量(局部變量)作為指令的操作數部分,跟在指令的操作碼之後,儲存在Stack中(實際上是簡單類型(int,byte,short 等)儲存在Stack中,對象類型在Stack中儲存位址,在Heap 中儲存值);

虛拟機棧也叫棧記憶體,是線上程建立時建立,它的生命期是跟随線程的生命期,線程結束棧記憶體也就釋放,對于棧來說不存在垃圾回收問題,隻要線程一結束,該棧就 Over,是以不存在垃圾回收。也有一些資料翻譯成JAVA方法棧,大概是因為它所描述的是java方法執行的記憶體模型,每個方法執行的同時建立幀棧(Strack Frame)用于存儲局部變量表(包含了對應的方法參數和局部變量),操作棧(Operand Stack,記錄出棧、入棧的操作),動态連結、方法出口等資訊,每個方法被調用直到執行完畢的過程,對應這幀棧在虛拟機棧的入棧和出棧的過程。

局部變量表存放了編譯期可知的各種基本資料類型(boolean、byte、char、short、int、float、long、double)、對象的引用(reference類型,不等同于對象本身,根據不同的虛拟機實作,可能是一個指向對象起始位址的引用指針,也可能是一個代表對象的句柄或者其他與對象相關的位置)和 returnAdress類型(指向下一條位元組碼指令的位址)。局部變量表所需的記憶體空間在編譯期間完成配置設定,在方法在運作之前,該局部變量表所需要的記憶體空間是固定的,運作期間也不會改變。

棧幀是一個記憶體區塊,是一個資料集,是一個有關方法(Method)和運作期資料的資料集,當一個方法 A 被調用時就産生了一個棧幀 F1,并被壓入到棧中,A 方法又調用了 B 方法,于是産生棧幀 F2 也被壓入棧,執行完畢後,先彈出 F2棧幀,再彈出 F1 棧幀,遵循“先進後出”原則。光說比較枯燥,我們看一個圖來了解一下 Java棧,如下圖所示:

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

3.Heap

Heap(堆)是JVM的記憶體資料區。Heap 的管理很複雜,是被所有線程共享的記憶體區域,在JVM啟動時候建立,專門用來儲存對象的執行個體。在Heap 中配置設定一定的記憶體來儲存對象執行個體,實際上也隻是儲存對象執行個體的屬性值,屬性的類型和對象本身的類型标記等,并不儲存對象的方法(以幀棧的形式儲存在Stack中),在Heap 中配置設定一定的記憶體儲存對象執行個體。而對象執行個體在Heap 中配置設定好以後,需要在Stack中儲存一個4位元組的Heap 記憶體位址,用來定位該對象執行個體在Heap 中的位置,便于找到該對象執行個體,是垃圾回收的主要場所。java堆處于實體不連續的記憶體空間中,隻要邏輯上連續即可。

4.Method Area

Object Class Data(加載類的類定義資料) 是存儲在方法區的。除此之外,常量、靜态變量、JIT(即時編譯器)編譯後的代碼也都在方法區。正因為方法區所存儲的資料與堆有一種類比關系,是以它還被稱為 Non-Heap。方法區也可以是記憶體不連續的區域組成的,并且可設定為固定大小,也可以設定為可擴充的,這點與堆一樣。

垃圾回收在這個區域會比較少出現,這個區域記憶體回收的目的主要針對常量池的回收和類的解除安裝。

5.運作時常量池(Runtime Constant Pool)

方法區内部有一個非常重要的區域,叫做運作時常量池(Runtime Constant Pool,簡稱 RCP)。在位元組碼檔案(Class檔案)中,除了有類的版本、字段、方法、接口等相關資訊描述外,還有常量池(Constant Pool Table)資訊,用于存儲編譯器産生的字面量和符号引用。這部分内容在類被加載後,都會存儲到方法區中的RCP。值得注意的是,運作時産生的新常量也可以被放入常量池中,比如 String 類中的 intern() 方法産生的常量。

常量池就是這個類型用到的常量的一個有序集合。包括直接常量(基本類型,String)和對其他類型、方法、字段的符号引用.例如:

◆類和接口的全限定名;

◆字段的名稱和描述符;

◆方法和名稱和描述符。

池中的資料和數組一樣通過索引通路。由于常量池包含了一個類型所有的對其他類型、方法、字段的符号引用,是以常量池在Java的動态連結中起了核心作用.

很有用且重要關于常量池的擴充:Java常量池詳解 http://www.cnblogs.com/DreamSea/archive/2011/11/20/2256396.html

6.Native Method Stack

與VM Strack相似,VM Strack為JVM提供執行JAVA方法的服務,Native Method Stack則為JVM提供使用native 方法的服務。

7.直接記憶體區

直接記憶體區并不是 JVM 管理的記憶體區域的一部分,而是其之外的。該區域也會在 Java 開發中使用到,并且存在導緻記憶體溢出的隐患。如果你對 NIO 有所了解,可能會知道 NIO 是可以使用 Native Methods 來使用直接記憶體區的。

小結:

  • 在此,你對JVM的記憶體區域有了一定的了解,JVM記憶體區域可以分為線程共享和非線程共享兩部分,線程共享的有堆和方法區,非線程共享的有虛拟機棧,本地方法棧和程式計數器。

8.JVM運作原理 例子

以上都是純理論,我們舉個例子來說明 JVM 的運作原理,我們來寫一個簡單的類,代碼如下:

1 public class JVMShowcase {

2 //靜态類常量,

3 public final static String ClASS_CONST = "I'm a Const";

4 //私有執行個體變量

5 private int instanceVar=15;

6 public static void main(String[] args) {

7 //調用靜态方法

8 runStaticMethod();

9 //調用非靜态方法

10 JVMShowcase showcase=new JVMShowcase();

11 showcase.runNonStaticMethod(100);

12 }

13 //正常靜态方法

14 public static String runStaticMethod(){

15 return ClASS_CONST;

16 }

17 //非靜态方法

18 public int runNonStaticMethod(int parameter){

19 int methodVar=this.instanceVar * parameter;

20 return methodVar;

21 }

22 }

這個類沒有任何意義,不用猜測這個類是做什麼用,隻是寫一個比較典型的類,然後我們來看

看 JVM 是如何運作的,也就是輸入 java JVMShow 後,我們來看 JVM 是如何處理的:

第 1 步 、向作業系統申請空閑記憶體。JVM 對作業系統說“給我 64M(随便模拟資料,并不是真實資料) 空閑記憶體”,于是,JVM 向作業系統申請空閑記憶體作系統就查找自己的記憶體配置設定表,找了段 64M 的記憶體寫上“Java 占用”标簽,然後把記憶體段的起始位址和終止位址給 JVM,JVM 準備加載類檔案。

第 2 步,配置設定記憶體記憶體。JVM 配置設定記憶體。JVM 獲得到 64M 記憶體,就開始得瑟了,首先給 heap 分個記憶體,然後給棧記憶體也配置設定好。

第 3 步,檔案檢查和分析class 檔案。若發現有錯誤即傳回錯誤。

第 4 步,加載類。加載類。由于沒有指定加載器,JVM 預設使用 bootstrap 加載器,就把 rt.jar 下的所有類都加載到了堆類存的Method Area,JVMShow 也被加載到記憶體中。我們來看看Method Area區域,如下圖:(這時候包含了 main 方法和 runStaticMethod方法的符号引用,因為它們都是靜态方法,在類加載的時候就會加載)

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

Heap 是空,Stack 是空,因為還沒有對象的建立和線程被執行。

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

(public final static String ClASS_CONST = "I'm a Const"; )

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

在 Method Area 加入了 CLASS_CONST 常量,它是在第一次被通路時産生的(runStaticMethod方法内部)。

堆記憶體中有兩個對象 object 和 showcase 對象,如下圖所示:(執行了JVMShowcase showcase=new JVMShowcase(); )

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

為什麼會有 Object 對象呢?是因為它是 JVMShowcase 的父類,JVM 是先初始化父類,然後再初始化子類,甭管有多少個父類都初始化。

在棧記憶體中有三個棧幀,如下圖所示:

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

于此同時,還建立了一個程式計數器指向下一條要執行的語句。

第 6 步,釋放記憶體。釋放記憶體。運作結束,JVM 向作業系統發送消息,說“記憶體用完了,我還給你”,運作結束。

--------------------------------------------------------------------------------------------

現在來看JVM記憶體是如何配置設定的,該部分轉載來自 http://blog.csdn.net/shimiso/article/details/8595564

預備知識:

1.一個Java檔案,隻要有main入口方法,我們就認為這是一個Java程式,可以單獨編譯運作。

2.無論是普通類型的變量還是引用類型的變量(俗稱執行個體),都可以作為局部變量,他們都可以出現在棧中。隻不過普通類型的變量在棧中直接儲存它所對應的值,而引用類型的變量儲存的是一個指向堆區的指針,通過這個指針,就可以找到這個執行個體在堆區對應的對象。是以,普通類型變量隻在棧區占用一塊記憶體,而引用類型變量要在棧區和堆區各占一塊記憶體。

示例:(以下所有執行個體中,是根據需要對于棧記憶體中的幀棧簡化成了隻有局部變量表,實際上由上面對幀棧的介紹知道不僅僅隻有這些資訊,同理堆記憶體也一樣)

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

1.JVM自動尋找main方法,執行第一句代碼,建立一個Test類的執行個體,在棧中配置設定一塊記憶體,存放一個指向堆區對象的指針110925。

2.建立一個int型的變量date,由于是基本類型,直接在棧中存放date對應的值9。

3.建立兩個BirthDate類的執行個體d1、d2,在棧中分别存放了對應的指針指向各自的對象。他們在執行個體化時調用了有參數的構造方法,是以對象中有自定義初始值。

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

調用test對象的change1方法,并且以date為參數。JVM讀到這段代碼時,檢測到i是局部變量,是以會把i放在棧中,并且把date的值賦給i。

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

把1234賦給i。很簡單的一步。

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

change1方法執行完畢,立即釋放局部變量i所占用的棧空間。

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

調用test對象的change2方法,以執行個體d1為參數。JVM檢測到change2方法中的b參數為局部變量,立即加入到棧中,由于是引用類型的變量,是以b中儲存的是d1中的指針,此時b和d1指向同一個堆中的對象。在b和d1之間傳遞是指針。

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

change2方法中又執行個體化了一個BirthDate對象,并且賦給b。在内部執行過程是:在堆區new了一個對象,并且把該對象的指針儲存在棧中的b對應空間,此時執行個體b不再指向執行個體d1所指向的對象,但是執行個體d1所指向的對象并無變化,這樣無法對d1造成任何影響。

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

change2方法執行完畢,立即釋放局部引用變量b所占的棧空間,注意隻是釋放了棧空間,堆空間要等待自動回收。

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

調用test執行個體的change3方法,以執行個體d2為參數。同理,JVM會在棧中為局部引用變量b配置設定空間,并且把d2中的指針存放在b中,此時d2和b指向同一個對象。再調用執行個體b的setDay方法,其實就是調用d2指向的對象的setDay方法。

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

調用執行個體b的setDay方法會影響d2,因為二者指向的是同一個對象。

jvm記憶體參數配置_通過Java程式中參數傳遞深入了解JVM之記憶體區域與記憶體配置設定

change3方法執行完畢,立即釋放局部引用變量b。

以上就是Java程式運作時記憶體配置設定的大緻情況。其實也沒什麼,掌握了思想就很簡單了。無非就是兩種類型的變量:基本類型和引用類型。二者作為局部變量,都放在棧中,基本類型直接在棧中儲存值,引用類型隻儲存一個指向堆區的指針,真正的對象在堆裡。作為參數時基本類型就直接傳值,引用類型傳指針。

小結:

1.厘清什麼是執行個體什麼是對象。Class a= new Class();此時a叫執行個體,而不能說a是對象。執行個體在棧中,對象在堆中,操作執行個體實際上是通過執行個體的指針間接操作對象。多個執行個體可以指向同一個對象。

2.棧中的資料和堆中的資料銷毀并不是同步的。方法一旦結束,棧中的局部變量立即銷毀,但是堆中對象不一定銷毀。因為可能有其他變量也指向了這個對象,直到棧中沒有變量指向堆中的對象時,它才銷毀,而且還不是馬上銷毀,要等垃圾回收掃描時才可以被銷毀。

3.以上的棧、堆、代碼段、資料段等等都是相對于應用程式而言的。每一個應用程式都對應唯一的一個JVM執行個體,每一個JVM執行個體都有自己的記憶體區域,互不影響。并且這些記憶體區域是所有線程共享的。這裡提到的棧和堆都是整體上的概念,這些堆棧還可以細分。

4.類的成員變量在不同對象中各不相同,都有自己的存儲空間(成員變量在堆中的對象中)。而類的方法卻是該類的所有對象共享的,隻有一套,對象使用方法的時候方法才被壓入棧,方法不使用則不占用記憶體。