java虛擬機的內存分配與回收機制


  分為4個方面來介紹內存分配與回收,分別是內存是如何分配的哪些內存需要回收在什么情況下執行回收如何監控和優化GC機制

  java GC(Garbage Collction)垃圾回收機制,是java與C/C++的主要區別之一。通過對jvm中內存進行標記,自主回收一些無用的內存。目前使用的最多的是sun公司jdk中的HotSpot,所以本文也以該jvm作為介紹的根本。

  1.Java內存區域

  在java運行時的數據取里,由jvm管理的內存區域分為多個部分:

  程序計數器(program counter register):程序計數器是一個比較校的內存單元,用來表示當前程序運行哪里的一個指示器。由於每個線程都由自己的執行順序,所以程序計數器是線程私有的,每個線程都要由一個自己的程序計數器來指示自己(線程)下一步要執行哪條指令。

  如果程序執行的是一個java方法,那么計數器記錄的是正在執行的虛擬機字節碼指令地址;如果正在執行的是一個本地方法(native方法),那么計數器的值為Undefined。由於程序計數器記錄的只是當前指令地址,所以不存在內存泄漏的情況,也是jvm內存區域中唯一一個沒有OOME(out of memory error)定義的區域。

  虛擬機棧(JVM stack):當線程的每個方法在執行的時候都會創建一個棧幀(Stack Frame)用來存儲方法中的局部變量、方法出口等,同時會將這個棧幀放入JVM棧中,方法調用完成時,這個棧幀出棧。每個線程都要一個自己的虛擬機棧來保存自己的方法調用時候的數據,因此虛擬機棧也是線程私有的。

  虛擬機棧中定義了兩種異常,如果線程調用的棧深度大於虛擬機允許的最大深度,拋出StackOverFlowError,不過虛擬機基本上都允許動態擴展虛擬機棧的大小。這樣的話線程可以一直申請棧,直到內存不足的時候,會拋出OOME(out of memory error)內存溢出。

  本地方法棧(Native Method Stack):本地方法棧與虛擬機棧類似,只是本地方法棧存放的棧幀是在native方法調用的時候產生的。有的虛擬機中會將本地方法棧和虛擬棧放在一起,因此本地方法棧也是線程私有的。

  堆(Heap):堆是java GC機制中最重要的區域。堆是為了放置“對象的實例”,對象都是在堆區上分配內存的,堆在邏輯上連續,在物理上不一定連續。所有的線程共用一個堆,堆的大小是可擴展的,如果在執行GC之后,仍沒有足夠的內存可以分配且堆大小不可再擴展,將會拋出OOME。

  方法區(Method Area):又叫靜態區,用於存儲類的信息、常量池等,邏輯上是堆的一部分,是各個線程共享的區域,為了與堆區分,又叫非堆。在永久代還存在時,方法區被用作永久代。方法區可以選擇是否開啟垃圾回收。jvm內存不足時會拋出OOME。

  直接內存(Direct Memory):直接內存指的是非jvm管理的內存,是機器剩余的內存。用基於通道(Channel)和緩沖區(Buffer)的方式來進行內存分配,用存儲在JVM中的DirectByteBuffer來引用,當機器本身內存不足時,也會拋出OOME。

  舉例說明:Object obj = new Object();

  obj表示一個本地引用,存儲在jvm棧的本地變量表中,new Object()作為一個對象放在堆中,Object類的類型信息(接口,方法,對象類型等)放在堆中,而這些類型信息的地址放在方法區中。

  這里需要知道如何通過引用訪問到具體對象,也就是通過obj引用如何找到new出來的這個Object()對象,主要有兩種方法,通過句柄通過直接指針訪問。

  通過句柄

  在java堆中會專門有一塊區域被划分為句柄池,一個引用的背后是一個對象實例數據(java堆中)的指針和對象類型信息(方法區中)的指針,而這兩個指針都是在java堆上的。這種方法是優勢是較為穩定,但是速度不是很快。

  通過直接指針

  一個引用背后是一個對象的實例數據,這個實例數據里面包含了“到對象類型信息的指針”。這種方式的優勢是速度快,在HotSpot中用的就是這種方式。

 

  2.內存是如何分配和回收的

  內存分配主要是在堆上的分配,如前面new出來的對象,放在堆上,但是現代技術也支持在棧上分配,較為少見,本文不考慮。分配內存與回收內存的標准是八個字:分代分配,分代回收。那么這個代是什么呢?

  jvm中將對象根據存活的時間划分為三代:年輕代(Young Generation)、年老代(Old Generation)和永久代(Permannent Generation)。在jdk1.8中已經不再使用永久代,因此這里不再介紹。

  

 

  年輕代:又叫新生代,所有新生成的對象都是先放在年輕代。年輕代分三個區,一個Eden區,兩個Survivor區,一個叫From,一個叫To(這個名字是動態變化的)。當Eden中滿時,執行Minor GC將消亡的對象清理掉,仍存活的對象將被復制到Survivor中的From區,清空Eden。當這個From區滿的時候,仍存活的對象將被復制到To區,清空From區,並且原From區變為To區,原To區變為From區,這樣的目的是保證To區一直為空。當From區滿無對象可清理或者From-To區交換的次數超過設定(HotSpot默認為15,通過-XX:MaxTenuringThreashold控制)的時候,仍存活的對象進入老年代。年輕代中Eden和Servivor的比例通過-XX:SerivorRation參數來配置,默認為8,也就時說Eden:From:To=8:1:1。年輕代的回收方式叫做Minor GC,又叫停止-復制清理法。這種方法在回收的時候,需要暫停其他所有線程的執行,導致效率很低,現在雖然有優化,但是僅僅是將停止的時間變短,並沒有徹底取消這個停止。

  年老代:年老代的空間較大,當年老代內存不足時,將執行Major GC也叫Full GC。如果對象比較大,可能會直接分配到老年代上而不經過年輕代。用-XX:pertenureSizeThreashold來設定這個值,大於這個的對象會直接分配到老年代上。

  3.垃圾收集器

  在GC機制中,起作用的是垃圾收集器。HotSpot1.6中使用的垃圾收集器如下(有連線表示有聯系):

  

  Serial收集器:新生代(年輕代)收集器,使用停止-復制算法,使用一個線程進行GC,其他工作線程暫停。

  ParNew收起:新生代收集器,使用停止-復制算法,Serial收集器的多線程版,用多個線程進行GC,其他工作線程暫停,關注縮短垃圾收集時間。

  Parallel Scavenge收集器:新生代收集器,使用停止-復制算法,關注CPU吞吐量,即運行用戶代碼的時間/總時間。

  Serial Old收集器:年老代收集器,單線程收集器,使用標記-整理算法(整理的方法包括sweep清理和compact壓縮,標記-清理是先標記需要回收的對象,在標記完成后統一清楚標記的對象,這樣清理之后空閑的內存是不連續的;標記-壓縮是先標記需要回收的對象,把存活的對象都向一端移動,然后直接清理掉端邊界以外的內存,這樣清理之后空閑的內存是連續的)。

  Parallel Old收集器:老年代收集器,多線程收集器,使用標記-整理算法(整理的方法包括summary匯總和compact壓縮,標記-壓縮與Serial Old一樣,標記-匯總是將幸存的對象復制到預先准備好的區域,再清理之前的對象)。

  CMS(Concurrent Mark Sweep)收集器:老年老代收集器,多線程收集器,關注最短回收時間停頓,使用標記-清除算法,用戶線程可以和GC線程同時工作。

   G1收集器:JDK1.7中發布,使用較少,不作介紹。

 

  Java GC是一個非常復雜的機制,想要詳細說清楚他需要很多時間,如有錯誤懇請指正。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM