Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要區別之一,做爲Java開發者,通常不須要專門編寫內存回收和垃圾清理代碼,對內存泄露和溢出的問題,也不須要像C程序員那樣戰戰兢兢。通過這麼長時間的發展,Java GC機制已經日臻完善,幾乎能夠自動的爲咱們作絕大多數的事情。java
雖然java不須要開發人員顯示的分配和回收內存,這對開發人員確實下降了很多編程難度,但也可能帶來一些反作用:程序員
1. 有可能不知不覺浪費了不少內存 2. JVM花費過多時間來進行內存回收 3. 內存泄露1234
所以,做爲一名java編程人員,必須學會JVM內存管理和回收機制,這能夠幫助咱們在平常工做中排查各類內存溢出或泄露問題,解決性能瓶頸,達到更高的併發量,寫出更高效的程序。算法
根據JVM規範,JVM把內存劃分了以下幾個區域:數據庫
1. 方法區 2. 堆區 3. 本地方法棧 4. 虛擬機棧 5. 程序計數器 123456
其中,方法區和堆是全部線程共享的。編程
方法區存放了要加載的類的信息(如類名,修飾符)、類中的靜態變量、final定義的常量、類中的field、方法信息,當開發人員調用類對象中的getName、isInterface等方法來獲取信息時,這些數據都來源於方法區。方法區是全局共享的,在必定條件下它也會被GC。當方法區使用的內存超過它容許的大小時,就會拋出OutOfMemory:PermGen Space異常。數組
在Hotspot虛擬機中,這塊區域對應的是Permanent Generation(持久代),通常的,方法區上執行的垃圾收集是不多的,所以方法區又被稱爲持久代的緣由之一,但這也不表明着在方法區上徹底沒有垃圾收集,其上的垃圾收集主要是針對常量池的內存回收和對已加載類的卸載。在方法區上進行垃圾收集,條件苛刻並且至關困難,關於其回後面再介紹。緩存
運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存儲編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址,將在類連接階段完成翻譯);運行時常量池除了存儲編譯期常量外,也能夠存儲在運行時間產生的常量,好比String類的intern()方法,做用是String維護了一個常量池,若是調用的字符「abc」已經在常量池中,則返回池中的字符串地址,不然,新建一個常量加入池中,並返回地址。服務器
JVM方法區的相關參數,最小值:--XX:PermSize
;最大值 --XX:MaxPermSize
。多線程
堆區是理解JavaGC機制最重要的區域。在JVM所管理的內存中,堆區是最大的一塊,堆區也是JavaGC機制所管理的主要內存區域,堆區由全部線程共享,在虛擬機啓動時建立。堆區用來存儲對象實例及數組值,能夠認爲java中全部經過new建立的對象都在此分配。併發
對於堆區大小,能夠經過參數-Xms
和-Xmx
來控制,-Xms爲JVM啓動時申請的最新heap內存,默認爲物理內存的1/64但小於1GB;-Xmx爲JVM可申請的最大Heap內存,默認爲物理內存的1/4但小於1GB,默認當剩餘堆空間小於40%時,JVM會增大Heap到-Xmx大小,可經過-XX:MinHeapFreeRadio
參數來控制這個比例;當空餘堆內存大於70%時,JVM會減少Heap大小到-Xms指定大小,可經過-XX:MaxHeapFreeRatio
來指定這個比例。對於系統而言,爲了不在運行期間頻繁的調整Heap大小,咱們一般將-Xms和-Xmx設置成同樣。
爲了讓內存回收更加高效(後面會具體講爲什麼要分代劃分),從Sun JDK 1.2開始對堆採用了分代管理方式,以下圖所示:
對象在被建立時,內存首先是在年輕代進行分配(注意,大對象能夠直接在老年代分配)。當年輕代須要回收時會觸發Minor GC(也稱做Young GC)。
年輕代由Eden Space和兩塊相同大小的Survivor Space(又稱S0和S1)構成,可經過-Xmn參數來調整新生代大小,也可經過-XX:SurvivorRadio
來調整Eden Space和Survivor Space大小。不一樣的GC方式會按不一樣的方式來按此值劃分Eden Space和Survivor Space,有些GC方式還會根據運行情況來動態調整Eden、S0、S1的大小。
年輕代的Eden區內存是連續的,因此其分配會很是快;一樣Eden區的回收也很是快(由於大部分狀況下Eden區對象存活時間很是短,而Eden區採用的複製回收算法,此算法在存活對象比例不多的狀況下很是高效,後面會詳細介紹)。
若是在執行垃圾回收以後,仍沒有足夠的內存分配,也不能再擴展,將會拋出OutOfMemoryError:Java Heap Space異常。
老年代用於存放在年輕代中經屢次垃圾回收仍然存活的對象,能夠理解爲比較老一點的對象,例如緩存對象;新建的對象也有可能在老年代上直接分配內存,這主要有兩種狀況:一種爲大對象,能夠經過啓動參數設置-XX:PretenureSizeThreshold=1024
,表示超過多大時就不在年輕代分配,而是直接在老年代分配。此參數在年輕代採用Parallel Scavenge GC時無效,由於其會根據運行狀況本身決定什麼對象直接在老年代上分配內存;另外一種爲大的數組對象,且數組對象中無引用外部對象。
當老年代滿了的時候就須要對老年代進行垃圾回收,老年代的垃圾回收稱做Major GC(也稱做Full GC)。
老年代所佔用的內存大小爲-Xmx對應的值減去-Xmn對應的值。
本地方法棧用於支持native方法的執行,存儲了每一個native方法調用的狀態。本地方法棧和虛擬機方法棧運行機制一致,它們惟一的區別就是,虛擬機棧是執行Java方法的,而本地方法棧是用來執行native方法的,在不少虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將本地方法棧與虛擬機棧放在一塊兒使用。
程序計數器是一個比較小的內存區域,多是CPU寄存器或者操做系統內存,其主要用於指示當前線程所執行的字節碼執行到了第幾行,能夠理解爲是當前線程的行號指示器。字節碼解釋器在工做時,會經過改變這個計數器的值來取下一條語句指令。 每一個程序計數器只用來記錄一個線程的行號,因此它是線程私有(一個線程就有一個程序計數器)的。
若是程序執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令地址;若是正在執行的是一個本地(native,由C語言編寫完成)方法,則計數器的值爲Undefined,因爲程序計數器只是記錄當前指令地址,因此不存在內存溢出的狀況,所以,程序計數器也是全部JVM內存區域中惟一一個沒有定義OutOfMemoryError的區域。
虛擬機棧佔用的是操做系統內存,每一個線程都對應着一個虛擬機棧,它是線程私有的,並且分配很是高效。一個線程的每一個方法在執行的同時,都會建立一個棧幀(Statck Frame),棧幀中存儲的有局部變量表、操做站、動態連接、方法出口等,當方法被調用時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。
局部變量表中存儲着方法的相關局部變量,包括各類基本數據類型,對象的引用,返回地址等。在局部變量表中,只有long和double類型會佔用2個局部變量空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。須要注意的是,局部變量表是在編譯時就已經肯定好的,方法運行所須要分配的空間在棧幀中是徹底肯定的,在方法的生命週期內都不會改變。
虛擬機棧中定義了兩種異常,若是線程調用的棧深度大於虛擬機容許的最大深度,則拋出StatckOverFlowError(棧溢出);不過多數Java虛擬機都容許動態擴展虛擬機棧的大小(有少部分是固定長度的),因此線程能夠一直申請棧,直到內存不足,此時,會拋出OutOfMemoryError(內存溢出)。
通常來講,一個Java的引用訪問涉及到3個內存區域:JVM棧,堆,方法區。以最簡單的本地變量引用:Object objRef = new Object()
爲例:
Object objRef 表示一個本地引用,存儲在JVM棧的本地變量表中,表示一個reference類型數據;
new Object()做爲實例對象數據存儲在堆中;
堆中還記錄了可以查詢到此Object對象的類型數據(接口、方法、field、對象類型等)的地址,實際的數據則存儲在方法區中;
在Java虛擬機規範中,只規定了指向對象的引用,對於經過reference類型引用訪問具體對象的方式並未作規定,不過目前主流的實現方式主要有兩種:
經過句柄訪問的實現方式中,JVM堆中會劃分單獨一塊內存區域做爲句柄池,句柄池中存儲了對象實例數據(在堆中)和對象類型數據(在方法區中)的指針。這種實現方法因爲用句柄表示地址,所以十分穩定。
經過直接指針訪問的方式中,reference中存儲的就是對象在堆中的實際地址,在堆中存儲的對象信息中包含了在方法區中的相應類型數據。這種方法最大的優點是速度快,在HotSpot虛擬機中用的就是這種方式。
Java對象所佔用的內存主要在堆上實現,由於堆是線程共享的,所以在堆上分配內存時須要進行加鎖,這就致使了建立對象的開銷比較大。當堆上空間不足時,會出發GC,若是GC後空間仍然不足,則會拋出OutOfMemory異常。
爲了提高內存分配效率,在年輕代的Eden區HotSpot虛擬機使用了兩種技術來加快內存分配 ,分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers)。因爲Eden區是連續的,所以bump-the-pointer技術的核心就是跟蹤最後建立的一個對象,在對象建立時,只須要檢查最後一個對象後面是否有足夠的內存便可,從而大大加快內存分配速度;而對於TLAB技術是對於多線程而言的, 它會爲每一個新建立的線程在新生代的Eden Space上分配一塊獨立的空間,這塊空間稱爲TLAB(Thread Local Allocation Buffer),其大小由JVM根據運行狀況計算而得。可經過-XX:TLABWasteTargetPercent
來設置其可佔用的Eden Space的百分比,默認是1%。在TLAB上分配內存不須要加鎖,通常JVM會優先在TLAB上分配內存,若是對象過大或者TLAB空間已經用完,則仍然在堆上進行分配。所以,在編寫程序時,多個小對象比大的對象分配起來效率更高。可在啓動參數上增長-XX:+PrintTLAB
來查看TLAB空間的使用狀況。
對象若是在年輕代存活了足夠長的時間而沒有被清理掉(即在幾回Minor GC後存活了下來),則會被複制到年老代,年老代的空間通常比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時,將執行Major GC,也叫 Full GC。
可使用-XX:+UseAdaptiveSizePolicy
開關來控制是否採用動態控制策略,若是動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。
若是對象比較大(好比長字符串或大數組),年輕代空間不足,則大對象會直接分配到老年代上(大對象可能觸發提早GC,應少用,更應避免使用短命的大對象)。用 -XX:PretenureSizeThreshold
來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上。
JVM經過GC來回收堆和方法區中的內存,這個過程是自動執行的。說到Java GC機制,其主要完成3件事:肯定哪些內存須要回收;肯定何時須要執行GC;如何執行GC。JVM主要採用收集器的方式實現GC,主要的收集器有引用計數收集器和跟蹤收集器。
引用計數器採用分散式管理方式,經過計數器記錄對象是否被引用。當計數器爲0時,說明此對象已經再也不被使用,可進行回收,如圖所示:
在上圖中,ObjectA釋放了對ObjectB的引用後,ObjectB的引用計數器變爲0,此時可回收ObjectB所佔有的內存。
引用計數器須要在每次對象賦值時進行引用計數器的增減,他有必定消耗。另外,引用計數器對於循環引用的場景沒有辦法實現回收。例如在上面的例子中,若是ObjectB和ObjectC互相引用,那麼即便ObjectA釋放了對ObjectB和ObjectC的引用,也沒法回收ObjectB、ObjectC,所以對於java這種會造成複雜引用關係的語言而言,引用計數器是很是不適合的,SunJDK在實現GC時也未採用這種方式。
跟蹤收集器採用的爲集中式的管理方式,會全局記錄數據引用的狀態。基於必定條件的觸發(例如定時、空間不足時),執行時須要從根集合來掃描對象的引用關係,這可能會形成應用程序暫停。主要有複製(Copying)、標記-清除(Mark-Sweep)和標記-壓縮(Mark-Compact)三種實現算法。
複製採用的方式爲從根集合掃描出存活的對象,並將找到的存活的對象複製到一塊新的徹底未被使用的空間中,如圖所示:
複製收集器方式僅須要從根集合掃描全部存活對象,當要回收的空間中存活對象較少時,複製算法會比較高效(年輕代的Eden區就是採用這個算法),其帶來的成本是要增長一塊空的內存空間及進行對象的移動。
標記-清除採用的方式爲從根集合開始掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未標記的對象,並進行清除,標記和清除過程以下圖所示:
上圖中藍色的部分是有被引用的存活的對象,褐色部分沒被引用的可回收的對象。在marking階段爲了mark對象,全部的對象都會被掃描一遍,掃描這個過程是比較耗時的。
清除階段回收的是沒有被引用的對象,存活的對象被保留。內存分配器會持有空閒空間的引用列表,當有分配請求時會查詢空閒空間引用列表進行分配。
標記-清除動做不須要進行對象移動,且僅對其不存活的對象進行處理。在空間中存活對象較多的狀況下較爲高效,但因爲標記-清除直接回收不存活對象佔用的內存,所以會形成內存碎片。
標記-壓縮和標記-清除同樣,是對活的對象進行標記,可是在清除後的處理不同,標記-壓縮在清除對象佔用的內存後,會把全部活的對象向左端空閒空間移動,而後再更新引用其對象的指針,以下圖所示:
很明顯,標記-壓縮在標記-清除的基礎上對存活的對象進行了移動規整動做,解決了內存碎片問題,獲得更多連續的內存空間以提升分配效率,但因爲須要對對象進行移動,所以成本也比較高。
在一開始的時候,JVM的GC就是採用標記-清除-壓縮方式進行的,這麼作並非很高效,由於當對象分配的愈來愈多時,對象列表也越來也大,掃描和移動愈來愈耗時,形成了內存回收愈來愈慢。然而,通過根據對java應用的分析,發現大部分對象的存活時間都很是短,只有少部分數據存活週期是比較長的,請看下面對java對象內存存活時間的統計:
從圖表中能夠看出,大部分對象存活時間是很是短的,隨着時間的推移,被分配的對象愈來愈少。
通過上面介紹,咱們已經知道了JVM爲什麼要分代回收,下面咱們就詳細看一下整個回收過程。
在初始階段,新建立的對象被分配到Eden區,survivor的兩塊空間都爲空。
當Eden區滿了的時候,minor garbage 被觸發
通過掃描與標記,存活的對象被複制到S0,不存活的對象被回收
在下一次的Minor GC中,Eden區的狀況和上面一致,沒有引用的對象被回收,存活的對象被複制到survivor區。然而在survivor區,S0的全部的數據都被複制到S1,須要注意的是,在上次minor GC過程當中移動到S0中的兩個對象在複製到S1後其年齡要加1。此時Eden區S0區被清空,全部存活的數據都複製到了S1區,而且S1區存在着年齡不同的對象,過程以下圖所示:
再下一次MinorGC則重複這個過程,這一次survivor的兩個區對換,存活的對象被複制到S0,存活的對象年齡加1,Eden區和另外一個survivor區被清空。
下面演示一下Promotion過程,再通過幾回Minor GC以後,當存活對象的年齡達到一個閾值以後(可經過參數配置,默認是8),就會被從年輕代Promotion到老年代。
隨着MinorGC一次又一次的進行,不斷會有新的對象被promote到老年代。
上面基本上覆蓋了整個年輕代全部的回收過程。最終,MajorGC將會在老年代發生,老年代的空間將會被清除和壓縮。
從上面的過程能夠看出,Eden區是連續的空間,且Survivor總有一個爲空。通過一次GC和複製,一個Survivor中保存着當前還活着的對象,而Eden區和另外一個Survivor區的內容都再也不須要了,能夠直接清空,到下一次GC時,兩個Survivor的角色再互換。所以,這種方式分配內存和清理內存的效率都極高,這種垃圾回收的方式就是著名的「中止-複製(Stop-and-copy)」清理法(將Eden區和一個Survivor中仍然存活的對象拷貝到另外一個Survivor中),這不表明着中止複製清理法很高效,其實,它也只在這種狀況下(基於大部分對象存活週期很短的事實)高效,若是在老年代採用中止複製,則是很是不合適的。
老年代存儲的對象比年輕代多得多,並且不乏大對象,對老年代進行內存清理時,若是使用中止-複製算法,則至關低效。通常,老年代用的算法是標記-壓縮算法,即:標記出仍然存活的對象(存在引用的),將全部存活的對象向一端移動,以保證內存的連續。在發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大於老年代的剩餘空間大小,若是大於,則直接觸發一次Full GC,不然,就查看是否設置了-XX:+HandlePromotionFailure
(容許擔保失敗),若是容許,則只會進行MinorGC,此時能夠容忍內存分配失敗;若是不容許,則仍然進行Full GC(這表明着若是設置-XX:+Handle PromotionFailure
,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有不少內存,因此,最好不要這樣作)。
關於方法區即永久代的回收,永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就能夠被回收。對於無用的類進行回收,必須保證3點:
1. 類的全部實例都已經被回收 2. 加載類的ClassLoader已經被回收 3. 類對象的Class對象沒有被引用(即沒有經過反射引用該類的地方)1234
永久代的回收並非必須的,能夠經過參數來設置是否對類進行回收。
經過上面的介紹,咱們已經瞭解到了JVM的內存回收過程,而在虛擬機中,GC是由垃圾回收器來具體執行的,因此,在實際應用場景中咱們須要根據應用狀況選擇合適的垃圾收集器,下面咱們就介紹一下垃圾收集器。
串行收集器JavaSE5和6中客戶端虛擬機所採用的默認配置,它是最簡單的收集器,比較適合於只有一個處理器的系統。在串行收集器中,minor和major GC過程都是用一個線程進行垃圾回收。
首先,串行GC通常用在對應用暫停要求不是很高和運行在客戶端模式的場景,它僅僅利用一個CPU核心來進行垃圾回收。在如今的硬件條件下,串行GC能夠管理不少小內存的應用,而且可以保證相對較小的暫停(在Full GC的狀況下大約須要幾秒的時間)。另外一個一般採用串行GC的場景就是一臺機器運行多個JVM虛擬機的狀況(JVM虛擬機個數大於CPU核心數),在這種場景下,當一個JVM進行垃圾回收時只利用一個處理器,不會對其它JVM形成較大的影響。最後,在一些內存比較小和CPU核心數比較少的硬件設備中也比較適合採用串行收集器。
1 啓用串行收集器: -XX:+UseSerialGC
2 命令行示例:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar12
並行收集器採用多線程的方式來進行垃圾回收,採用並行的方式可以帶來極大的CPU吞吐量。它在不進行垃圾回收的時候對正在運行的應用程序沒有任何影響,在進程GC的時候採用多線程的方式來提升回收速度,所以,並行收集器很是適用於批處理的情形。固然,若是應用對程序暫停要求很高的話,建議採用下面介紹的併發收集器。默認一個N cpu的機器上,並行回收的線程數爲N。固然,並行的數量能夠經過參數進行控制: -XX:ParallelGCThreads=<desired number>
。並行收集器是Server級別機器(CPU大於2且內存大於2G)上採用的默認回收方式,
在單核CPU的機器上,即便配置了並行收集器,實際回收時仍然採用的是默認收集器。若是一臺機器上只有兩個CPU,採用並行回收器和默認回收器的效果其實差很少,只有當CPU個數大於2個時,年輕代回收的暫停時間纔會減小。
並行回收器適用於多CPU、對暫停時間要求短的狀況下。一般,一些批處理的應用如報告打印、數據庫查詢可採用並行收集器。
1 啓用命令:-XX:+UseParallelGC
2 命令行示例:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar12
1 啓用命令:-XX:+UseParallelOldGC
當啓用 -XX:+UseParallelOldGC 選項時,年輕代和老年代的垃圾收集都會用多線程進行,在壓縮階段也是多線程。由於HotSpot虛擬機在年輕代採用的是中止-複製算法,年輕代沒有壓縮過程,而老年代採用的是標記-清除-壓縮算法,因此僅在老年代有compact過程。
2 命令行示例:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelOldGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar12
CMS收集器主要用於永久區,它試圖用多線程併發的形式來減小垃圾收集過程當中的暫停。CMS收集器不會對存活的對象進行復制或移動。
CMS收集器主要用在應用程序對暫停時間要求很高的場景,好比桌面UI應用須要及時響應用戶操做事件、服務器必須能快速響應客戶端請求或者數據庫要快速響應查詢請求等等。
1 啓用CMS收集器:-XX:+UseConcMarkSweepGC
2 設置線程數:-XX:ParallelCMSThreads=<n>
3 命令行示例:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseConcMarkSweepGC -XX:ParallelCMSThreads=2 -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar12
G1即Garbage First,它是在java 7中出現的新的收集器,它的目標是替換掉現有的CMS收集器。G1具備並行、併發、增量壓縮、暫停時間段等特色,在這裏先不作詳細介紹。
1 啓用G1收集器:-XX:+UseG1GC
2 命令行示例:
java -Xmx12m -Xms3m -XX:+UseG1GC -jar c:\javademos\demo\jfc\Jav