java學習-----jvm的內存分配及運行機制

VM運行時數據區域java

根據《Java虛擬機規範(第二版)》的規定,JVM包括下列幾個運行時區域:算法

咱們思考幾個問題:數組

1.jVM是怎麼運行的?緩存

2.JVM運行時內存是怎麼分配的?多線程

3.咱們寫的java代碼(類,對象,方法,常量,變量等等)最終存放在哪一個區?框架

VM運行時數據區域:jvm

1.程序計數器(program Counter Register):測試

     是一塊較小的內存空間,它的做用能夠看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各類虛擬機可能會經過一些更高效的 方式去實 現),字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個 計數器來完成。優化

         因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核) 只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存 儲,咱們稱這類內存區域爲「線程私有」的內存。spa

         若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Natvie方法,這個計數器值則爲空 (Undefined)。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。

2.java虛擬機棧(Java Virtual Machine Stacks):

裏面存放的是本地變量表(存放了編譯期可知的各類標量類型:Boolean,byte,char,short,int,float,long,double)、對象的引用(不是對象自己,僅僅是引用指針)、方法返回地址等。

虛擬棧中規定了兩種異常情況:

  1. 若是線程請求的深度大於虛擬機所容許的深度,就會拋出stackoverflowerror異常,也就是棧溢出異常。在使用遞歸的調用方法的狀況下,很容易拋出這個異常。
  2. 若是VM棧能夠動態擴展,當擴展的時候沒法申請到足夠的內存空間,則拋出OutOfMemoryError異常,內存溢出。

3.本地方法棧(native method stacks)

   這塊區域在jvm運行內存中職責就相對比較少了。只是執行Native 方法。若是這個區的內存不足也是會拋出StackOverflowError 和 OutOfMemoryError 異常。

4.java 堆

這塊區域是jvm中最大的一塊區域了,java堆是被全部線程所共享的,也是GC主要的回收區,在jvm啓動的時候就建立了。java堆的惟一的目的就是存放對象實例(全部new出來的對象)絕大部分對象的實例都是在這塊區域分配。

從圖中能夠看出heap中還能夠分爲新生代(Young Generation)和老年代(Old Generation)。下面看這個圖:

  • 新 生代:GC每隔一段時間就會對新生代進行回收,在分配對象遇到內存不足的時候,先對新生代進行GC,當新生代GC後,沒法知足內存空間的分配需求,纔會對 整個對空間和方法區進行GC(FULL GC).而新生代又能夠分爲:一個Eden Space和兩塊相同大小的Survivor Space(s0,s1或From Survivor 和 To Survivor)正式圖中所看到的。新生代中的E區和S區又有不一樣的職責。
    • E區:GC觸發比較頻繁的區域,存儲的是新new的對象,幾乎全部對象都通過E區,若是屢次GC仍然有存活的對象,就把存活的對象放到S區。
    • S區:S區做爲Eden區和old(老年代)的緩存。它是能夠向老年代轉移活動對象的實例.
  • 老年代:用於存放屢次新生代GC仍然活着的對象,如緩存對象。新建的對象也有可能直接進入老年代,主要有兩種狀況:①.大對象,可經過啓動參數設置-XX:PretenureSizeThreshold=1024(單位爲字節,默認爲0)來表明超過多大時就不在新生代分配,而是直接在老年代分配。②.大的數組對象,切數組中無引用外部對象。
  • 無 論對java堆如何劃分,目的是爲了更好的回收內存,或者是更快的分配內存;java的堆在物理空間上處於不連續的空間,但在邏輯上是連續的便可。虛擬機 堆內存空間是可擴展到的,能夠經過-Xmx和-Xms控制,若是堆上沒法分配內存空間,而且堆也沒法再擴展到額時候,將會拋出 OutOfMemoryError異常。  

5.方法區(Mehod Area)

   方法區和堆同樣也是線程共享的區域,它主要是用於存儲被虛擬機加載的類信息、常量、靜態變量、及時編譯器編譯後的代碼等數據,不屬於heap的一部分。 相對來講,GC行爲在這個區域是相對比較少發生的,但並非某些描述那樣永久代不會發生GC。對於sun公司的HotSpot虛擬機來講。gc也會對這塊 區域進行垃圾回收,這裏的回收主要是常量池的回收和對類的卸載。

     若是細分方法區的裏面有爲運行時常量池(Runtime Constant Pool),它主要存儲Class文件中的版本、字段、方法、接口等描述信息。還 有一項信息是常量表(constant_pool table)用於存放編譯期已可知的常量,這部份內容將在類加載後進入方法區(永久代)存放。可是java語言並不要求常量必定只有編譯期預先置入 Class的常量表的內容才能進入方法區常量池,運行期間才能夠將新內容放入常量池(最典型的是String.intern()方法)

實戰OutOfMemoryError:

除了程序計數器,其餘在VMSpec中都描述了產生OutOfMemoryError(下稱OOM)的情形,那咱們就實戰模擬一下,經過幾段簡單的代碼,令對應的區域產生OOM異常以便加深認識,同時初步介紹一些與內存相關的虛擬機參數。

1.Java堆:

java 堆存放的是對象實例,所以只要不斷創建對象,而且保證GCRoots到對象之間有可達路徑便可產生OOM異常。測試中限制Java堆大小爲20M,不可擴 展,經過參數-XX:+HeapDumpOnOutOfMemoryError讓虛擬機在出現OOM異常的時候Dump出內存映像以便分析。

代碼:

package com.lp.ecjtu; import java.util.ArrayList; public class JVMTestDemo_heap { public static void main(String[]args){ java.util.List<OOMObject>list = new ArrayList<OOMObject>(); while(true){ list.add(new OOMObject()); } } } /** * VMArgs:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError * @author Administrator * */ class OOMObject{ }
運行結果:
java.lang.OutOfMemoryError:Javaheapspace Dumpingheaptojava_pid3404.hprof... Heapdumpfilecreated[22045981bytesin0.663secs]

 

 

 

垃圾收集GC(GarbageCollection,下文簡稱GC):

總 結下:其中程序計數器、VM棧、本地方法棧隨線程而生,隨線程而滅;棧中的幀隨着方法的進入退出,有條不紊的進行的出棧和入棧操做;每個幀中分配多少內 存,基本上是在Class文件生成時就已知的(可能會由JIT動態晚期編譯進行一些優化,但大致上能夠認爲是編譯期可知
的),所以這幾個區域的內存的分配和回收具有很高的肯定性,所以在這幾個區域不須要過多考慮回收問題。而java堆和方法區(包括運行時常量池)則不同,咱們必須等到程序實際運行期間才能知道會建立那些對象,這部份內存的回收和分配是動態的。

GC的歷史遠遠比java來的久,在1960年誕生於MIT的Lisp(是一門真正的使用內存冬天分配和垃圾回收集)的語言。當Lisp在胚胎時期,人們在GC須要作的3件事情:

  1. 哪些內存須要回收
  2. 何時須要回收
  3. 怎麼樣回收

方法區的回收:

      方法區即後文提到的永久代,不少人認爲永久代是沒有GC的,《Java虛擬機規範》中確實說過能夠不要求虛擬機在這區實現GC,並且這區GC的「性價比」通常比較低:在堆中,尤爲是在新生代,常規應用進行一次GC能夠通常能夠回收70%~95%的空間,而永久代的GC效率遠小於此。雖然VMSpec不要求,但當前生產中的商業JVM都有實現永久代的GC,主要回收兩部份內容:廢棄常量與無用類。這兩點回收思想與Java堆中的對象回收很相似,都是搜索是否存在引
用,常量的相對很簡單,與對象相似的斷定便可。而類的回收則比較苛刻,須要
知足下面3個條件:
   1.該類全部的實例都已經被GC,也就是JVM中不存在該Class的任何實例。
   2.加載該類的ClassLoader已經被GC。
   3.該類對應的java.lang.Class對象沒有在任何地方被引用,如不能在任何地方經過反射訪問該類的方法。

在大量使用反射、動態代理、CGLib等bytecode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都須要JVM具有類卸載的支持以保證
永久代不會溢出。

垃圾收集算法

     最基礎的蒐集算法是「標記-清除算法」(Mark-Sweep),如它的名字同樣,算法分層「標記」和「清除」兩個階段,首先標記出全部須要回收的對象,而後回收全部須要回收的對象,整個過程其實前一節講對象標記斷定的時候已經基本介紹完了。說它是最基礎的收集算法緣由是後續的收集算法都是基於這種思路並優化其缺點獲得的。它的主要缺點有兩個,一是效率問題,標記和清理兩個過程效率都不高,二是空間問題,標記清理以後會產生大量不連續的內存碎片,空間碎片太多可能會致使後續使用中沒法找到足夠的連續內存而提早觸發另外一次的垃圾蒐集動做。

    爲了解決效率問題,一種稱爲「複製」(Copying)的蒐集算法出現,它將用內存劃分爲兩塊,每次只使用其中的一塊,當半區內存用完了,僅將還存活的對象複製到另一塊上面,而後就把原來整塊內存空間一次過清理掉。這樣使得每次內存回收都是對整個半區的回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存就能夠了,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,未免過高了一點。

   如今的商業虛擬機中都是用了這一種收集算法來回收新生代,IBM有專門研究代表新生代中的對象98%是朝生夕死的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的eden空間和2塊較少的survivor空間,每次使用eden和其中一塊survivor,當回收時將eden和survivor還存活的對象一次過拷貝到另一塊survivor空間上,而後清理掉eden和用過的survivor。SunHotspot虛擬機默認eden和survivor的大小比例是8:1,也就是每次只有10%的內存是「浪費」的。固然,98%的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有10%之內的對象存活,當survivor空間不夠用時,須要依賴其餘內存(譬如老年代)進行分配擔保(Handle Promotion)。

    複製收集算法在對象存活率高的時候,效率有所降低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保用於應付半區內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。所以人們提出另一種「標記-整理」(Mark-Compact)算法,標記過程仍然同樣,但後續步驟不是進行直接清理,而是令全部存活的對象一端移動,而後直接清理掉這端邊界之外的內存。

   當前商業虛擬機的垃圾收集都是採用「分代收集」(Generational Collecting)算法,這種算法並無什麼新的思想出現,只是根據對象不一樣的存活週期將內存劃分爲幾塊。通常是把Java堆分做新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法,譬如新生代每次GC都有大批對象死去,只有少許存活,那就選用複製算法只須要付出少許存活對象的複製成本就能夠完成收集。

相關文章
相關標籤/搜索