Java內存結構、虛擬機垃圾回收和gc算法

http://blog.csdn.net/tjiyu/article/details/53915869

一、Java虛擬機

1. 什麼是Java虛擬機

main函數被執行時,java虛擬機就啓動了。
你運行幾個application就有幾個java.exe/javaw.exe。或者更加具體的說,你運行了幾個main函數就啓動了幾個java應用,同時也啓動了幾個java的虛擬機。
總結:
什麼是java虛擬機,什麼是java的虛擬機實例?
java的虛擬機相當於我們的一個java類,而java虛擬機實例,相當我們new一個java類,不過java虛擬機不是通過new這個關鍵字而是通過java.exe或者javaw.exe來啓動一個虛擬機實例。

2. 生命週期

java的虛擬機種有兩種線程,一種叫叫守護線程,一種叫非守護線程,main函數就是個非守護線程,虛擬機的gc就是一個守護線程。java的虛擬機中,只要有任何非守護線程還沒有結束,java虛擬機的實例都不會退出,所以即使main函數這個非守護線程退出,但是由於在main函數中啓動的匿名線程也是非守護線程,它還沒有結束,所以jvm沒辦法退出
總結:
java虛擬機的生命週期,當一個java應用main函數啓動時虛擬機也同時被啓動,而只有當在虛擬機實例中的所有非守護進程都結束時,java虛擬機實例才結束生命。

3.java虛擬機的體系結構

  • 虛擬機結構類似操作系統內存佈局,

和操作系統內存的對比

  • 基於操作系統的角度,jvm就是個該死的java.exe/javaw.exe,也就是一個應用,而基於class文件來說,jvm就是個操作系統,而jvm的方法區,也就相當於操作系統的硬盤區,所以你知道我爲什麼喜歡叫他permanent區嗎,因爲這個單詞是永久的意思,也就是永久區,我們的磁盤就是不斷電的永久區
  • 而java棧和操作系統棧是一致的,無論是生長方向還是管理的方式,
  • 堆嘛,雖然概念上一致目標也一致,分配內存的方式也一直(new,或者malloc等等),但是由於他們的管理方式不同,jvm是gc回收,而操作系統是程序員手動釋放,所以在算法上有很多的差異,

內存對比

  • 計算機上的pc寄存器是計算機上的硬件,本來就是屬於計算機,計算機用pc寄存器來存放“僞指令”或地址,而相對於虛擬機,pc寄存器它表現爲一塊內存(一個字長,虛擬機要求字長最小爲32位)
  • 虛擬機的pc寄存器的功能也是存放僞指令,更確切的說存放的是將要執行指令的地址,它甚至可以是操作系統指令的本地地址,當虛擬機正在執行的方法是一個本地方法的時候,jvm的pc寄存器存儲的值是undefined,所以你現在應該很明確的知道,虛擬機的pc寄存器是用於存放下一條將要執行的指令的地址(字節碼流)。
  • 當一個classLoder啓動的時候,classLoader的生存地點在jvm中的堆,然後它會去主機硬盤上將A.class裝載到jvm的方法區,方法區中的這個字節文件會被虛擬機拿來new A字節碼(),然後在堆內存生成了一個A字節碼的對象,然後A字節碼這個內存文件有兩個引用一個指向A的class對象,一個指向加載自己的classLoader,
    指向

  • 【可以不背這塊兒】方法區中的字節碼內存塊,除了記錄一個class自己的class對象引用和一個加載自己的ClassLoader引用之外,還記錄了什麼信息呢??我們還是看圖,然後我會講給你聽,聽過一遍之後一輩子都不會忘記。
    這裏寫圖片描述

虛擬機運行流程【重點】

首先,當一個程序啓動之前,它的class會被類裝載器裝入方法區(不好聽,其實這個區我喜歡叫做Permanent區),執行引擎讀取方法區的字節碼自適應解析,邊解析就邊運行(其中一種方式),然後pc寄存器指向了main函數所在位置,虛擬機開始爲main函數在java棧中預留一個棧幀(每個方法都對應一個棧幀),然後開始跑main函數,main函數裏的代碼被執行引擎映射成本地操作系統裏相應的實現,然後調用本地方法接口,本地方法運行的時候,操縱系統會爲本地方法分配本地方法棧,用來儲存一些臨時變量,然後運行本地方法,調用操作系統APIi等等。

二、Java內存

1. JVM內存結構

內存模型

  1. 類裝載器:作用是在JVM啓動時或某個Class要運行的時候把類裝載到JVM中。
  2. 運行時數據區(內存區域):這是JVM在運行時操作的內存區域,


      • Java堆:線程共享,存放Java對象,所有的對象(包括數組,但Class對象除外)數據實際存放地方。堆是程序級別,每一個Java程序共享一個堆(所以存在多線程訪問堆內存同步問題)。
      • 分爲:Old區+ Young區,Young區又劃分爲:Eden + Survivor,Survivor又劃分爲:From + To (From 、To 大小相等,這些大小都能手動設置)

    1. 棧又分爲兩種,一是Java方法棧,一是本地方法棧(有的JVM這兩者是合在一起的,不過這裏還是討論邏輯上)。另外,每個線程都有各自的程序計數器,也是棧格式的。
      • 本地方法棧:線程私有,與Java棧類似,不過用於執行C/C++的native方法。
      • Java棧:線程私有,每運行一個方法就創建一個棧幀,用於存儲局部變量表、操作棧、方法返回值、常量引用等(如果是基本類型,則存的是值)。
      • 程序計數器(PC):線程私有,用於保存當前線程執行的內存地址。每個線程都有各自的程序計數器,也是棧格式的。
    2. 非堆
      主要用來存儲加載的類的信息、常量、靜態變量等,因爲主要是方法,所以也叫方法區;也因爲gc基本不涉及這區,也叫永久代。
      是程序共享區域,從這一點可以看出,JVM規範把他描述爲堆的邏輯一部分是有一定的道理,雖然它有個非堆(Non Heap)名稱。

      • 方法區(永久代):線程共享,裏面存儲了類結構信息、常量池以及靜態變量,即時編譯後的代碼等,一般不進行GC,因此也被稱爲永久代。方法區還包含一個運行時常量池。
      • 堆與非堆的區別是,堆是供給程序使用,而非堆是供給JVM使用的。
        運行時內存
  3. 執行引擎:負責執行class文件中包含的字節碼指令

  4. 本地方法接口:主要是調用C/C++實現的本地方法
  5. 總結
    1. 棧線程私有,存放局部變量;堆程序共享,存放對象實例數據;非堆主要存放類信息(Class對象)。這是Java內存最主要的三塊內存,而直接內存,如果沒用到NIO是不會操作到的。
    2. 數據存放地方
      局部變量:棧中,包括基本類型(存放的是值)、對象引用、返回地址;
      類變量:方法區(非堆),類變量算是類信息一部分;
      字符串和基本類型常量:常量池(事實上,常量池已被放到堆中,不過我們姑且將常量池單獨在邏輯上拿出來);
      Class對象:方法區(非堆);
      new對象:引用放到棧中,對象數據放在堆中;
      這些存放地方可能因爲Java不斷髮展改變而不一樣,但是邏輯上大概是這個意思,還有細節地方可能還有所不同(比如Eden可能還存放其他信息),如果想追究具體是什麼情況的話,可以查看最新的jdk說明文檔。
      本節主要講的是JVM運行時數據區,JVM內存各個區域的劃分以及作用。

2 堆內存中各代分佈

堆內存
Java堆主要分爲3代:
- 年輕代:這裏是所有新對象產生的地方。年輕代被分爲3個部分——Enden區(伊甸園區,新對象的出生地,這部分均爲連續內存,分配快速)和兩個Survivor區(From和to)。當Eden區被對象填滿時,就會對Eden區和Survivor From區執行Minor GC(Young GC),並把所有存活下來的對象轉移到Survivor To區,然後把from區變成下次GC的to區。這樣在一段時間內,總會有一個空的Survivor區。經過多次GC週期後,仍然存活下來的對象會被轉移到年老代內存空間。通常這是在年輕代有資格提升到年老代前通過設定年齡閾值來完成的。需要注意,Survivor的兩個區是對稱的,沒先後關係,from和to是相對的。
- 年老代:與年輕代相比,年老代裏面的對象存活時間較長,大小也較大(較大的對象可能直接進入年老代)。當年老代被空間佔滿時,會觸發Major GC(Full GC),不僅對年老代進行GC,對年輕代和永久代也進行GC,釋放掉已經沒有被引用的對象。
- 永久代:永久代即上文提及的方法區,由於方法區放的都是靜態文件,故GC影響並不是很大。

  • 爲什麼要這樣劃分?
    因爲性能,每次GC可以按照不同區域GC,加快垃圾回收速度。不過也因此,如果有更好的GC算法,可能劃分就不一樣了。
  • 新建的對象存放到Eden區,From/To存放經過一次及以上GC的對象,若經過n次(可設置,通常爲0,這裏的0意思是GC檢查時是0移到From/To區,並且該值+1,不是0移到Old區),對象每次GC若能存活(每次GC,Eden區清空),移到From/To區,以後都是從From到To再到From跳來跳去,經過n次,變成“老對象”就會被移到Old區。
  • 堆是共享空間,每次分配空間要加鎖。但是有的JVM會爲每個線程分配一個TLAB空間,這樣就不用加鎖(不過這種僅適用於小對象,大的還是直接分配在堆上)。
  • 堆在物理上是可以分散的,只要在邏輯上連續的就可以,大小可以是固定的,也可以是可擴展的,主流的JVM採用的都是可擴展的,可以用-Xmx -Xms控制(詳見JVM優化一節)。
  • Old區、Eden區的內存並不一定會被填滿,相反,一般情況下,不可能被填滿,而是達到一定的值就會啓動GC(詳見GC一節)。

三、JVM垃圾收集算法(gc算法)

  1. JVM垃圾收集算法有四種:標記-清除算法、複製算法、標記-整理算法、分代收集算法
  2. 觸發gc的條件
    (1).Minor GC:當Eden區滿了或達到指定值,觸發Minor GC,清理Eden區和Survivor區;
    (2).Major GC:當Old區滿了或達到指定值,觸發Major GC,清理Old區(一般也會觸發Minor GC,但不一定,各個GC回收器策略不一樣,耗時一般是Minor GC的10倍,因爲採用的回收方法不一樣)
    (3).Full GC:當前兩者觸發,但仍內存不足,或者方法區滿了,則會觸發Full GC,清理內存所有對象空間(包括方法區)。
  3. 這三者並沒有那麼明確的區分,因爲,一般Old區對象都有Young區的引用,所以一般Major GC會引發Minor GC,從而減輕Major GC負擔,而Full GC也會觸發Major GC,你要關心的不是這三者的區別,而是當什麼條件下會觸發GC,以及相應的GC算法,以及執行這些算法GC的時候對程序的影響。
    堆內存

1.複製算法:

  • 複製算法將可用的內存分成兩樣大小的兩塊,每次只使用其中一塊內存。當這塊內存用完之後,就把還存活的對象複製到另外一塊上面,然後,把這塊清空。
  • 即針對Young區,依次掃描這個區的所有可達對象(如何確定可達對象,請參考前一節),掃描只掃描GC維護的一張對象關係有向圖(以下稱爲可達對象鏈),只要在這個圖上的,就將這個對象複製到另一個區域(實現這種算法需要堆內存保留一個與Young區大小一樣的區域),原先的Eden區對象,移到From區,From區移到To區,有必要的話,將對象移到Old區(區域劃分,見Java內存結構),原先內存全部清空,作爲下一次GC用。
  • 【優化】隨着時間的積累,現在使用的複製算法的虛擬機,不再是把內存分爲1:1的兩塊。因爲98%的對象是壽命很短的,創建之後,很快就被回收了,存活下來的只有2%,所以,用來存儲存活對象的內存區,可以小一些。現在的商業虛擬機是把可用內存分爲一個較大的Eden空間和兩個較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當回收時,把Eden和Survivor中的存活對象一次複製到另一塊Survivor內存區上,然後把Eden和剛纔用過的Survivor空間清空。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,這樣,每次新產生的對象可以使用90%的內存空間。
  • 優缺點:只需遍歷可達的對象,不用訪問不可達對象,遍歷少,但需要巨大的複製成本和較多的內存。

2.標記-清除算法:

遍歷可達對象鏈,對這些對象進行標記,下次,遍歷整個區域的對象,沒有標記的清除。
優缺點:不需要額外空間,但是遍歷空間花費大,而且會產生大量內存碎片

3.標記-整理算法

前二者的結合,遍歷可達對象鏈,標記這些對象,再按順序將這些對象合併到一塊內存上,比如,有1、2、3、4、5、6、7、8塊連續內存對象,其中2,5,8是可達鏈上對象,標記整理算法的做法是:先標記他們,再從1開始遍歷,1不是,到2,2是,將2複製到1,2標記清除,再遍歷3,4,不是,遍歷5,是,將5複製到2,依次如此,最後得到1,2,3有用內存,後面內存就被清除了。
優缺點:相對標記清除來說,沒有了內存碎片,但是遍歷花費仍然很大

4.分代收集算法

分代收集算法是根據對象的存活週期的不同,將內存劃分爲幾塊。當前的商業虛擬機的垃圾收集都採用了該算法。一般把Java堆分成新生代(年輕代)和老年代(年老代)。這樣就可以根據各年代中對象的存活週期來選擇最合適的收集算法了。新生代,由於只有少量的對象能存活下來,所以選用“複製算法”,只需要付出少量存活對象的複製成本。老年代,由於對象的存活率高,沒有額外的空間分擔,就必須使用“標記-清除”或“標記-整理”算法。

5.使用範圍

實際上,GC根據堆內存空間不同區域,採用不同的算法回收:
Young區:存活的對象較少,複製代價小,但次數多,採用複製收集算法;
Old區和方法區:對象存活較多,次數少,較慢,採用標記清除或標記整理算法。

http://blog.csdn.net/a327369238/article/details/52120272
http://blog.csdn.net/zhuangyalei/article/details/51585852
http://blog.csdn.net/a327369238/article/details/52132579

四、GC回收器種類

GC回收器的衡量指標:

1.Throughput(吞吐量):所有沒有花在執行GC上的時間佔總運行時間的比重。
2.Pauses(暫停):當GC在運行時程序的暫停次數。或者是在感興趣的暫停次數中,暫停的平均時長和最大時長。
3.Footprint(足跡?):當前使用的堆內存大小(算法所有花費的額外空間)。
4.Promptness(及時性):不再使用的對象多久能被清除掉並釋放其內存。

1. 串行垃圾回收器

Serial收集器/Serial Old收集器,是單線程的,使用“複製”算法。當它工作時,必須暫停其它所有工作線程。特點:簡單而高效。對於運行在Client模式下的虛擬機來說是一個很好的選擇。
1.Serial:只對新生代使用;2.Serial Old:只對老年代使用,採用的算法不一樣(一般作爲CMS的替補)

2. 並行垃圾回收器

ParNew收集器
ParNew收集器,是Serial收集器的多線程版。是運行在Server模式下的虛擬機中首選的新生代收集器。除了Serial收集器外,目前只有它能與CMS收集器配合工作。
Parallel Scavenge收集器/Parallel Old收集器
Parallel Scavenge收集器,也是使用“複製”算法的、並行的多線程收集器。這些都和ParNew收集器一樣。但它關注的是吞吐量(CPU用於運行用戶代碼的時間與CPU總消耗時間的比值),而其它收集器(Serial/Serial Old、ParNew、CMS)關注的是垃圾收集時用戶線程的停頓時間。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。

3. 併發標記掃描垃圾回收器(CMS)

  CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,使用“標記-清除”算法。
  CMS收集器分4個步驟進行垃圾收集工作:
  1、初始標記   2、併發標記   3、重新標記   4、併發清除
  其中“初始標記”、“重新標記”是需要暫停其它所有工作線程的。

多線程,標記清理(Full GC的時候用)通過JVM命令 -XX:+UseConcMarkSweepGC使用, 主要用於老生代,策略爲:
年老代只有兩次短暫停,其他時間應用程序與收集線程併發的清除。採用兩次短暫停來替代標記整理算法的長暫停,它的收集週期:
初始標記(CMS-initial-mark) -> 併發標記(CMS-concurrent-mark) -> 重新標記(CMS-remark)-> 併發清除(CMS-concurrent-sweep) ->併發重設狀態等待下次CMS的觸發(CMS-concurrent-reset)。
它的主要適合場景是對響應時間的重要性需求大於對吞吐量的要求,能夠承受垃圾回收線程和應用線程共享處理器資源,並且應用中存在比較多的長生命週期的對象的應用。但CMS收集算法在最爲耗時的內存區域遍歷時採用多線程併發操作,對於服務器CPU資源不夠的情況下,其實對性能是沒有提升的,反而會導致系統吞吐量的下降;

4. G1垃圾回收器

G1(Garbage First)收集器,基於“標記-整理”算法,可以非常精確地控制停頓。
適用於堆內存很大的情況,它將對內存分割成不同的區域,並且併發的對其進行回收,回收後對剩餘內存壓縮,標記整理,服務器端適用。

五、方法區補充【超綱】

這裏寫圖片描述
1. 類信息:修飾符(public final)
是類還是接口(class,interface)
類的全限定名(Test/ClassStruct.class)
直接父類的全限定名(java/lang/Object.class)
直接父接口的權限定名數組(java/io/Serializable)
也就是 public final class ClassStruct extends Object implements Serializable這段描述的信息提取
2. 字段信息:修飾符(pirvate)
字段類型(java/lang/String.class)
字段名(name)
也就是類似private String name;這段描述信息的提取
3. 方法信息:修飾符(public static final)
方法返回值(java/lang/String.class)
方法名(getStatic_str)
參數需要用到的局部變量的大小還有操作數棧大小(操作數棧我們後面會講)
方法體的字節碼(就是花括號裏的內容)
異常表(throws Exception)
也就是對方法public static final String getStatic_str ()throws Exception的字節碼的提取
4. 常量池:
- 4.1.直接常量:
1.1CONSTANT_INGETER_INFO整型直接常量池public final int CONST_INT=0;
1.2CONSTANT_String_info字符串直接常量池 public final String CONST_STR=”CONST_STR”;
1.3CONSTANT_DOUBLE_INFO浮點型直接常量池
等等各種基本數據類型基礎常量池(待會我們會反編譯一個類,來查看它的常量池等。)
- 4.2.方法名、方法描述符、類名、字段名,字段描述符的符號引用
也就是所以編譯器能夠被確定,能夠被快速查找的內容都存放在這裏,它像數組一樣通過索引訪問,就是專門用來做查找的。
編譯時就能確定數值的常量類型都會複製它的所有常量到自己的常量池中,或者嵌入到它的字節碼流中。作爲常量池或者字節碼流的一部分,編譯時常量保存在方法區中,就和一般的類變量一樣。但是當一般的類變量作爲他們的類型的一部分數據而保存的時候,編譯時常量作爲使用它們的類型的一部分而保存
5. 類變量:
就是靜態字段( public static String static_str=”static_str”;)
虛擬機在使用某個類之前,必須在方法區爲這些類變量分配空間。
6. 一個到classLoader的引用,通過this.getClass().getClassLoader()來取得爲什麼要先經過class呢?思考一下,然後看第七點的解釋,再回來思考
7. 一個到class對象的引用,這個對象存儲了所有這個字節碼內存塊的相關信息。所以你能夠看到的區域,比如:類信息,你可以通過this.getClass().getName()取得
所有的方法信息,可以通過this.getClass().getDeclaredMethods(),字段信息可以通過this.getClass().getDeclaredFields(),等等,所以在字節碼中你想得到的,調用的,通過class這個引用基本都能夠幫你完成。因爲他就是字節碼在內存塊在堆中的一個對象
8. 方法表,如果學習c++的人應該都知道c++的對象內存模型有一個叫虛表的東西,java本來的名字就叫c++- -,它的方法表其實說白了就是c++的虛表,它的內容就是這個類的所有實例可能被調用的所有實例方法的直接引用。也是爲了動態綁定的快速定位而做的一個類似緩存的查找表,它以數組的形式存在於內存中。不過這個表不是必須存在的,取決於虛擬機的設計者,以及運行虛擬機的機器是否有足夠的內存

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