java學習之路----內存的分析

java內存分析

          在java中,java語言對程序員做了一個美好的承諾,就是程序員無需去管理內存,因爲有GC,其實不然;
          
          1.垃圾回收並不會按照程序員的要求,進行垃圾回收
          2.垃圾回收並不會及時的清理內存,即使你需要額外的內存
          3.程序員不能對垃圾回收進行控制

一:內存區域的劃分

   1.程序計數器
       每一個java線程都有一個程序計數器來保存程序執行到了那一步,對於非Native方法,這個區域記錄就是字節碼的指令的地址,如果是Native方法,這個區域就爲空,此內存區是唯一一個在Java規範中沒有任何OutOfMemoryError情況的區域。
   2.jvm虛擬機棧

     我們通常粗略的把內存分爲兩個部分,一個就是棧,一個就是堆。而這裏jvm虛擬機棧就是我們常說的棧,它和程序計數器一樣,也是線程私有的,隨着線程的產生而產生,滅亡而滅亡,聲明週期和線程一樣。每個方法執行的時候都會產生一個棧幀,用來存儲局部變量表,動態鏈表,操作數,方法出入口等。方法的執行就是棧幀在JVM中出棧和入棧的過程。局部變量表存儲的是各種基本數據類型,boolean,int ,char,long.float,double,byte,short,還有對象的引用(不是對象本身,僅僅是一個引用指針),方法返回地址等。其中longdouble會佔用2個本地變量空間(32bit),其餘佔用1個。本地變量表在進入方法時進行分配,當進入一個方法時,這個方法需要在幀中分配多大的本地變量是一件完全確定的事情,在方法運行期間不改變本地變量表的大小。這個區域可能會拋出兩種異常,如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果VM棧可以動態擴展(VM Spec中允許固定長度的VM棧),當擴展時無法申請到足夠內存則拋出OutOfMemoryError異常。
  

  3.本地方法棧

     本地方法棧與JVM棧所發揮作用是類似的,只不過JVM棧爲虛擬機運行JVM原語服務,而本地方法棧是爲虛擬機使用到的Native方法服務。它的實現的語言、方式與結構並沒有強制規定,甚至有的虛擬機(譬如Sun Hotspot虛擬機)直接就把本地方法棧和VM棧合二爲一。和VM棧一樣,這個區域也會拋出StackOverflowErrorOutOfMemoryError異常。


  4.堆

     堆內存是內存中最重要的一塊,也是最有必要進行深究的一部分。因爲Java性能的優化,主要就是針對這部分內存的。所有的對象實例及數組都是在堆上面分配的(隨着JIT技術的逐漸成熟,這句話視乎有些絕對,不過至少目前還基本是這樣的),可通過-Xmx和-Xms來控制堆的大小。JIT技術的發展產生了新的技術,如棧上分配和標量替換,也許在不久的幾年裏,即時編譯會誕生及成熟,那個時候,“所有的對象實例及數組都是在堆上面分配的”這句話就應該稍微改改了。堆內存是垃圾回收的主要區域,所以在下文垃圾回收板塊會重點介紹,此處只做概念方面的解釋。在32位系統上最大爲2G,64位系統上無限制。可通過-Xms和-Xmx控制,-Xms爲JVM啓動時申請的最小Heap內存,-Xmx爲JVM可申請的最大Heap內存。

  5.方法區


     叫“方法區”可能認識它的人還不太多,如果叫永久代(Permanent Generation)它的粉絲也許就多了。它還有個別名叫做Non-Heap(非堆),但是VM Spec上則描述方法區爲堆的一個邏輯部分(原文:the method area is logically part of the heap),這個名字的問題還真容易令人產生誤解,我們在這裏就不糾結了。
方法區中存放了每個Class的結構信息,包括常量池、字段描述、方法描述等等。VM Space描述中對這個區域的限制非常寬鬆,除了和Java堆一樣不需要連續的內存,也可以選擇固定大小或者可擴展外,甚至可以選擇不實現垃圾收集。相對來說,垃圾收集行爲在這個區域是相對比較少發生的,但並不是某些描述那樣永久代不會發生GC(至少對當前主流的商業JVM實現來說是如此),這裏的GC主要是對常量池的回收和對類的卸載,雖然回收的“成績”一般也比較差強人意,尤其是類卸載,條件相當苛刻。   


二:垃圾回收


    1.我們爲什麼要進行垃圾的回收?

     因爲如果我們不進行垃圾的回收,隨着對象的越來越多的創建,內存會越來越小,這樣會帶來程序性能下降,甚至出現異常

     2.哪些垃圾需要回收


       有三個是不需要進行垃圾回收的:程序計數器、JVM棧、本地方法棧。因爲它們的生命週期是和線程同步的,隨着線程的銷燬,它們佔用的內存會自動釋放,所以只有方法區和堆需要進行GC。具體到哪些對象的話,簡單概況一句話:如果某個對象已經不存在任何引用,那麼它可以被回收。通俗解釋一下就是說,如果一個對象,已經沒有什麼作用了,就可以被當廢棄物被回收了。


   3.什麼時候進行垃圾回收

     根據一個經典的引用計數算法,每個對象添加一個引用計數器,每被引用一次,計數器加1,失去引用,計數器減1,當計數器在一段時間內保持爲0時,該對象就認爲是可以被回收得了。但是,這個算法有明顯的缺陷:當兩個對象相互引用,但是二者已經沒有作用時,按照常規,應該對其進行垃圾回收,但是其相互引用,又不符合垃圾回收的條件,因此無法完美處理這塊內存清理,因此Sun的JVM並沒有採用引用計數算法來進行垃圾回收。而是採用一個叫:根搜索算法,如下圖:

基本思想就是:從一個叫GC Roots的對象開始,向下搜索,如果一個對象不能到達GC Roots對象的時候,說明它已經不再被引用,即可被進行垃圾回收(此處 暫且這樣理解,其實事實還有一些不同,當一個對象不再被引用時,並沒有完全“死亡”,如果類重寫了finalize()方法,且沒有被系統調用過,那麼系統會調用一次finalize()方法,以完成最後的工作,在這期間,如果可以將對象重新與任何一個和GC Roots有引用的對象相關聯,則該對象可以“重生”,如果不可以,那麼就說明徹底可以被回收了),如上圖中的Object5、Object6、Object7,雖然它們3個依然可能相互引用,但是總體來說,它們已經沒有作用了,這樣就解決了引用計數算法無法解決的問題。


4.如何進行垃圾回收


     這裏只說算法,不去研究實現

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

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

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

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

當前商業虛擬機的垃圾收集都是採用“分代收集”(Generational Collecting)算法,這種算法並沒有什麼新的思想出現,只是根據對象不同的存活週期將內存劃分爲幾塊。一般是把Java堆分作新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法,譬如新生代每次GC都有大批對象死去,只有少量存活,那就選用複製算法只需要付出少量存活對象的複製成本就可以完成收集。  



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