天天看點

JAVA記憶體讀寫速度極限_JVM調優總結

一些概念

資料類型

Java虛拟機中,資料類型可以分為兩類:基本類型和引用類型。基本類型的變量儲存原始值,即:他代表的值就是數值本身;而引用類型的變量儲存引用值。“引用值”代表了某個對象的引用,而不是對象本身,對象本身存放在這個引用值所表示的位址的位置。

基本類型包括:byte,short,int,long,char,float,double,Boolean,returnAddress

引用類型包括:類類型,接口類型和數組。

堆與棧

堆和棧是程式運作的關鍵,很有必要把他們的關系說清楚。

JAVA記憶體讀寫速度極限_JVM調優總結

棧是運作時的機關,而堆是存儲的機關。

棧解決程式的運作問題,即程式如何執行,或者說如何處理資料;堆解決的是資料存儲的問題,即資料怎麼放、放在哪兒。

在Java中一個線程就會相應有一個線程棧與之對應,這點很容易了解,因為不同的線程執行邏輯有所不同,是以需要一個獨立的線程棧。而堆則是所有線程共享的。棧因為是運作機關,是以裡面存儲的資訊都是跟目前線程(或程式)相關資訊的。包括局部變量、程式運作狀态、方法傳回值等等;而堆隻負責存儲對象資訊。

為什麼要把堆和棧區分出來呢?棧中不是也可以存儲資料嗎?

第一,從軟體設計的角度看,棧代表了處理邏輯,而堆代表了資料。這樣分開,使得處理邏輯更為清晰。分而治之的思想。這種隔離、子產品化的思想在軟體設計的方方面面都有展現。

第二,堆與棧的分離,使得堆中的内容可以被多個棧共享(也可以了解為多個線程通路同一個對象)。這種共享的收益是很多的。一方面這種共享提供了一種有效的資料互動方式(如:共享記憶體),另一方面,堆中的共享常量和緩存可以被所有棧通路,節省了空間。

第三,棧因為運作時的需要,比如儲存系統運作的上下文,需要進行位址段的劃分。由于棧隻能向上增長,是以就會限制住棧存儲内容的能力。而堆不同,堆中的對象是可以根據需要動态增長的,是以棧和堆的拆分,使得動态增長成為可能,相應棧中隻需記錄堆中的一個位址即可。

第四,面向對象就是堆和棧的完美結合。其實,面向對象方式的程式與以前結構化的程式在執行上沒有任何差別。但是,面向對象的引入,使得對待問題的思考方式發生了改變,而更接近于自然方式的思考。當我們把對象拆開,你會發現,對象的屬性其實就是資料,存放在堆中;而對象的行為(方法),就是運作邏輯,放在棧中。我們在編寫對象的時候,其實即編寫了資料結構,也編寫的處理資料的邏輯。不得不承認,面向對象的設計,确實很美。

在Java中,Main函數就是棧的起始點,也是程式的起始點。

程式要運作總是有一個起點的。同C語言一樣,java中的Main就是那個起點。無論什麼java程式,找到main就找到了程式執行的入口:)

堆中存什麼?棧中存什麼?

堆中存的是對象。棧中存的是基本資料類型和堆中對象的引用。一個對象的大小是不可估計的,或者說是可以動态變化的,但是在棧中,一個對象隻對應了一個4btye的引用(堆棧分離的好處:))。

為什麼不把基本類型放堆中呢?因為其占用的空間一般是1~8個位元組——需要空間比較少,而且因為是基本類型,是以不會出現動态增長的情況——長度固定,是以棧中存儲就夠了,如果把他存在堆中是沒有什麼意義的(還會浪費空間,後面說明)。可以這麼說,基本類型和對象的引用都是存放在棧中,而且都是幾個位元組的一個數,是以在程式運作時,他們的處理方式是統一的。但是基本類型、對象引用和對象本身就有所差別了,因為一個是棧中的資料一個是堆中的資料。最常見的一個問題就是,Java中參數傳遞時的問題。

Java中的參數傳遞時傳值呢?還是傳引用?

要說明這個問題,先要明确兩點:

1. 不要試圖與C進行類比,Java中沒有指針的概念

2. 程式運作永遠都是在棧中進行的,因而參數傳遞時,隻存在傳遞基本類型和對象引用的問題。不會直接傳對象本身。

明确以上兩點後。Java在方法調用傳遞參數時,因為沒有指針,是以它都是進行傳值調用(這點可以參考C的傳值調用)。是以,很多書裡面都說Java是進行傳值調用,這點沒有問題,而且也簡化的C中複雜性。

但是傳引用的錯覺是如何造成的呢?在運作棧中,基本類型和引用的處理是一樣的,都是傳值,是以,如果是傳引用的方法調用,也同時可以了解為“傳引用值”的傳值調用,即引用的處理跟基本類型是完全一樣的。但是當進入被調用方法時,被傳遞的這個引用的值,被程式解釋(或者查找)到堆中的對象,這個時候才對應到真正的對象。如果此時進行修改,修改的是引用對應的對象,而不是引用本身,即:修改的是堆中的資料。是以這個修改是可以保持的了。

對象,從某種意義上說,是由基本類型組成的。可以把一個對象看作為一棵樹,對象的屬性如果還是對象,則還是一顆樹(即非葉子節點),基本類型則為樹的葉子節點。程式參數傳遞時,被傳遞的值本身都是不能進行修改的,但是,如果這個值是一個非葉子節點(即一個對象引用),則可以修改這個節點下面的所有内容。

堆和棧中,棧是程式運作最根本的東西。程式運作可以沒有堆,但是不能沒有棧。而堆是為棧進行資料存儲服務,說白了堆就是一塊共享的記憶體。不過,正是因為堆和棧的分離的思想,才使得Java的垃圾回收成為可能。

Java中,棧的大小通過-Xss來設定,當棧中存儲資料比較多時,需要适當調大這個值,否則會出現java.lang.StackOverflowError異常。常見的出現這個異常的是無法傳回的遞歸,因為此時棧中儲存的資訊都是方法傳回的記錄點。

Java對象的大小

基本資料的類型的大小是固定的,這裡就不多說了。對于非基本類型的Java對象,其大小就值得商榷。

在Java中,一個空Object對象的大小是8byte,這個大小隻是儲存堆中一個沒有任何屬性的對象的大小。看下面語句:Object ob = new Object();

這樣在程式中完成了一個Java對象的生命,但是它所占的空間為:4byte+8byte。4byte是上面部分所說的Java棧中儲存引用的所需要的空間。而那8byte則是Java堆中對象的資訊。因為所有的Java非基本類型的對象都需要預設繼承Object對象,是以不論什麼樣的Java對象,其大小都必須是大于8byte。

有了Object對象的大小,我們就可以計算其他對象的大小了。Class NewObject {

int count;

boolean flag;

Object ob;

}

其大小為:空對象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。但是因為Java在對對象記憶體配置設定時都是以8的整數倍來分,是以大于17byte的最接近8的整數倍的是24,是以此對象的大小為24byte。

這裡需要注意一下基本類型的包裝類型的大小。因為這種包裝類型已經成為對象了,是以需要把他們作為對象來看待。包裝類型的大小至少是12byte(聲明一個空Object至少需要的空間),而且12byte沒有包含任何有效資訊,同時,因為Java對象大小是8的整數倍,是以一個基本類型包裝類的大小至少是16byte。這個記憶體占用是很恐怖的,它是使用基本類型的N倍(N>2),有些類型的記憶體占用更是誇張(随便想下就知道了)。是以,可能的話應盡量少使用包裝類。在JDK5.0以後,因為加入了自動類型裝換,是以,Java虛拟機會在存儲方面進行相應的優化。

引用類型

對象引用類型分為強引用、軟引用、弱引用和虛引用。

強引用:就是我們一般聲明對象是時虛拟機生成的引用,強引用環境下,垃圾回收時需要嚴格判斷目前對象是否被強引用,如果被強引用,則不會被垃圾回收

軟引用:軟引用一般被做為緩存來使用。與強引用的差別是,軟引用在垃圾回收時,虛拟機會根據目前系統的剩餘記憶體來決定是否對軟引用進行回收。如果剩餘記憶體比較緊張,則虛拟機會回收軟引用所引用的空間;如果剩餘記憶體相對富裕,則不會進行回收。換句話說,虛拟機在發生OutOfMemory時,肯定是沒有軟引用存在的。

弱引用:弱引用與軟引用類似,都是作為緩存來使用。但與軟引用不同,弱引用在進行垃圾回收時,是一定會被回收掉的,是以其生命周期隻存在于一個垃圾回收周期内。

強引用不用說,我們系統一般在使用時都是用的強引用。而“軟引用”和“弱引用”比較少見。他們一般被作為緩存使用,而且一般是在記憶體大小比較受限的情況下做為緩存。因為如果記憶體足夠大的話,可以直接使用強引用作為緩存即可,同時可控性更高。因而,他們常見的是被使用在桌面應用系統的緩存。

可以從不同的的角度去劃分垃圾回收算法:

按照基本回收政策分

引用計數(Reference Counting):

比較古老的回收算法。原理是此對象有一個引用,即增加一個計數,删除一個引用則減少一個計數。垃圾回收時,隻用收集計數為0的對象。此算法最緻命的是無法處理循環引用的問題。

标記-清除(Mark-Sweep):

JAVA記憶體讀寫速度極限_JVM調優總結

此算法執行分兩階段。第一階段從引用根節點開始标記所有被引用的對象,第二階段周遊整個堆,把未标記的對象清除。此算法需要暫停整個應用,同時,會産生記憶體碎片。

複制(Copying):

JAVA記憶體讀寫速度極限_JVM調優總結

此算法把記憶體空間劃為兩個相等的區域,每次隻使用其中一個區域。垃圾回收時,周遊目前使用區域,把正在使用中的對象複制到另外一個區域中。次算法每次隻處理正在使用中的對象,是以複制成本比較小,同時複制過去以後還能進行相應的記憶體整理,不會出現“碎片”問題。當然,此算法的缺點也是很明顯的,就是需要兩倍記憶體空間。

标記-整理(Mark-Compact):

JAVA記憶體讀寫速度極限_JVM調優總結

此算法結合了“标記-清除”和“複制”兩個算法的優點。也是分兩階段,第一階段從根節點開始标記所有被引用對象,第二階段周遊整個堆,把清除未标記對象并且把存活對象“壓縮”到堆的其中一塊,按順序排放。此算法避免了“标記-清除”的碎片問題,同時也避免了“複制”算法的空間問題。

按分區對待的方式分

增量收集(Incremental Collecting):實時垃圾回收算法,即:在應用進行的同時進行垃圾回收。不知道什麼原因JDK5.0中的收集器沒有使用這種算法的。

分代收集(Generational Collecting):基于對對象生命周期分析後得出的垃圾回收算法。把對象分為年青代、年老代、持久代,對不同生命周期的對象使用不同的算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此算法的。

按系統線程分

串行收集:串行收集使用單線程處理所有垃圾回收工作, 因為無需多線程互動,實作容易,而且效率比較高。但是,其局限性也比較明顯,即無法使用多處理器的優勢,是以此收集适合單處理器機器。當然,此收集器也可以用在小資料量(100M左右)情況下的多處理器機器上。

并行收集:并行收集使用多線程處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數目越多,越能展現出并行收集器的優勢。

并發收集:相對于串行收集和并行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個運作環境,而隻有垃圾回收程式在運作,是以,系統在垃圾回收時會有明顯的暫停,而且暫停時間會因為堆越大而越長。

如何區分垃圾

上面說到的“引用計數”法,通過統計控制生成對象和删除對象時的引用數來判斷。垃圾回收程式收集計數為0的對象即可。但是這種方法無法解決循環引用。是以,後來實作的垃圾判斷算法中,都是從程式運作的根節點出發,周遊整個對象引用,查找存活的對象。那麼在這種方式的實作中,垃圾回收從哪兒開始的呢?即,從哪兒開始查找哪些對象是正在被目前系統使用的。上面分析的堆和棧的差別,其中棧是真正進行程式執行地方,是以要擷取哪些對象正在被使用,則需要從Java棧開始。同時,一個棧是與一個線程對應的,是以,如果有多個線程的話,則必須對這些線程對應的所有的棧進行檢查。

JAVA記憶體讀寫速度極限_JVM調優總結

同時,除了棧外,還有系統運作時的寄存器等,也是存儲程式運作資料的。這樣,以棧或寄存器中的引用為起點,我們可以找到堆中的對象,又從這些對象找到對堆中其他對象的引用,這種引用逐漸擴充,最終以null引用或者基本類型結束,這樣就形成了一顆以Java棧中引用所對應的對象為根節點的一顆對象樹,如果棧中有多個引用,則最終會形成多顆對象樹。在這些對象樹上的對象,都是目前系統運作所需要的對象,不能被垃圾回收。而其他剩餘對象,則可以視為無法被引用到的對象,可以被當做垃圾進行回收。

是以,垃圾回收的起點是一些根對象(java棧, 靜态變量, 寄存器...)。而最簡單的Java棧就是Java程式執行的main函數。這種回收方式,也是上面提到的“标記-清除”的回收方式

如何處理碎片

由于不同Java對象存活時間是不一定的,是以,在程式運作一段時間以後,如果不進行記憶體整理,就會出現零散的記憶體碎片。碎片最直接的問題就是會導緻無法配置設定大塊的記憶體空間,以及程式運作效率降低。是以,在上面提到的基本垃圾回收算法中,“複制”方式和“标記-整理”方式,都可以解決碎片的問題。

如何解決同時存在的對象建立和對象回收問題

垃圾回收線程是回收記憶體的,而程式運作線程則是消耗(或配置設定)記憶體的,一個回收記憶體,一個配置設定記憶體,從這點看,兩者是沖突的。是以,在現有的垃圾回收方式中,要進行垃圾回收前,一般都需要暫停整個應用(即:暫停記憶體的配置設定),然後進行垃圾回收,回收完成後再繼續應用。這種實作方式是最直接,而且最有效的解決二者沖突的方式。

但是這種方式有一個很明顯的弊端,就是當堆空間持續增大時,垃圾回收的時間也将會相應的持續增大,對應應用暫停的時間也會相應的增大。一些對相應時間要求很高的應用,比如最大暫停時間要求是幾百毫秒,那麼當堆空間大于幾個G時,就很有可能超過這個限制,在這種情況下,垃圾回收将會成為系統運作的一個瓶頸。為解決這種沖突,有了并發垃圾回收算法,使用這種算法,垃圾回收線程與程式運作線程同時運作。在這種方式下,解決了暫停的問題,但是因為需要在新生成對象的同時又要回收對象,算法複雜性會大大增加,系統的處理能力也會相應降低,同時,“碎片”問題将會比較難解決。

為什麼要分代

分代的垃圾回收政策,是基于這樣一個事實:不同的對象的生命周期是不一樣的。是以,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。

在Java程式運作的過程中,會産生大量的對象,其中有些對象是與業務資訊相關,比如Http請求中的Session對象、線程、Socket連接配接,這類對象跟業務直接挂鈎,是以生命周期比較長。但是還有一些對象,主要是程式運作過程中生成的臨時變量,這些對象生命周期會比較短,比如:String對象,由于其不變類的特性,系統會産生大量的這些對象,有些對象甚至隻用一次即可回收。

試想,在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因為每次回收都需要周遊所有存活對象,但實際上,對于生命周期長的對象而言,這種周遊是沒有效果的,因為可能進行了很多次周遊,但是他們依舊存在。是以,分代垃圾回收采用分治的思想,進行代的劃分,把不同生命周期的對象放在不同代上,不同代上采用最适合它的垃圾回收方式進行回收。

如何分代

JAVA記憶體讀寫速度極限_JVM調優總結

如圖所示:

虛拟機中的共劃分為三個代:年輕代(Young Generation)、年老點(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類資訊,與垃圾收集要收集的Java對象關系不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。

年輕代:

所有新生成的對象首先都是放在年輕代的。年輕代的目标就是盡可能快速的收集掉那些生命周期短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(一般而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象将被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象将被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複制過來的并且此時還存活的對象,将被複制“年老區(Tenured)”。需要注意,Survivor的兩個區是對稱的,沒先後關系,是以同一個區中可能同時存在從Eden複制過來 對象,和從前一個Survivor複制過來的對象,而複制到年老區的隻有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空的。同時,根據程式需要,Survivor區是可以配置為多個的(多于兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。

年老代:

在年輕代中經曆了N次垃圾回收後仍然存活的對象,就會被放到年老代中。是以,可以認為年老代中存放的都是一些生命周期較長的對象。

持久代:

用于存放靜态檔案,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動态生成或者調用一些class,例如Hibernate等,在這種時候需要設定一個比較大的持久代空間來存放這些運作過程中新增的類。持久代大小通過-XX:MaxPermSize= 進行設定。

什麼情況下觸發垃圾回收

由于對象進行了分代處理,是以垃圾回收區域、時間也不一樣。GC有兩種類型:Scavenge GC和Full GC。

Scavenge GC

一般情況下,當新對象生成,并且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,并且把尚且存活的對象移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分對象都是從Eden區開始的,同時Eden區不會配置設定的很大,是以Eden區的GC會頻繁進行。因而,一般在這裡需要使用速度快、效率高的算法,使Eden去能盡快空閑出來。

Full GC

對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個對進行回收,是以比Scavenge GC要慢,是以應該盡可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對于FullGC的調節。有如下原因可能導緻Full GC:· 年老代(Tenured)被寫滿

· 持久代(Perm)被寫滿

· System.gc()被顯示調用

·上一次GC之後Heap的各域配置設定政策動态變化

分代垃圾回收流程示意

JAVA記憶體讀寫速度極限_JVM調優總結
JAVA記憶體讀寫速度極限_JVM調優總結
JAVA記憶體讀寫速度極限_JVM調優總結
JAVA記憶體讀寫速度極限_JVM調優總結

選擇合适的垃圾收集算法

JAVA記憶體讀寫速度極限_JVM調優總結

用單線程處理所有垃圾回收工作,因為無需多線程互動,是以效率比較高。但是,也無法使用多處理器的優勢,是以此收集器适合單處理器機器。當然,此收集器也可以用在小資料量(100M左右)情況下的多處理器機器上。可以使用-XX:+UseSerialGC打開。

JAVA記憶體讀寫速度極限_JVM調優總結

對年輕代進行并行垃圾回收,是以可以減少垃圾回收時間。一般在多線程多處理器機器上使用。使用-XX:+UseParallelGC.打開。并行收集器在J2SE5.0第六6更新上引入,在Java SE6.0中進行了增強--可以對年老代進行并行收集。如果年老代不使用并發收集的話,預設是使用單線程進行垃圾回收,是以會制約擴充能力。使用-XX:+UseParallelOldGC打開。

使用-XX:ParallelGCThreads= 設定并行垃圾回收的線程數。此值可以設定與機器處理器數量相等。

此收集器可以進行如下配置:最大垃圾回收暫停:指定垃圾回收時的最長暫停時間,通過-XX:MaxGCPauseMillis=\(

吞吐量:吞吐量為垃圾回收時間與非垃圾回收時間的比值,通過-XX:GCTimeRatio=\(

并發收集器

可以保證大部分工作都并發進行(應用不停止),垃圾回收隻暫停很少的時間,此收集器适合對響應時間要求比較高的中、大規模應用。使用-XX:+UseConcMarkSweepGC打開。

JAVA記憶體讀寫速度極限_JVM調優總結

并發收集器主要減少年老代的暫停時間,他在應用不停止的情況下使用獨立的垃圾回收線程,跟蹤可達對象。在每個年老代垃圾回收周期中,在收集初期并發收集器 會對整個應用進行簡短的暫停,在收集中還會再暫停一次。第二次暫停會比第一次稍長,在此過程中多個線程同時進行垃圾回收工作。

并發收集器使用處理器換來短暫的停頓時間。在一個N個處理器的系統上,并發收集部分使用K/N個可用處理器進行回收,一般情況下1<=K<=N/4。

在隻有一個處理器的主機上使用并發收集器,設定為incremental mode模式也可獲得較短的停頓時間。

浮動垃圾:由于在應用運作的同時進行垃圾回收,是以有些垃圾可能在垃圾回收進行完成時産生,這樣就造成了“Floating Garbage”,這些垃圾需要在下次垃圾回收周期時才能回收掉。是以,并發收集器一般需要20%的預留白間用于這些浮動垃圾。

Concurrent Mode Failure:并發收集器在應用運作時進行收集,是以需要保證堆在垃圾回收的這段時間有足夠的空間供程式使用,否則,垃圾回收還未完成,堆空間先滿了。這種情況下将會發生“并發模式失敗”,此時整個應用将會暫停,進行垃圾回收。

啟動并發收集器:因為并發收集在應用運作時進行收集,是以必須保證收集完成之前有足夠的記憶體空間供程式使用,否則會出現“Concurrent Mode Failure”。通過設定-XX:CMSInitiatingOccupancyFraction= 指定還有多少剩餘堆時開始執行并發收集

小結

串行處理器:适用情況:資料量比較小(100M左右);單處理器下并且對響應時間無要求的應用。

缺點:隻能用于小型應用

并行處理器:适用情況:“對吞吐量有高要求”,多CPU、對應用響應時間無要求的中、大型應用。舉例:背景處理、科學計算。

缺點:垃圾收集過程中應用響應時間可能加長

并發處理器:适用情況:“對響應時間有高要求”,多CPU、對應用響應時間有較高要求的中、大型應用。舉例:Web伺服器/應用伺服器、電信交換、內建開發環境。

以下配置主要針對分代垃圾回收算法而言。

堆大小設定

年輕代的設定很關鍵

JVM中最大堆大小有三方面限制:相關作業系統的資料模型(32-bt還是64-bit)限制;系統的可用虛拟記憶體限制;系統的可用實體記憶體限制。32位系統下,一般限制在1.5G~2G;64為作業系統對記憶體無限制。在Windows Server 2003 系統,3.5G實體記憶體,JDK5.0下測試,最大可設定為1478m。

典型設定:java -Xmx3550m -Xms3550m -Xmn2g –Xss128k

-Xmx3550m:設定JVM最大可用記憶體為3550M。

-Xms3550m:設定JVM促使記憶體為3550m。此值可以設定與-Xmx相同,以避免每次垃圾回收完成後JVM重新配置設定記憶體。

-Xmn2g:設定年輕代大小為2G。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m,是以增大年輕代後,将會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。

-Xss128k:設定每個線程的堆棧大小。JDK5.0以後每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。更具應用的線程所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的線程。但是作業系統對一個程序内的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4:設定年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設定為4,則年輕代與年老代所占比值為1:4,年輕代占整個堆棧的1/5

-XX:SurvivorRatio=4:設定年輕代中Eden區與Survivor區的大小比值。設定為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/6

-XX:MaxPermSize=16m:設定持久代大小為16m。

-XX:MaxTenuringThreshold=0:設定垃圾最大年齡。如果設定為0的話,則年輕代對象不經過Survivor區,直接進入年老代。對于年老代比較多的應用,可以提高效率。如果将此值設定為一個較大值,則年輕代對象會在Survivor區進行多次複制,這樣可以增加對象再年輕代的存活時間,增加在年輕代即被回收的概論。

回收器選擇

JVM給了三種選擇:串行收集器、并行收集器、并發收集器,但是串行收集器隻适用于小資料量的情況,是以這裡的選擇主要針對并行收集器和并發收集器。預設情況下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在啟動時加入相應參數。JDK5.0以後,JVM會根據目前[系統配置][Link 1]進行判斷。

吞吐量優先的并行收集器

如上文所述,并行收集器主要以到達一定的吞吐量為目标,适用于科學技術和背景處理等。

典型配置:java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

-XX:+UseParallelGC:選擇垃圾收集器為并行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用并發收集,而年老代仍舊使用串行收集。

-XX:ParallelGCThreads=20:配置并行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等。java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

-XX:+UseParallelOldGC:配置年老代垃圾收集方式為并行收集。JDK6.0支援對年老代并行收集。java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=100:設定每次年輕代垃圾回收的最長時間,如果無法滿足此時間,JVM會自動調整年輕代大小,以滿足此值。n java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

-XX:+UseAdaptiveSizePolicy:設定此選項後,并行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目标系統規定的最低相應時間或者收集頻率等,此值建議使用并行收集器時,一直打開。

響應時間優先的并發收集器

如上文所述,并發收集器主要是保證系統的響應時間,減少垃圾收集時的停頓時間。适用于應用伺服器、電信領域等。

典型配置:java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:設定年老代為并發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明。是以,此時年輕代大小最好用-Xmn設定。

-XX:+UseParNewGC: 設定年輕代為并行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設定,是以無需再設定此值。java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction:由于并發收集器不對記憶體空間進行壓縮、整理,是以運作一段時間以後會産生“碎片”,使得運作效率降低。此值設定運作多少次GC以後對記憶體空間進行壓縮、整理。

-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除碎片

輔助資訊

JVM提供了大量指令行參數,列印資訊,供調試使用。主要有以下一些:

-XX:+PrintGC:輸出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails:輸出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可與上面兩個混合使用

輸出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationConcurrentTime:列印每次垃圾回收前,程式未中斷的執行時間。可與上面混合使用。輸出形式:Application time: 0.5291524 seconds

-XX:+PrintGCApplicationStoppedTime:列印垃圾回收期間程式暫停的時間。可與上面混合使用。輸出形式:Total time for which application threads were stopped: 0.0468229 seconds

-XX:PrintHeapAtGC: 列印GC前後的詳細堆棧資訊。輸出形式:

34.702: [GC {Heap before gc invocations=7:

def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)

eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)

from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)

to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)

tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)

the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)

compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:

def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)

eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)

from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)

to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)

tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)

the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)

compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

}

, 0.0757599 secs]

-Xloggc:filename:與上面幾個配合使用,把相關日志資訊記錄到檔案以便分析。

常見配置彙總

堆設定

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:設定年輕代大小

-XX:NewRatio=n:設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4

-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5

-XX:MaxPermSize=n:設定持久代大小

收集器設定

-XX:+UseSerialGC:設定串行收集器

-XX:+UseParallelGC:設定并行收集器

-XX:+UseParalledlOldGC:設定并行年老代收集器

-XX:+UseConcMarkSweepGC:設定并發收集器

垃圾回收統計資訊

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

并行收集器設定

-XX:ParallelGCThreads=n:設定并行收集器收集時使用的CPU數。并行收集線程數。

-XX:MaxGCPauseMillis=n:設定并行收集最大暫停時間

-XX:GCTimeRatio=n:設定垃圾回收時間占程式運作時間的百分比。公式為1/(1+n)

并發收集器設定

-XX:+CMSIncrementalMode:設定為增量模式。适用于單CPU情況。

-XX:ParallelGCThreads=n:設定并發收集器年輕代收集方式為并行收集時,使用的CPU數。并行收集線程數。

調優總結

年輕代大小選擇

響應時間優先的應用:盡可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的對象。

吞吐量優先的應用:盡可能的設定大,可能到達Gbit的程度。因為對響應時間沒有要求,垃圾收集可以并行進行,一般适合8CPU以上的應用。

年老代大小選擇

響應時間優先的應用:年老代使用并發收集器,是以其大小需要小心設定,一般要考慮并發會話率和會話持續時間等一些參數。如果堆設定小了,可以會造成記憶體碎片、高回收頻率以及應用暫停而使用傳統的标記清除方式;如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下資料獲得:并發垃圾收集資訊

持久代并發收集次數

傳統GC資訊

花在年輕代和年老代回收上的時間比例

減少年輕代和年老代花費的時間,一般會提高應用的效率

吞吐量優先的應用

一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以盡可能回收掉大部分短期對象,減少中期的對象,而年老代盡存放長期存活對象。

較小堆引起的碎片問題

因為年老代的并發收集器使用标記、清除算法,是以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合并,這樣可以配置設定給較大的對象。但是,當堆空間較小時,運作一段時間以後,就會出現“碎片”,如果并發收集器找不到足夠的空間,那麼并發收集器将會停止,然後使用傳統的标記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:-XX:+UseCMSCompactAtFullCollection:使用并發收集器時,開啟對年老代的壓縮。

-XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下,這裡設定多少次Full GC後,對年老代進行壓縮

垃圾回收的瓶頸

傳統分代垃圾回收方式,已經在一定程度上把垃圾回收給應用帶來的負擔降到了最小,把應用的吞吐量推到了一個極限。但是他無法解決的一個問題,就是Full GC所帶來的應用暫停。在一些對實時性要求很高的應用場景下,GC暫停所帶來的請求堆積和請求失敗是無法接受的。這類應用可能要求請求的傳回時間在幾百甚至幾十毫秒以内,如果分代垃圾回收方式要達到這個名額,隻能把最大堆的設定限制在一個相對較小範圍内,但是這樣有限制了應用本身的處理能力,同樣也是不可接收的。

分代垃圾回收方式确實也考慮了實時性要求而提供了并發回收器,支援最大暫停時間的設定,但是受限于分代垃圾回收的記憶體劃分模型,其效果也不是很理想。

為了達到實時性的要求(其實Java語言最初的設計也是在嵌入式系統上的),一種新垃圾回收方式呼之欲出,它既支援短的暫停時間,又支援大的記憶體空間配置設定。可以很好的解決傳統分代方式帶來的問題。

增量收集的演進

增量收集的方式在理論上可以解決傳統分代方式帶來的問題。增量收集把對堆空間劃分成一系列記憶體塊,使用時,先使用其中一部分(不會全部用完),垃圾收集時把之前用掉的部分中的存活對象再放到後面沒有用的空間中,這樣可以實作一直邊使用邊收集的效果,避免了傳統分代方式整個使用完了再暫停的回收的情況。

當然,傳統分代收集方式也提供了并發收集,但是他有一個很緻命的地方,就是把整個堆做為一個記憶體塊,這樣一方面會造成碎片(無法壓縮),另一方面他的每次收集都是對整個堆的收集,無法進行選擇,在暫停時間的控制上還是很弱。而增量方式,通過記憶體空間的分塊,恰恰可以解決上面問題。

Garbage Firest(G1)

這部分的内容主要參考[這裡][Link 2],這篇文章算是對G1算法論文的解讀。我也沒加什麼東西了。

目标

從設計目标看G1完全是為了大型應用而準備的。支援很大的堆

高吞吐量

--支援多CPU和垃圾回收線程

--在主線程暫停的情況下,使用并行收集

--在主線程運作的情況下,使用并發收集

實時目标:可配置在N毫秒内最多隻占用M毫秒的時間進行垃圾回收

當然G1要達到實時性的要求,相對傳統的分代回收算法,在性能上會有一些損失。

算法詳解

JAVA記憶體讀寫速度極限_JVM調優總結

G1可謂博采衆家之長,力求到達一種完美。他吸取了增量收集優點,把整個堆劃分為一個一個等大小的區域(region)。記憶體的回收和劃分都以region為機關;同時,他也吸取了CMS的特點,把這個垃圾回收過程分為幾個階段,分散一個垃圾回收過程;而且,G1也認同分代垃圾回收的思想,認為不同對象的生命周期不同,可以采取不同收集方式,是以,它也支援分代的垃圾回收。為了達到對回收時間的可預計性,G1在掃描了region以後,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間(要複制的活躍對象少了),因為活躍對象小,裡面可以認為多數都是垃圾,是以這種方式被稱為Garbage First(G1)的垃圾回收算法,即:垃圾優先的回收。

回收步驟:

初始标記(Initial Marking)

G1對于每個region都儲存了兩個辨別用的bitmap,一個為previous marking bitmap,一個為next marking bitmap,bitmap中包含了一個bit的位址資訊來指向對象的起始點。

開始Initial Marking之前,首先并發的清空next marking bitmap,然後停止所有應用線程,并掃描辨別出每個region中root可直接通路到的對象,将region中top的值放入next top at mark start(TAMS)中,之後恢複所有應用線程。

觸發這個步驟執行的條件為:G1定義了一個JVM Heap大小的百分比的閥值,稱為h,另外還有一個H,H的值為(1-h)*Heap Size,目前這個h的值是固定的,後續G1也許會将其改為動态的,根據jvm的運作情況來動态的調整,在分代方式下,G1還定義了一個u以及soft limit,soft limit的值為H-u*Heap Size,當Heap中使用的記憶體超過了soft limit值時,就會在一次clean up執行完畢後在應用允許的GC暫停時間範圍内盡快的執行此步驟;

在pure方式下,G1将marking與clean up組成一個環,以便clean up能充分的使用marking的資訊,當clean up開始回收時,首先回收能夠帶來最多記憶體空間的regions,當經過多次的clean up,回收到沒多少空間的regions時,G1重新初始化一個新的marking與clean up構成的環。

并發标記(Concurrent Marking)

按照之前Initial Marking掃描到的對象進行周遊,以識别這些對象的下層對象的活躍狀态,對于在此期間應用線程并發修改的對象的以來關系則記錄到remembered set logs中,新建立的對象則放入比top值更高的位址區間中,這些新建立的對象預設狀态即為活躍的,同時修改top值。

最終标記暫停(Final Marking Pause)

當應用線程的remembered set logs未滿時,是不會放入filled RS buffers中的,在這樣的情況下,這些remebered set logs中記錄的card的修改就會被更新了,是以需要這一步,這一步要做的就是把應用線程中存在的remembered set logs的内容進行處理,并相應的修改remembered sets,這一步需要暫停應用,并行的運作。

存活對象計算及清除(Live Data Counting and Cleanup)

值得注意的是,在G1中,并不是說Final Marking Pause執行完了,就肯定執行Cleanup這步的,由于這步需要暫停應用,G1為了能夠達到準實時的要求,需要根據使用者指定的最大的GC造成的暫停時間來合理的規劃什麼時候執行Cleanup,另外還有幾種情況也是會觸發這個步驟的執行的:G1采用的是複制方法來進行收集,必須保證每次的”to space”的空間都是夠的,是以G1采取的政策是當已經使用的記憶體空間達到了H時,就執行Cleanup這個步驟;

對于full-young和partially-young的分代模式的G1而言,則還有情況會觸發Cleanup的執行,full-young模式下,G1根據應用可接受的暫停時間、回收young regions需要消耗的時間來估算出一個yound regions的數量值,當JVM中配置設定對象的young regions的數量達到此值時,Cleanup就會執行;partially-young模式下,則會盡量頻繁的在應用可接受的暫停時間範圍内執行Cleanup,并最大限度的去執行non-young regions的Cleanup。

展望

以後JVM的調優或許跟多需要針對G1算法進行調優了。

JVM調優工具

Jconsole,jProfile,VisualVM

Jconsole : jdk自帶,功能簡單,但是可以在系統有一定負荷的情況下使用。對垃圾回收算法有很詳細的跟蹤。

JProfiler:商業軟體,需要付費。功能強大。

VisualVM:JDK自帶,功能強大,與JProfiler類似。推薦。

如何調優

觀察記憶體釋放情況、集合類檢查、對象樹

上面這些調優工具都提供了強大的功能,但是總的來說一般分為以下幾類功能

堆資訊檢視

JAVA記憶體讀寫速度極限_JVM調優總結

可檢視堆空間大小配置設定(年輕代、年老代、持久代配置設定)

提供即時的垃圾回收功能

垃圾監控(長時間監控回收情況)

JAVA記憶體讀寫速度極限_JVM調優總結

檢視堆内類、對象資訊檢視:數量、類型等

JAVA記憶體讀寫速度極限_JVM調優總結

對象引用情況檢視

有了堆資訊檢視方面的功能,我們一般可以順利解決以下問題:年老代年輕代大小劃分是否合理

記憶體洩漏

垃圾回收算法設定是否合理

線程監控

JAVA記憶體讀寫速度極限_JVM調優總結

線程資訊監控:系統線程數量。

線程狀态監控:各個線程都處在什麼樣的狀态下

JAVA記憶體讀寫速度極限_JVM調優總結

Dump線程詳細資訊:檢視線程内部運作情況

死鎖檢查

熱點分析

JAVA記憶體讀寫速度極限_JVM調優總結

CPU熱點:檢查系統哪些方法占用的大量CPU時間

記憶體熱點:檢查哪些對象在系統中數量最大(一定時間記憶體活對象和銷毀對象一起統計)

這兩個東西對于系統優化很有幫助。我們可以根據找到的熱點,有針對性的進行系統的瓶頸查找和進行系統優化,而不是漫無目的的進行所有代碼的優化。

快照

快照是系統運作到某一時刻的一個定格。在我們進行調優的時候,不可能用眼睛去跟蹤所有系統變化,依賴快照功能,我們就可以進行系統兩個不同運作時刻,對象(或類、線程等)的不同,以便快速找到問題

舉例說,我要檢查系統進行垃圾回收以後,是否還有該收回的對象被遺漏下來的了。那麼,我可以在進行垃圾回收前後,分别進行一次堆情況的快照,然後對比兩次快照的對象情況。

記憶體洩漏檢查

記憶體洩漏是比較常見的問題,而且解決方法也比較通用,這裡可以重點說一下,而線程、熱點方面的問題則是具體問題具體分析了。

記憶體洩漏一般可以了解為系統資源(各方面的資源,堆、棧、線程等)在錯誤使用的情況下,導緻使用完畢的資源無法回收(或沒有回收),進而導緻新的資源配置設定請求無法完成,引起系統錯誤。

記憶體洩漏對系統危害比較大,因為他可以直接導緻系統的崩潰。

需要差別一下,記憶體洩漏和系統超負荷兩者是有差別的,雖然可能導緻的最終結果是一樣的。記憶體洩漏是用完的資源沒有回收引起錯誤,而系統超負荷則是系統确實沒有那麼多資源可以配置設定了(其他的資源都在使用)。

年老代堆空間被占滿

異常: java.lang.OutOfMemoryError: Java heap space

說明:

JAVA記憶體讀寫速度極限_JVM調優總結

這是最典型的記憶體洩漏方式,簡單說就是所有堆空間都被無法回收的垃圾對象占滿,虛拟機無法再在配置設定新空間。

如上圖所示,這是非常典型的記憶體洩漏的垃圾回收情況圖。所有峰值部分都是一次垃圾回收點,所有谷底部分表示是一次垃圾回收後剩餘的記憶體。連接配接所有谷底的點,可以發現一條由底到高的線,這說明,随時間的推移,系統的堆空間被不斷占滿,最終會占滿整個堆空間。是以可以初步認為系統内部可能有記憶體洩漏。(上面的圖僅供示例,在實際情況下收集資料的時間需要更長,比如幾個小時或者幾天)

解決:

這種方式解決起來也比較容易,一般就是根據垃圾回收前後情況對比,同時根據對象引用情況(常見的集合對象引用)分析,基本都可以找到洩漏點。

持久代被占滿

異常:java.lang.OutOfMemoryError: PermGen space

說明:

Perm空間被占滿。無法為新的class配置設定存儲空間而引發的異常。這個異常以前是沒有的,但是在Java反射大量使用的今天這個異常比較常見了。主要原因就是大量動态反射生成的類不斷被加載,最終導緻Perm區被占滿。

更可怕的是,不同的classLoader即便使用了相同的類,但是都會對其進行加載,相當于同一個東西,如果有N個classLoader那麼他将會被加載N次。是以,某些情況下,這個問題基本視為無解。當然,存在大量classLoader和大量反射類的情況其實也不多。

解決:-XX:MaxPermSize=16m

換用JDK。比如JRocket。

堆棧溢出

異常:java.lang.StackOverflowError

說明:這個就不多說了,一般就是遞歸沒傳回,或者循環調用造成

線程堆棧滿

異常:Fatal: Stack size too small

說明:java中一個線程的空間大小是有限制的。JDK5.0以後這個值是1M。與這個線程相關的資料将會儲存在其中。但是當線程空間滿了以後,将會出現上面異常。

解決:增加線程棧大小。-Xss2m。但這個配置無法解決根本問題,還要看代碼部分是否有造成洩漏的部分。

系統記憶體被占滿

異常:java.lang.OutOfMemoryError: unable to create new native thread

說明:

這個異常是由于作業系統沒有足夠的資源來産生這個線程造成的。系統建立線程時,除了要在Java堆中配置設定記憶體外,作業系統本身也需要配置設定資源來建立線程。是以,當線程數量大到一定程度以後,堆中或許還有空間,但是作業系統配置設定不出資源來了,就出現這個異常了。

配置設定給Java虛拟機的記憶體愈多,系統剩餘的資源就越少,是以,當系統記憶體固定時,配置設定給Java虛拟機的記憶體越多,那麼,系統總共能夠産生的線程也就越少,兩者成反比的關系。同時,可以通過修改-Xss來減少配置設定給單個線程的空間,也可以增加系統總共内生産的線程數。

解決:重新設計系統減少線程數量。

線程數量不能減少的情況下,通過-Xss減小單個線程大小。以便能生産更多的線程。

垃圾回收的悖論

所謂“成也蕭何敗蕭何”。Java的垃圾回收确實帶來了很多好處,為開發帶來了便利。但是在一些高性能、高并發的情況下,垃圾回收确成為了制約Java應用的瓶頸。目前JDK的垃圾回收算法,始終無法解決垃圾回收時的暫停問題,因為這個暫停嚴重影響了程式的相應時間,造成擁塞或堆積。這也是後續JDK增加G1算法的一個重要原因。

當然,上面是從技術角度出發解決垃圾回收帶來的問題,但是從系統設計方面我們就需要問一下了:我們需要配置設定如此大的記憶體空間給應用嗎?

我們是否能夠通過有效使用記憶體而不是通過擴大記憶體的方式來設計我們的系統呢?

我們的記憶體中都放了什麼

記憶體中需要放什麼呢?個人認為,記憶體中需要放的是你的應用需要在不久的将來再次用到到的東西。想想看,如果你在将來不用這些東西,何必放記憶體呢?放檔案、資料庫不是更好?這些東西一般包括:系統運作時業務相關的資料。比如web應用中的session、即時消息的session等。這些資料一般在一個使用者通路周期或者一個使用過程中都需要存在。

緩存。緩存就比較多了,你所要快速通路的都可以放這裡面。其實上面的業務資料也可以了解為一種緩存。

線程。

是以,我們是不是可以這麼認為,如果我們不把業務資料和緩存放在JVM中,或者把他們獨立出來,那麼Java應用使用時所需的記憶體将會大大減少,同時垃圾回收時間也會相應減少。

我認為這是可能的。

解決之道

資料庫、檔案系統

把所有資料都放入資料庫或者檔案系統,這是一種最為簡單的方式。在這種方式下,Java應用的記憶體基本上等于處理一次峰值并發請求所需的記憶體。資料的擷取都在每次請求時從資料庫和檔案系統中擷取。也可以了解為,一次業務通路以後,所有對象都可以進行回收了。

這是一種記憶體使用最有效的方式,但是從應用角度來說,這種方式很低效。

記憶體-硬碟映射

上面的問題是因為我們使用了檔案系統帶來了低效。但是如果我們不是讀寫硬碟,而是寫記憶體的話效率将會提高很多。

資料庫和檔案系統都是實實在在進行了持久化,但是當我們并不需要這樣持久化的時候,我們可以做一些變通——把記憶體當硬碟使。

記憶體-硬碟映射很好很強大,既用了緩存又對Java應用的記憶體使用又沒有影響。Java應用還是Java應用,他隻知道讀寫的還是檔案,但是實際上是記憶體。

這種方式兼得的Java應用與緩存兩方面的好處。memcached的廣泛使用也正是這一類的代表。

同一機器部署多個JVM

這也是一種很好的方式,可以分為縱拆和橫拆。縱拆可以了解為把Java應用劃分為不同子產品,各個子產品使用一個獨立的Java程序。而橫拆則是同樣功能的應用部署多個JVM。

通過部署多個JVM,可以把每個JVM的記憶體控制一個垃圾回收可以忍受的範圍内即可。但是這相當于進行了分布式的處理,其額外帶來的複雜性也是需要評估的。另外,也有支援分布式的這種JVM可以考慮,不要要錢哦:)

程式控制的對象生命周期

這種方式是理想當中的方式,目前的虛拟機還沒有,純屬假設。即:考慮由程式設計方式配置哪些對象在垃圾收集過程中可以直接跳過,減少垃圾回收線程周遊标記的時間。

這種方式相當于在程式設計的時候告訴虛拟機某些對象你可以在*時間後在進行收集或者由代碼辨別可以收集了(類似C、C++),在這之前你即便去周遊他也是沒有效果的,他肯定是還在被引用的。

這種方式如果JVM可以實作,個人認為将是一個飛躍,Java即有了垃圾回收的優勢,又有了C、C++對記憶體的可控性。

線程配置設定

Java的阻塞式的線程模型基本上可以抛棄了,目前成熟的NIO架構也比較多了。阻塞式IO帶來的問題是線程數量的線性增長,而NIO則可以轉換成為常數線程。是以,對于服務端的應用而言,NIO還是唯一選擇。不過,JDK7中為我們帶來的AIO是否能讓人眼前一亮呢?我們拭目以待。

其他的JDK

本文說的都是Sun的JDK,目前常見的JDK還有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高很多,不過Sun JDK6.0以後提高也很大。而且JRocket在垃圾回收方面,也具有優勢,其可設定垃圾回收的最大暫停時間也是很吸引人的。不過,系統Sun的G1實作以後,在這方面會有一個質的飛躍。能整理出上面一些東西,也是因為站在巨人的肩上。下面是一些參考資料,供大家學習,大家有更好的,可以繼續完善:)

· [Java 理論與實踐: 垃圾收集簡史][Java _]

· 《深入Java虛拟機》。雖然過去了很多年,但這本書依舊是經典。

這裡是本系列的最後一篇了,很高興大家能夠喜歡這系列的文章。期間也提了很多問題,其中有些是我之前沒有想到的或者考慮欠妥的,感謝提出這些問題的朋友,我也學到的不少東西。關注公衆号:java寶典