32位jdk最大内存_你了解Java 内存区域和GC机制吗?
目錄
- Java垃圾回收概況
- Java內(nèi)存區(qū)域
- Java對象的訪問方式
- Java內(nèi)存分配機制
- Java GC機制
- 垃圾收集器
Java垃圾回收概況
Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要區(qū)別之一,作為Java開發(fā)者,一般不需要專門編寫內(nèi)存回收和垃圾清理代碼,對內(nèi)存泄露和溢出的問題,也不需要像C程序員那樣戰(zhàn)戰(zhàn)兢兢。這是因為在Java虛擬機中,存在自動內(nèi)存管理和垃圾清掃機制。概括地說,該機制對JVM(Java Virtual Machine)中的內(nèi)存進行標記,并確定哪些內(nèi)存需要回收,根據(jù)一定的回收策略,自動的回收內(nèi)存,永不停息(Nerver Stop)的保證JVM中的內(nèi)存空間,防止出現(xiàn)內(nèi)存泄露和溢出問題。
關(guān)于JVM,需要說明一下的是,目前使用最多的Sun公司的JDK中,自從1999年的JDK1.2開始直至現(xiàn)在仍在廣泛使用的JDK6,其中默認的虛擬機都是HotSpot。2009年,Oracle收購Sun,加上之前收購的EBA公司,Oracle擁有3大虛擬機中的兩個:JRockit和HotSpot,Oracle也表明了想要整合兩大虛擬機的意圖,但是目前在新發(fā)布的JDK7中,默認的虛擬機仍然是HotSpot,因此本文中默認介紹的虛擬機都是HotSpot,相關(guān)機制也主要是指HotSpot的GC機制。
Java GC機制主要完成3件事:確定哪些內(nèi)存需要回收,確定什么時候需要執(zhí)行GC,如何執(zhí)行GC。經(jīng)過這么長時間的發(fā)展(事實上,在Java語言出現(xiàn)之前,就有GC機制的存在,如Lisp語言),Java GC機制已經(jīng)日臻完善,幾乎可以自動的為我們做絕大多數(shù)的事情。然而,如果我們從事較大型的應(yīng)用軟件開發(fā),曾經(jīng)出現(xiàn)過內(nèi)存優(yōu)化的需求,就必定要研究Java GC機制。
學(xué)習(xí)Java GC機制,可以幫助我們在日常工作中排查各種內(nèi)存溢出或泄露問題,解決性能瓶頸,達到更高的并發(fā)量,寫出更高效的程序。
我們將從4個方面學(xué)習(xí)Java GC機制,1,內(nèi)存是如何分配的;2,如何保證內(nèi)存不被錯誤回收(即:哪些內(nèi)存需要回收);3,在什么情況下執(zhí)行GC以及執(zhí)行GC的方式;4,如何監(jiān)控和優(yōu)化GC機制。
Java內(nèi)存區(qū)域
了解Java GC機制,必須先清楚在JVM中內(nèi)存區(qū)域的劃分。在Java運行時的數(shù)據(jù)區(qū)里,由JVM管理的內(nèi)存區(qū)域分為下圖幾個模塊:
其中:
1,程序計數(shù)器(Program Counter Register):程序計數(shù)器是一個比較小的內(nèi)存區(qū)域,用于指示當前線程所執(zhí)行的字節(jié)碼執(zhí)行到了第幾行,可以理解為是當前線程的行號指示器。字節(jié)碼解釋器在工作時,會通過改變這個計數(shù)器的值來取下一條語句指令。
每個程序計數(shù)器只用來記錄一個線程的行號,所以它是線程私有(一個線程就有一個程序計數(shù)器)的。
如果程序執(zhí)行的是一個Java方法,則計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令地址;如果正在執(zhí)行的是一個本地(native,由C語言編寫完成)方法,則計數(shù)器的值為Undefined,由于程序計數(shù)器只是記錄當前指令地址,所以不存在內(nèi)存溢出的情況,因此,程序計數(shù)器也是所有JVM內(nèi)存區(qū)域中唯一一個沒有定義OutOfMemoryError的區(qū)域。
2,虛擬機棧(JVM Stack):一個線程的每個方法在執(zhí)行的同時,都會創(chuàng)建一個棧幀(Statck Frame),棧幀中存儲的有局部變量表、操作站、動態(tài)鏈接、方法出口等,當方法被調(diào)用時,棧幀在JVM棧中入棧,當方法執(zhí)行完成時,棧幀出棧。
局部變量表中存儲著方法的相關(guān)局部變量,包括各種基本數(shù)據(jù)類型,對象的引用,返回地址等。在局部變量表中,只有l(wèi)ong和double類型會占用2個局部變量空間(Slot,對于32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,局部變量表是在編譯時就已經(jīng)確定好的,方法運行所需要分配的空間在棧幀中是完全確定的,在方法的生命周期內(nèi)都不會改變。
虛擬機棧中定義了兩種異常,如果線程調(diào)用的棧深度大于虛擬機允許的最大深度,則拋出StatckOverFlowError(棧溢出);不過多數(shù)Java虛擬機都允許動態(tài)擴展虛擬機棧的大小(有少部分是固定長度的),所以線程可以一直申請棧,直到內(nèi)存不足,此時,會拋出OutOfMemoryError(內(nèi)存溢出)。
每個線程對應(yīng)著一個虛擬機棧,因此虛擬機棧也是線程私有的。
3,本地方法棧(Native Method Statck):本地方法棧在作用,運行機制,異常類型等方面都與虛擬機棧相同,唯一的區(qū)別是:虛擬機棧是執(zhí)行Java方法的,而本地方法棧是用來執(zhí)行native方法的,在很多虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將本地方法棧與虛擬機棧放在一起使用。
本地方法棧也是線程私有的。
4,堆區(qū)(Heap):堆區(qū)是理解Java GC機制最重要的區(qū)域,沒有之一。在JVM所管理的內(nèi)存中,堆區(qū)是最大的一塊,堆區(qū)也是Java GC機制所管理的主要內(nèi)存區(qū)域,堆區(qū)由所有線程共享,在虛擬機啟動時創(chuàng)建。堆區(qū)的存在是為了存儲對象實例,原則上講,所有的對象都在堆區(qū)上分配內(nèi)存(不過現(xiàn)代技術(shù)里,也不是這么絕對的,也有棧上直接分配的)。
一般的,根據(jù)Java虛擬機規(guī)范規(guī)定,堆內(nèi)存需要在邏輯上是連續(xù)的(在物理上不需要),在實現(xiàn)時,可以是固定大小的,也可以是可擴展的,目前主流的虛擬機都是可擴展的。如果在執(zhí)行垃圾回收之后,仍沒有足夠的內(nèi)存分配,也不能再擴展,將會拋出OutOfMemoryError:Java heap space異常。
關(guān)于堆區(qū)的內(nèi)容還有很多,將在下節(jié)“Java內(nèi)存分配機制”中詳細介紹。
5,方法區(qū)(Method Area):在Java虛擬機規(guī)范中,將方法區(qū)作為堆的一個邏輯部分來對待,但事實上,方法區(qū)并不是堆(Non-Heap);另外,不少人的博客中,將Java GC的分代收集機制分為3個代:青年代,老年代,永久代,這些作者將方法區(qū)定義為“永久代”,這是因為,對于之前的HotSpot Java虛擬機的實現(xiàn)方式中,將分代收集的思想擴展到了方法區(qū),并將方法區(qū)設(shè)計成了永久代。不過,除HotSpot之外的多數(shù)虛擬機,并不將方法區(qū)當做永久代,HotSpot本身,也計劃取消永久代。本文中,由于筆者主要使用Oracle JDK6.0,因此仍將使用永久代一詞。
方法區(qū)是各個線程共享的區(qū)域,用于存儲已經(jīng)被虛擬機加載的類信息(即加載類時需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態(tài)變量、編譯器即時編譯的代碼等。
方法區(qū)在物理上也不需要是連續(xù)的,可以選擇固定大小或可擴展大小,并且方法區(qū)比堆還多了一個限制:可以選擇是否執(zhí)行垃圾收集。一般的,方法區(qū)上執(zhí)行的垃圾收集是很少的,這也是方法區(qū)被稱為永久代的原因之一(HotSpot),但這也不代表著在方法區(qū)上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的內(nèi)存回收和對已加載類的卸載。
在方法區(qū)上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以后進一步深入研究時使用。
在方法區(qū)上定義了OutOfMemoryError:PermGen space異常,在內(nèi)存不足時拋出。
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分,用于存儲編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據(jù)符號引用翻譯出來的地址,將在類鏈接階段完成翻譯);運行時常量池除了存儲編譯期常量外,也可以存儲在運行時間產(chǎn)生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果調(diào)用的字符“abc”已經(jīng)在常量池中,則返回池中的字符串地址,否則,新建一個常量加入池中,并返回地址)。
6,直接內(nèi)存(Direct Memory):直接內(nèi)存并不是JVM管理的內(nèi)存,可以這樣理解,直接內(nèi)存,就是JVM以外的機器內(nèi)存,比如,你有4G的內(nèi)存,JVM占用了1G,則其余的3G就是直接內(nèi)存,JDK中有一種基于通道(Channel)和緩沖區(qū)(Buffer)的內(nèi)存分配方式,將由C語言實現(xiàn)的native函數(shù)庫分配在直接內(nèi)存中,用存儲在JVM堆中的DirectByteBuffer來引用。由于直接內(nèi)存收到本機器內(nèi)存的限制,所以也可能出現(xiàn)OutOfMemoryError的異常。
Java對象的訪問方式
一般來說,一個Java的引用訪問涉及到3個內(nèi)存區(qū)域:JVM棧,堆,方法區(qū)。
以最簡單的本地變量引用:Object obj = new Object()為例:
- Object obj表示一個本地引用,存儲在JVM棧的本地變量表中,表示一個reference類型數(shù)據(jù);
- new Object()作為實例對象數(shù)據(jù)存儲在堆中;
- 堆中還記錄了Object類的類型信息(接口、方法、field、對象類型等)的地址,這些地址所執(zhí)行的數(shù)據(jù)存儲在方法區(qū)中;
在Java虛擬機規(guī)范中,對于通過reference類型引用訪問具體對象的方式并未做規(guī)定,目前主流的實現(xiàn)方式主要有兩種:
1,通過句柄訪問(圖來自于《深入理解Java虛擬機:JVM高級特效與最佳實現(xiàn)》):
通過句柄訪問的實現(xiàn)方式中,JVM堆中會專門有一塊區(qū)域用來作為句柄池,存儲相關(guān)句柄所執(zhí)行的實例數(shù)據(jù)地址(包括在堆中地址和在方法區(qū)中的地址)。這種實現(xiàn)方法由于用句柄表示地址,因此十分穩(wěn)定。
2,通過直接指針訪問:(圖來自于《深入理解Java虛擬機:JVM高級特效與最佳實現(xiàn)》)
通過直接指針訪問的方式中,reference中存儲的就是對象在堆中的實際地址,在堆中存儲的對象信息中包含了在方法區(qū)中的相應(yīng)類型數(shù)據(jù)。這種方法最大的優(yōu)勢是速度快,在HotSpot虛擬機中用的就是這種方式。
Java內(nèi)存分配機制
這里所說的內(nèi)存分配,主要指的是在堆上的分配,一般的,對象的內(nèi)存分配都是在堆上進行,但現(xiàn)代技術(shù)也支持將對象拆成標量類型(標量類型即原子類型,表示單個值,可以是基本類型或String等),然后在棧上分配,在棧上分配的很少見,我們這里不考慮。
Java內(nèi)存分配和回收的機制概括的說,就是:分代分配,分代回收。對象將根據(jù)存活的時間被分為:年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法區(qū))。如下圖(來源于《成為JavaGC專家part I》,http://www.importnew.com/1993.html):
年輕代(Young Generation):對象被創(chuàng)建時,內(nèi)存的分配首先發(fā)生在年輕代(大對象可以直接被創(chuàng)建在年老代),大部分的對象在創(chuàng)建后很快就不再使用,因此很快變得不可達,于是被年輕代的GC機制清理掉(IBM的研究表明,98%的對象都是很快消亡的),這個GC機制被稱為Minor GC或叫Young GC。注意,Minor GC并不代表年輕代內(nèi)存不足,它事實上只表示在Eden區(qū)上的GC。
年輕代上的內(nèi)存分配是這樣的,年輕代可以分為3個區(qū)域:Eden區(qū)(伊甸園,亞當和夏娃偷吃禁果生娃娃的地方,用來表示內(nèi)存首次分配的區(qū)域,再貼切不過)和兩個存活區(qū)(Survivor 0 、Survivor 1)。內(nèi)存分配過程為(來源于《成為JavaGC專家part I》,http://www.importnew.com/1993.html):
- 絕大多數(shù)剛創(chuàng)建的對象會被分配在Eden區(qū),其中的大多數(shù)對象很快就會消亡。Eden區(qū)是連續(xù)的內(nèi)存空間,因此在其上分配內(nèi)存極快;
- 最初一次,當Eden區(qū)滿的時候,執(zhí)行Minor GC,將消亡的對象清理掉,并將剩余的對象復(fù)制到一個存活區(qū)Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);
- 下次Eden區(qū)滿了,再執(zhí)行一次Minor GC,將消亡的對象清理掉,將存活的對象復(fù)制到Survivor1中,然后清空Eden區(qū);
- 將Survivor0中消亡的對象清理掉,將其中可以晉級的對象晉級到Old區(qū),將存活的對象也復(fù)制到Survivor1區(qū),然后清空Survivor0區(qū);
- 當兩個存活區(qū)切換了幾次(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大于該值進入老年代,但這只是個最大值,并不代表一定是這個值)之后,仍然存活的對象(其實只有一小部分,比如,我們自己定義的對象),將被復(fù)制到老年代。
從上面的過程可以看出,Eden區(qū)是連續(xù)的空間,且Survivor總有一個為空。經(jīng)過一次GC和復(fù)制,一個Survivor中保存著當前還活著的對象,而Eden區(qū)和另一個Survivor區(qū)的內(nèi)容都不再需要了,可以直接清空,到下一次GC時,兩個Survivor的角色再互換。因此,這種方式分配內(nèi)存和清理內(nèi)存的效率都極高,這種垃圾回收的方式就是著名的“停止-復(fù)制(Stop-and-copy)”清理法(將Eden區(qū)和一個Survivor中仍然存活的對象拷貝到另一個Survivor中),這不代表著停止復(fù)制清理法很高效,其實,它也只在這種情況下高效,如果在老年代采用停止復(fù)制,則挺悲劇的。
在Eden區(qū),HotSpot虛擬機使用了兩種技術(shù)來加快內(nèi)存分配。分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),這兩種技術(shù)的做法分別是:由于Eden區(qū)是連續(xù)的,因此bump-the-pointer技術(shù)的核心就是跟蹤最后創(chuàng)建的一個對象,在對象創(chuàng)建時,只需要檢查最后一個對象后面是否有足夠的內(nèi)存即可,從而大大加快內(nèi)存分配速度;而對于TLAB技術(shù)是對于多線程而言的,將Eden區(qū)分為若干段,每個線程使用獨立的一段,避免相互影響。TLAB結(jié)合bump-the-pointer技術(shù),將保證每個線程都使用Eden區(qū)的一段,并快速的分配內(nèi)存。
年老代(Old Generation):對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC后存活了下來),則會被復(fù)制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發(fā)生的GC次數(shù)也比年輕代少。當年老代內(nèi)存不足時,將執(zhí)行Major GC,也叫 Full GC。
可以使用-XX:+UseAdaptiveSizePolicy開關(guān)來控制是否采用動態(tài)控制策略,如果動態(tài)控制,則動態(tài)調(diào)整Java堆中各個區(qū)域的大小以及進入老年代的年齡。
如果對象比較大(比如長字符串或大數(shù)組),Young空間不足,則大對象會直接分配到老年代上(大對象可能觸發(fā)提前GC,應(yīng)少用,更應(yīng)避免使用短命的大對象)。用-XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,大于這個值的對象會直接分配在老年代上。
可能存在年老代對象引用新生代對象的情況,如果需要執(zhí)行Young GC,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護一個512 byte的塊——”card table“,所有老年代對象引用新生代對象的記錄都記錄在這里。Young GC時,只要查這里即可,不用再去查全部老年代,因此性能大大提高。
Java GC機制
GC機制的基本算法是:分代收集,這個不用贅述。下面闡述每個分代的收集方法。
年輕代:
事實上,在上一節(jié),已經(jīng)介紹了新生代的主要垃圾回收方法,在新生代中,使用“停止-復(fù)制”算法進行清理,將新生代內(nèi)存分為2部分,1部分 Eden區(qū)較大,1部分Survivor比較小,并被劃分為兩個等量的部分。每次進行清理時,將Eden區(qū)和一個Survivor中仍然存活的對象拷貝到 另一個Survivor中,然后清理掉Eden和剛才的Survivor。
這里也可以發(fā)現(xiàn),停止復(fù)制算法中,用來復(fù)制的兩部分并不總是相等的(傳統(tǒng)的停止復(fù)制算法兩部分內(nèi)存相等,但新生代中使用1個大的Eden區(qū)和2個小的Survivor區(qū)來避免這個問題)
由于絕大部分的對象都是短命的,甚至存活不到Survivor中,所以,Eden區(qū)與Survivor的比例較大,HotSpot默認是 8:1,即分別占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下來的內(nèi)存超過了10%,則需要將一部分對象分配到 老年代。用-XX:SurvivorRatio參數(shù)來配置Eden區(qū)域Survivor區(qū)的容量比值,默認是8,代表Eden:Survivor1:Survivor2=8:1:1.
老年代:
老年代存儲的對象比年輕代多得多,而且不乏大對象,對老年代進行內(nèi)存清理時,如果使用停止-復(fù)制算法,則相當?shù)托АR话?#xff0c;老年代用的算法是標記-整理算法,即:標記出仍然存活的對象(存在引用的),將所有存活的對象向一端移動,以保證內(nèi)存的連續(xù)。
在發(fā)生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大于老年代的剩余空間大小,如果大于,則直接觸發(fā)一次Full GC,否則,就查看是否設(shè)置了-XX:+HandlePromotionFailure(允許擔保失敗),如果允許,則只會進行MinorGC,此時可以容忍內(nèi)存分配失敗;如果不允許,則仍然進行Full GC(這代表著如果設(shè)置-XX:+Handle PromotionFailure,則觸發(fā)MinorGC就會同時觸發(fā)Full GC,哪怕老年代還有很多內(nèi)存,所以,最好不要這樣做)。
方法區(qū)(永久代):
永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就可以被回收。對于無用的類進行回收,必須保證3點:
- 類的所有實例都已經(jīng)被回收
- 加載類的ClassLoader已經(jīng)被回收
- 類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)
永久代的回收并不是必須的,可以通過參數(shù)來設(shè)置是否對類進行回收。HotSpot提供-Xnoclassgc進行控制
使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看類加載和卸載信息
-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;
-XX:+TraceClassUnLoading需要fastdebug版HotSpot支持
垃圾收集器
在GC機制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具體實現(xiàn),Java虛擬機規(guī)范中對于垃圾收集器沒有任何規(guī)定,所以不同廠商實現(xiàn)的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下圖(圖來源于《深入理解Java虛擬機:JVM高級特效與最佳實現(xiàn)》,圖中兩個收集器之間有連線,說明它們可以配合使用):
在介紹垃圾收集器之前,需要明確一點,就是在新生代采用的停止復(fù)制算法中,“停 止(Stop-the-world)”的意義是在回收內(nèi)存時,需要暫停其他所 有線程的執(zhí)行。這個是很低效的,現(xiàn)在的各種新生代收集器越來越優(yōu)化這一點,但仍然只是將停止的時間變短,并未徹底取消停止。
- Serial收集器:新生代收集器,使用停止復(fù)制算法,使用一個線程進行GC,串行,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行內(nèi)存回收(這也是虛擬機在Client模式下運行的默認值)
- ParNew收集器:新生代收集器,使用停止復(fù)制算法,Serial收集器的多線程版,用多個線程進行GC,并行,其它工作線程暫停,關(guān)注縮短垃圾收集時間。使用-XX:+UseParNewGC開關(guān)來控制使用ParNew+Serial Old收集器組合收集內(nèi)存;使用-XX:ParallelGCThreads來設(shè)置執(zhí)行內(nèi)存回收的線程數(shù)。
- Parallel Scavenge 收集器:新生代收集器,使用停止復(fù)制算法,關(guān)注CPU吞吐量,即運行用戶代碼的時間/總時間,比如:JVM運行100分鐘,其中運行用戶代碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運行后臺運算(關(guān)注縮短垃圾收集時間的收集器,如CMS,等待時間很少,所以適 合用戶交互,提高用戶體驗)。使用-XX:+UseParallelGC開關(guān)控制使用Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認值);使用-XX:GCTimeRatio來設(shè)置用戶執(zhí)行時間占總時間的比例,默認99,即1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設(shè)置GC的最大停頓時間(這個參數(shù)只對Parallel Scavenge有效),用開關(guān)參數(shù)-XX:+UseAdaptiveSizePolicy可以進行動態(tài)控制,如自動調(diào)整Eden/Survivor比例,老年代對象年齡,新生代大小等,這個參數(shù)在ParNew下沒有。
- Serial Old收集器:老年代收集器,單線程收集器,串行,使用標記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象干掉,只留幸存的對象,壓縮是將移動對象,將空間填滿保證內(nèi)存分為2塊,一塊全是對象,一塊空閑)算法,使用單線程進行GC,其它工作線程暫停(注意,在老年代中進行標記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。
- Parallel Old收集器:老年代收集器,多線程,并行,多線程機制與Parallel Scavenge差不錯,使用標記整理(與Serial Old不同,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對象復(fù)制到預(yù)先準備好的區(qū)域,而不是像Sweep(清理)那樣清理廢棄的對象)算法,在Parallel Old執(zhí)行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。Parallel Old出現(xiàn)后(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現(xiàn)Parallel Scavenge收集器吞吐量優(yōu)先的效果。使用-XX:+UseParallelOldGC開關(guān)控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。
- CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于獲取最短回收停頓時間(即縮短垃圾回收的時間),使用標記清除算法,多線程,優(yōu)點是并發(fā)收集(用戶線程可以和GC線程同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行內(nèi)存回收,優(yōu)先使用ParNew+CMS(原因見后面),當用戶線程內(nèi)存不足時,采用備用方案Serial Old收集。
CMS收集的執(zhí)行過程是:初始標記(CMS-initial-mark) -> 并發(fā)標記(CMS-concurrent-mark) -->預(yù)清理(CMS-concurrent-preclean)-->可控預(yù)清理(CMS-concurrent-abortable-preclean)-> 重新標記(CMS-remark) -> 并發(fā)清除(CMS-concurrent-sweep) ->并發(fā)重設(shè)狀態(tài)等待下次CMS的觸發(fā)(CMS-concurrent-reset)
具體的說,先2次標記,1次預(yù)清理,1次重新標記,再1次清除。
1,首先jvm根據(jù)-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly來決定什么時間開始垃圾收集;
2,如果設(shè)置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有當old代占用確實達到了-XX:CMSInitiatingOccupancyFraction參數(shù)所設(shè)定的比例時才會觸發(fā)cms gc;
3,如果沒有設(shè)置-XX:+UseCMSInitiatingOccupancyOnly,那么系統(tǒng)會根據(jù)統(tǒng)計數(shù)據(jù)自行決定什么時候觸發(fā)cms gc;因此有時會遇到設(shè)置了80%比例才cms gc,但是50%時就已經(jīng)觸發(fā)了,就是因為這個參數(shù)沒有設(shè)置的原因;
4,當cms gc開始時,首先的階段是初始標記(CMS-initial-mark),是stop the world階段,因此此階段標記的對象只是從root集最直接可達的對象;
CMS-initial-mark:961330K(1572864K),指標記時,old代的已用空間和總空間
5,下一個階段是并發(fā)標記(CMS-concurrent-mark),此階段是和應(yīng)用線程并發(fā)執(zhí)行的,所謂并發(fā)收集器指的就是這個,主要作用是標記可達的對象,此階段不需要用戶停頓。
此階段會打印2條日志:CMS-concurrent-mark-start,CMS-concurrent-mark
6,下一個階段是CMS-concurrent-preclean,此階段主要是進行一些預(yù)清理,因為標記和應(yīng)用線程是并發(fā)執(zhí)行的,因此會有些對象的狀態(tài)在標記后會改變,此階段正是解決這個問題因為之后的Rescan階段也會stop the world,為了使暫停的時間盡可能的小,也需要preclean階段先做一部分工作以節(jié)省時間
此階段會打印2條日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean
7,下一階段是CMS-concurrent-abortable-preclean階段,加入此階段的目的是使cms gc更加可控一些,作用也是執(zhí)行一些預(yù)清理,以減少Rescan階段造成應(yīng)用暫停的時間
此階段涉及幾個參數(shù):
-XX:CMSMaxAbortablePrecleanTime:當abortable-preclean階段執(zhí)行達到這個時間時才會結(jié)束
-XX:CMSScheduleRemarkEdenSizeThreshold(默認2m):控制abortable-preclean階段什么時候開始執(zhí)行,
即當eden使用達到此值時,才會開始abortable-preclean階段
-XX:CMSScheduleRemarkEdenPenetratio(默認50%):控制abortable-preclean階段什么時候結(jié)束執(zhí)行
此階段會打印一些日志如下:
CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,
CMS:abort preclean due to time XXX
8,再下一個階段是第二個stop the world階段了,即Rescan階段,此階段暫停應(yīng)用線程,停頓時間比并發(fā)標記小得多,但比初始標記稍長。對對象進行重新掃描并標記;
YG occupancy:964861K(2403008K),指執(zhí)行時young代的情況
CMS remark:961330K(1572864K),指執(zhí)行時old代的情況
此外,還打印出了弱引用處理、類卸載等過程的耗時
9,再下一個階段是CMS-concurrent-sweep,進行并發(fā)的垃圾清理
10,最后是CMS-concurrent-reset,為下一次cms gc重置相關(guān)數(shù)據(jù)結(jié)構(gòu)
有2種情況會觸發(fā)CMS 的悲觀full gc,在悲觀full gc時,整個應(yīng)用會暫停
A,concurrent-mode-failure:預(yù)清理階段可能出現(xiàn),當cms gc正進行時,此時有新的對象要進行old代,但是old代空間不足造成的。其可能性有:1,O區(qū)空間不足以讓新生代晉級,2,O區(qū)空間用完之前,無法完成對無引用的對象的清理。這表明,當前有大量數(shù)據(jù)進入內(nèi)存且無法釋放。
B,promotion-failed:新生代young gc可能出現(xiàn),當進行young gc時,有部分young代對象仍然可用,但是S1或S2放不下,因此需要放到old代,但此時old代空間無法容納此。
影響cms gc時長及觸發(fā)的參數(shù)是以下2個:
-XX:CMSMaxAbortablePrecleanTime=5000
-XX:CMSInitiatingOccupancyFraction=80
解決也是針對這兩個參數(shù)來的,根本的原因是每次請求消耗的內(nèi)存量過大
解決方式:
A,針對cms gc的觸發(fā)階段,調(diào)整-XX:CMSInitiatingOccupancyFraction=50,提早觸發(fā)cms gc,就可以緩解當old代達到80%,cms gc處理不完,從而造成concurrent mode failure引發(fā)full gc
B,修改-XX:CMSMaxAbortablePrecleanTime=500,縮小CMS-concurrent-abortable-preclean階段的時間
C,考慮到cms gc時不會進行compact,因此加入-XX:+UseCMSCompactAtFullCollection
(cms gc后會進行內(nèi)存的compact)和-XX:CMSFullGCsBeforeCompaction=4(在full gc4次后會進行compact)參數(shù)
在CMS清理過程中,只有初始標記和重新標記需要短暫停頓,并發(fā)標記和并發(fā)清除都不需要暫停用戶線程,因此效率很高,很適合高交互的場合。
CMS也有缺點,它需要消耗額外的CPU和內(nèi)存資源,在CPU和內(nèi)存資源緊張,CPU較少時,會加重系統(tǒng)負擔(CMS默認啟動線程數(shù)為(CPU數(shù)量+3)/4)。
另外,在并發(fā)收集過程中,用戶線程仍然在運行,仍然產(chǎn)生內(nèi)存垃圾,所以可能產(chǎn)生“浮動垃圾”,本次無法清理,只能下一次Full GC才清理,因此在GC期間,需要預(yù)留足夠的內(nèi)存給用戶線程使用。所以使用CMS的收集器并不是老年代滿了才觸發(fā)Full GC,而是在使用了一大半(默認68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction來設(shè)置)的時候就要進行Full GC,如果用戶線程消耗內(nèi)存不是特別大,可以適當調(diào)高-XX:CMSInitiatingOccupancyFraction以降低GC次數(shù),提高性能,如果預(yù)留的用戶線程內(nèi)存不夠,則會觸發(fā)Concurrent Mode Failure,此時,將觸發(fā)備用方案:使用Serial Old 收集器進行收集,但這樣停頓時間就長了,因此-XX:CMSInitiatingOccupancyFraction不宜設(shè)的過大。
還有,CMS采用的是標記清除算法,會導(dǎo)致內(nèi)存碎片的產(chǎn)生,可以使用-XX:+UseCMSCompactAtFullCollection來設(shè)置是否在Full GC之后進行碎片整理,用-XX:CMSFullGCsBeforeCompaction來設(shè)置在執(zhí)行多少次不壓縮的Full GC之后,來一次帶壓縮的Full GC。
- G1收集器:在JDK1.7中正式發(fā)布,與現(xiàn)狀的新生代、老年代概念有很大不同,目前使用較少,不做介紹。
注意并發(fā)(Concurrent)和并行(Parallel)的區(qū)別:
并發(fā)是指用戶線程與GC線程同時執(zhí)行(不一定是并行,可能交替,但總體上是在同時執(zhí)行的),不需要停頓用戶線程(其實在CMS中用戶線程還是需要停頓的,只是非常短,GC線程在另一個CPU上執(zhí)行);
并行收集是指多個GC線程并行工作,但此時用戶線程是暫停的;
所以,Serial是串行的,Parallel收集器是并行的,而CMS收集器是并發(fā)的.
最后,為了幫助更多的小伙伴融入Java這個大家庭,小編為大家準備了Java學(xué)習(xí)資料和視頻教程
獲取方式:
關(guān)注“小編”后,私信小編即可獲取!
總結(jié)
以上是生活随笔為你收集整理的32位jdk最大内存_你了解Java 内存区域和GC机制吗?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 王牌竞速车辆轮毂在哪更换?
- 下一篇: 一键部署dns服务_OpenShift