1、JVM內存區域劃分
大多數 JVM 將內存區域劃分爲 Method Area(Non-Heap),Heap,Program Counter Register, Java Method Stack,Native Method Stack和Direct Memomry(注意 Directory Memory 並不屬於 JVM 管理的內存區域)。前三者通常譯爲:方法區、堆、程序計數器。但不一樣的資料和書籍上對於後三者的中文譯名不盡相同,這裏將它們分別譯做:Java 方法棧、原生方法棧和直接內存區。對於不一樣的 JVM,內存區域劃分可能會有所差別,好比 Hot Spot 就將 Java 方法棧和原生方法棧合二爲一,咱們能夠統稱爲方法棧(Method Stack)。
首先咱們熟悉一下一個通常性的 Java 程序的工做過程。一個 Java 源程序文件,會被編譯爲字節碼文件(以 class 爲擴展名),而後告知 JVM 程序的運行入口,再被 JVM 經過字節碼解釋器加載運行。那麼程序開始運行後,都是如何涉及到各內存區域的呢?
歸納地說來,JVM 每遇到一個線程,就爲其分配一個程序計數器、Java 方法棧和原生方法棧。當線程終止時,二者所佔用的內存空間也會被釋放掉。棧中存儲的是棧幀,能夠說每一個棧幀對應一個「運行現場」。在每一個「運行現場」中,若是出現了一個局部對象,則它的實例數據被保存在堆中,而類數據被保存在方法區。
2、指令、方法與屬性
在講各部分以前,咱們首先要搞清楚的是什麼是數據以及什麼是指令。而後要搞清楚對象的方法和對象的屬性分別保存在哪裏。
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數據內存區。
3、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
棧,以下圖所示:
4、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
5、The pc Register 程序計數器寄存器
JVM支持多個線程同時運行。每一個JVM都有本身的程序計數器。在任何一個點,每一個JVM線程執行單個方法的代碼,這個方法是線程的當前方法。若是方法不是native的,程序計數器寄存器包含了當前執行的JVM指令的地址,若是方法是 native的,程序計數器寄存器的值不會被定義。 JVM的程序計數器寄存器的寬度足夠保證能夠持有一個返回地址或者native的指針。
6、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,而不是堆中。
7、Java Method Stack Java 方法棧 與 Native Method Stack 原生方法棧
第七章內容來源:http://blog.csdn.net/poechant/article/details/7289093
Java 方法棧也是線程私有的,每一個 Java 方法棧都是由一個個棧幀組成的,每一個棧幀是一個方法運行期的基礎數據結構,它存儲着局部變量表、操做數棧、動態連接、方法出口等信息。當線程調用調用了一個 Java 方法時,一個棧幀就被壓入(push)到相應的 Java 方法棧。當線程從一個 Java 方法返回時,相應的 Java 方法棧就彈出(pop)一個棧幀。 java
其中要詳細介紹的是局部變量表,它保存者各類基本數據類型和對象引用(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 句柄方式 bootstrap
圖2 直接方式 數組
原生方法棧與 Java 方法棧相相似,這裏再也不贅述。 緩存
8、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 向操做系統發送消息,說「內存用完了,我還給你」
行結束。
9、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 有哪些調整參數?
答:很是多,本身去找,堆內存、棧內存的大小均可以定義,甚至是堆內存的三個部分、新生
代的各個比例都能調整。