1. 程式人生 > >Java學習筆記--記憶體劃分 堆疊方法區

Java學習筆記--記憶體劃分 堆疊方法區

這裡寫圖片描述

淺理解:
這裡寫圖片描述

假如你寫了一段程式碼:Object o=new Object();運行了起來!首先JVM會啟動,你的程式碼會編譯成一個.class檔案,然後被類載入器載入進jvm的記憶體中,你的類Object載入到方法區中,建立了Object類的class物件到堆中,注意這個不是new出來的物件,而是類的型別物件,每個類只有一個class物件,作為方法區類的資料結構的介面。jvm建立物件前,會先檢查類是否載入,尋找類對應的class物件,若載入好,則為你的物件分配記憶體,初始化也就是程式碼:new Object()。

這兩天看了一下深入淺出JVM這本書,推薦給高階的java程式設計師去看,對你瞭解JAVA的底層和執行機制有

比較大的幫助。

廢話不想講了.入主題:

先了解具體的概念:

JAVA的JVM的記憶體可分為3個區:堆(heap)、棧(stack)和方法區(method)

堆區:
1.儲存的全部是物件,每個物件都包含一個與之對應的class的資訊。(class的目的是得到操作指令)
2.jvm只有一個堆區(heap)被所有執行緒共享,堆中不存放基本型別和物件引用,只存放物件本身
棧區:
1.每個執行緒包含一個棧區,棧中只儲存基礎資料型別的物件和自定義物件的引用(不是物件),物件都存放在堆區中
2.每個棧中的資料(原始型別和物件引用)都是私有的,其他棧不能訪問。
3.棧分為3個部分:基本型別變數區、執行環境上下文、操作指令區(存放操作指令)。
方法區:
1.又叫靜態區,跟堆一樣,被所有的執行緒共享。方法區包含所有的class和static變數。
2.方法區中包含的都是在整個程式中永遠唯一的元素,如class,static變數。
為了更清楚地搞明白髮生在執行時資料區裡的黑幕,我們來準備2個小道具(2個非常簡單的小程式)。
AppMain.java

public class AppMain

//執行時, jvm 把appmain的資訊都放入方法區

{

public static void main(String[] args) //main 方法本身放入方法區。

{

Sample test1 = new Sample( ” 測試1 ” ); //test1是引用,所以放到棧區裡, Sample是自定義物件應該放到堆裡面

Sample test2 = new Sample( ” 測試2 ” );

test1.printName();

test2.printName();

}

}
Sample.java

public class Sample //執行時, jvm 把appmain的資訊都放入方法區

{

/** 範例名稱 */

private name; //new Sample例項後, name 引用放入棧區裡, name 物件放入堆裡

/** 構造方法 */

public Sample(String name)

{

this .name = name;

}

/** 輸出 */

public void printName() //print方法本身放入 方法區裡。

{

System.out.println(name);

}

}
OK,讓我們開始行動吧,出發指令就是:“java AppMain”,包包裡帶好我們的行動向導圖,Let’s GO!

系統收到了我們發出的指令,啟動了一個Java虛擬機器程序,這個程序首先從classpath中找到AppMain.class檔案,讀取這個檔案中的二進位制資料,然後把Appmain類的類資訊存放到執行時資料區的方法區中。這一過程稱為AppMain類的載入過程。
接著,Java虛擬機器定位到方法區中AppMain類的Main()方法的位元組碼,開始執行它的指令。這個main()方法的第一條語句就是:
Sample test1=new Sample(“測試1”);
語句很簡單啦,就是讓java虛擬機器建立一個Sample例項,並且呢,使引用變數test1引用這個例項。貌似小case一樁哦,就讓我們來跟蹤一下Java虛擬機器,看看它究竟是怎麼來執行這個任務的:
1、 Java虛擬機器一看,不就是建立一個Sample例項嗎,簡單,於是就直奔方法區而去,先找到Sample類的型別資訊再說。結果呢,嘿嘿,沒找到@@,這會兒的方法區裡還沒有Sample類呢。可Java虛擬機器也不是一根筋的笨蛋,於是,它發揚“自己動手,豐衣足食”的作風,立馬載入了Sample類,把Sample類的型別資訊存放在方法區裡。
2、 好啦,資料找到了,下面就開始幹活啦。Java虛擬機器做的第一件事情就是在堆區中為一個新的Sample例項分配記憶體, 這個Sample例項持有著指向方法區的Sample類的型別資訊的引用。這裡所說的引用,實際上指的是Sample類的型別資訊在方法區中的記憶體地址,其實,就是有點類似於C語言裡的指標啦~~,而這個地址呢,就存放了在Sample例項的資料區裡。
3、 在JAVA虛擬機器程序中,每個執行緒都會擁有一個方法呼叫棧,用來跟蹤執行緒執行中一系列的方法呼叫過程,棧中的每一個元素就被稱為棧幀,每當執行緒呼叫一個方法的時候就會向方法棧壓入一個新幀。這裡的幀用來儲存方法的引數、區域性變數和運算過程中的臨時資料。OK,原理講完了,就讓我們來繼續我們的跟蹤行動!位於“=”前的Test1是一個在main()方法中定義的變數,可見,它是一個區域性變數,因此,它被會新增到了執行main()方法的主執行緒的JAVA方法呼叫棧中。而“=”將把這個test1變數指向堆區中的Sample例項,也就是說,它持有指向Sample例項的引用。
OK,到這裡為止呢,JAVA虛擬機器就完成了這個簡單語句的執行任務。參考我們的行動向導圖,我們終於初步摸清了JAVA虛擬機器的一點點底細了,COOL!
接下來,JAVA虛擬機器將繼續執行後續指令,在堆區裡繼續建立另一個Sample例項,然後依次執行它們的printName()方法。當JAVA虛擬機器執行test1.printName()方法時,JAVA虛擬機器根據區域性變數test1持有的引用,定位到堆區中的Sample例項,再根據Sample例項持有的引用,定位到方法去中Sample類的型別資訊,從而獲得printName()方法的位元組碼,接著執行printName()方法包含的指令。

        </div>
            </div>

深理解:
轉載自大佬


理解JVM執行時的資料區是Java程式設計中的進階部分。我們在開發中都遇到過一個很頭疼的問題就是OutOfMemoryError(記憶體溢位錯誤),但是如果我們瞭解JVM的內部實現和其執行時的資料區的工作機制,那麼前面的問題就會迎刃而解。在這片文章中,我們將簡單瞭解JVM中有哪些執行時資料區以及這些資料區的工作機制。


1、JVM執行時資料區分類

  • 程式計數器 (Program Counter (PC) Register)

  • JVM棧 (Java Virtual Machine Stacks)

  • 堆記憶體 (Heap Memory)

  • 方法區 (Method Area)

  • 執行時常量池 (Run-time Constant Pool)

  • 本地方法棧 (Native Method Stacks)

2、看圖說話

3、按執行緒持有劃分

檢視上面的圖,可以得知以上六個資料區可以分為執行緒私有還是共享,總體分為如下兩種。

3.1 單個執行緒私有(Managed Per-Thread) 

屬於這一種的資料區包含 程式計數器, JVM棧還有本地方法棧。 每個執行緒都私有這三個資料區,這些資料區在其所屬的執行緒建立時初始化,並隨著所屬執行緒結束被銷燬。

3.1.1 程式計數器

在通用的計算機體系中,程式計數器用來記錄當前正在執行的指令,在JVM中也是如此。程式計數器是執行緒私有,所以當一個新的執行緒建立時,程式計數器也會建立。由於Java是支援多執行緒,Java中的程式計數器用來記錄當前執行緒中正在執行的指令。如果當前正在執行的方法是本地方法,那麼此刻程式計數器的值為undefined。注意這個區域是唯一一個不丟擲OutOfMemoryError的執行時資料區。

3.1.2 JVM棧

在介紹JVM棧之前,簡單介紹一個概念,棧幀

棧幀:一個棧幀隨著一個方法的呼叫開始而建立,這個方法呼叫完成而銷燬。棧幀記憶體放者方法中的區域性變數,運算元棧等資料。

JVM棧只對棧幀進行儲存,壓棧和出棧操作。棧記憶體的大小可以有兩種設定,固定值和根據執行緒需要動態增長。在JVM棧這個資料區可能會發生丟擲兩種錯誤。

  • StackOverflowError 出現在棧記憶體設定成固定值的時候,當程式執行需要的棧記憶體超過設定的固定值會丟擲這個錯誤。

  • OutOfMemoryError 出現在棧記憶體設定成動態增長的時候,當JVM嘗試申請的記憶體大小超過了其可用記憶體時會丟擲這個錯誤。

3.1.3 本地方法棧

一個支援native方法呼叫的JVM實現,需要有這樣一個數據區,就是本地方法棧,Java官方對於本地方法的定義為methods written in a language other than the Java programming language,就是使用非Java語言實現的方法,但是通常我們指的一般為C或者C++,因此這個棧也有著C棧這一稱號。一個不支援本地方法執行的JVM沒有必要實現這個資料區域。本地方法棧基本和JVM棧一樣,其大小也是可以設定為固定值或者動態增加,因此也會對應丟擲StackOverflowError和OutOfMemoryError錯誤。

3.2 多個執行緒共享 

屬於這一種的資料區包含 堆記憶體,方法區和執行時常量池。這些資料區可以被每一個執行緒訪問,他們隨著JVM啟動而初始化,同時伴隨JVM關閉而銷燬。

3.2.1 堆資料區

堆資料區是用來存放物件和陣列(特殊的物件)。堆記憶體由多個執行緒共享。堆記憶體隨著JVM啟動而建立。眾所周知,Java中有一個很好的特性就是自動垃圾回收。垃圾回收就操作這個資料區來回收物件進而釋放記憶體。如果堆記憶體剩餘的記憶體不足以滿足於物件建立,JVM會丟擲OutOfMemoryError錯誤。

3.2.2 方法區

在JVM規範中,方法區被視為堆記憶體的一個邏輯部分。這一點可能由於具體的JVM實現而不同,甚至在方法區不實現垃圾回收處理也是可以的。方法區和堆記憶體一樣被多個執行緒訪問,方法區中存放類的資訊,比如類載入器引用,屬性,方法程式碼和構造方法和常量等。當方法區的可用記憶體無法滿足記憶體分配需求時,JVM會丟擲OutOfMemoryError錯誤。

3.2.3 執行時常量池

執行時常量池建立在方法區,當一個類或者一個介面被建立的時候,JVM會建立一個執行時常量池。一個執行時常量池實際上是一個類或者介面的class檔案中常量池表(constant_pool table)的執行時展示形式。一個執行時常量池包含了多種型別的常量,從諸如執行時可以確定的數值型字面量到執行時才能決定的方法和屬性引用。當執行時常量池無法滿足於記憶體分配需求時,JVM會丟擲OutOfMemoryError錯誤。

4、Java 堆和棧的區別

當一個人開始學習Java或者其他程式語言的時候,會接觸到堆和棧,由於一開始沒有明確清晰的說明解釋,很多人會產生很多疑問,什麼是堆,什麼是棧,堆和棧有什麼區別?更糟糕的是,Java中存在棧這樣一個後進先出(Last In First Out)的順序的資料結構,這就是java.util.Stack。這種情況下,不免讓很多人更加費解前面的問題。事實上,堆和棧都是記憶體中的一部分,有著不同的作用,而且一個程式需要在這片區域上分配記憶體。眾所周知,所有的Java程式都執行在JVM虛擬機器內部,我們這裡介紹的自然是JVM(虛擬)記憶體中的堆和棧。

4.1 各司其職

最主要的區別就是棧記憶體用來儲存區域性變數和方法呼叫。

而堆記憶體用來儲存Java中的物件。無論是成員變數,區域性變數,還是類變數,它們指向的物件都儲存在堆記憶體中。

4.2 獨有還是共享

棧記憶體歸屬於單個執行緒,每個執行緒都會有一個棧記憶體,其儲存的變數只能在其所屬執行緒中可見,即棧記憶體可以理解成執行緒的私有記憶體。

而堆記憶體中的物件對所有執行緒可見。堆記憶體中的物件可以被所有執行緒訪問。

4.3 異常錯誤

如果棧記憶體沒有可用的空間儲存方法呼叫和區域性變數,JVM會丟擲java.lang.StackOverFlowError。

而如果是堆記憶體沒有可用的空間儲存生成的物件,JVM會丟擲java.lang.OutOfMemoryError。

4.4 空間大小

棧的記憶體要遠遠小於堆記憶體,如果你使用遞迴的話,那麼你的棧很快就會充滿。如果遞迴沒有及時跳出,很可能發生StackOverFlowError問題。

你可以通過-Xss選項設定棧記憶體的大小。-Xms選項可以設定堆的開始時的大小,-Xmx選項可以設定堆的最大值。

這就是Java中堆和棧的區別。理解好這個問題的話,可以對你解決開發中的問題,分析堆記憶體和棧記憶體使用,甚至效能調優都有幫助。

4.5 檢視預設值(Updated)

檢視堆的預設值,使用下面的程式碼,其中InitialHeapSize為最開始的堆的大小,MaxHeapSize為堆的最大值。

13:17 $ java -XX:+PrintFlagsFinal -version | grep HeapSize
    uintx ErgoHeapSizeLimit                         = 0                                   {product}
    uintx HeapSizePerGCThread                       = 87241520                            {product}
    uintx InitialHeapSize                          := 134217728                           {product}
    uintx LargePageHeapSizeThreshold                = 134217728                           {product}
    uintx MaxHeapSize                              := 2147483648                          {product}
java version "1.8.0_25"
Java(TM) SE Runtime Environment (build 1.8.0_25-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)

檢視棧的預設值,其中ThreadStackSize為棧記憶體的大小。

13:21 $ java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
     intx CompilerThreadStackSize                   = 0                                   {pd product}
     intx ThreadStackSize                           = 1024                                {pd product}
     intx VMThreadStackSize                         = 1024                                {pd product}
java version "1.8.0_25"
Java(TM) SE Runtime Environment (build 1.8.0_25-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)

Refer:

[1] Java JVM Run-time Data Areas

[2] Chapter 2. The Structure of the Java Virtual Machine

[3] JVM的相關知識整理和學習

[4] JVM 之 執行時資料區(更新)

[5] internal architecture of the Java Virtual Machine (JVM)

[6] Java記憶體管理原理及記憶體區域詳解

[7] Java中的堆和棧的區別

[8] JVM學習筆記

[9] JVM的記憶體區域劃分