標籤:
深入Java核心 Java記憶體配置原理精講
引言:棧、堆、常量池雖同屬Java記憶體配置時操作的地區,但其適用範圍和功用卻大不相同。本文將深入Java核心,詳細講解Java記憶體配置方面的知識。
Java記憶體配置與管理是Java的核心技術之一,之前我們曾介紹過Java的記憶體管理與記憶體泄露以及Java記憶體回收方面的知識,今天我們再次深入Java核心,詳細介紹一下Java在記憶體配置方面的知識。一般Java在記憶體配置時會涉及到以下地區:
◆寄存器:我們在程式中無法控制
◆棧:存放基本類型的資料和對象的引用,但對象本身不存放在棧中,而是存放在堆中
◆堆:存放用new產生的資料
◆靜態域:存放在對象中用static定義的靜態成員
◆常量池:存放常量
◆非RAM儲存:硬碟等永久儲存空間
Java記憶體配置中的棧
在函數中定義的一些基本類型的變數資料和對象的引用變數都在函數的棧記憶體中分配。 當在一段代碼塊定義一個變數時,Java就在棧中 為這個變數分配記憶體空間,當該變數退出該範圍後,Java會自動釋放掉為該變數所分配的記憶體空間,該記憶體空間可以立即被另作他用。
Java記憶體配置中的堆
堆記憶體用來存放由new建立的對象和數組。 在堆中分配的記憶體,由Java虛擬機器的自動記憶體回收行程來管理。
在堆中產生了一個數組或對象後,還可以 在棧中定義一個特殊的變數,讓棧中這個變數的取值等於數組或對象在堆記憶體中的首地址,棧中的這個變數就成了數組或對象的引用變數。 引用變數就相當於是 為數組或對象起的一個名稱,以後就可以在程式中使用棧中的引用變數來訪問堆中的數組或對象。引用變數就相當於是為數組或者對象起的一個名稱。
引用變數是普通的變數,定義時在棧中分配,引用變數在程式運行到其範圍之外後被釋放。而數組和對象本身在堆中分配,即使程式 運行到使用 new 產生數組或者對象的語句所在的代碼塊之外,數組和對象本身佔據的記憶體不會被釋放,數組和對象在沒有引用變數指向它的
時候,才變為垃圾,不能在被使用,但仍 然佔據記憶體空間不放,在隨後的一個不確定的時間被記憶體回收行程收走(釋放掉)。這也是 Java 比較占記憶體的原因。
實際上,棧中的變數指向堆記憶體中的變數,這就是Java中的指標! 常量池 (constant pool)
常量池指的是在編譯期被確定,並被儲存在已編譯的.class檔案中的一些資料。除了包含代碼中所定義的各種基本類型(如int、long等等)和對象型(如String及數組)的常量值(final)還包含一些以文本形式出現的符號引用,比如:
◆類和介面的全限定名;
◆欄位的名稱和描述符;
◆方法和名稱和描述符。
虛擬機器必須為每個被裝載的類型維護一個常量池。常量池就是該類型所用到常量的一個有序集和,包括直接常量(string,integer和 floating point常量)和對其他類型,欄位和方法的符號引用。
對於String常量,它的值是在常量池中的。而JVM中的常量池在記憶體當中是以表的形式存在的, 對於String類型,有一張固定長度的CONSTANT_String_info表用來儲存文字字串值,注意:該表只儲存文字字串值,不儲存符號引 用。說到這裡,對常量池中的字串值的儲存位置應該有一個比較明了的理解了。 在程式執行的時候,常量池 會儲存在Method Area,而不是堆中。
堆與棧
Java的堆是一個運行時資料區,類的(對象從中分配空間。這些對象通過new、newarray、 anewarray和multianewarray等指令建立,它們不需要程式碼來顯式的釋放。堆是由記憶體回收來負責的,堆的優勢是可以動態地分配記憶體 大小,生存期也不必事先告訴編譯器,因為它是在運行時動態分配記憶體的,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由於要在運行時動態 分配記憶體,存取速度較慢。
棧的優勢是,存取速度比堆要快,僅次於寄存器,棧資料可以共用。但缺點是,存在棧中的資料大小與生存期必須是 確定的,缺乏靈活性。棧中主要存放一些基本類型的變數資料(int, short, long, byte, float, double, boolean, char)和物件控點(引用)。
棧有一個很重要的特殊性,就是存在棧中的資料可以共用。假設我們同時定義:
1. Int a = 3;
2. Int b = 3;
編譯器先處理int a = 3;首先它會在棧中建立一個變數為a的引用,然後尋找棧中是否有3這個值,如果沒找到,就將3存放進來,然後將a指向3。接著處理int b = 3;在建立
完b的引用變數後,因為在棧中已經有3這個值,便將b直接指向3。這樣,就出現了a與b同時均指向3的情況。
這時,如果再令 a=4;那麼編譯器會重新搜尋棧中是否有4值,如果沒有,則將4存放進來,並令a指向4;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響 到b的值。
要注意這種資料的共用與兩個對象的引用同時指向一個對象的這種共用是不同的,因為這種情況a的修改並不會影響到b, 它是由編譯器完成的,它有利於節省空間的。而一個對象引用變數修改了這個對象的內部狀態,會影響到另一個對象引用變數。
String是一個特殊的封裝類資料。可以用:
1. String str = new String("abc");
2. String str = "abc";
兩種的形式來建立,第一種是用new()來建立對象的,它會在存放於堆中。每調用一次就會建立一個新的對象。而第二種是先在棧中建立一個對String類的對象引用變數str,然後通過符號引用去字串常量池 裡找有沒有"abc",如果沒有,則將"abc"存放進字串常量池 ,並令str指向”abc”,如果已經有”abc” 則直接令str指向“abc”。
比較類裡面的數值是否相等時,用equals()方法;當測試兩個封裝類的引用是否指向同一個對象時,用==,下面用例子說明上面的理論。
1. String str1 = "abc";
2. String str2 = "abc";
3. System.out.println(str1==str2); //true
可以看出str1和str2是指向同一個對象的。
1. String str1 =new String ("abc");
2. String str2 =new String ("abc");
3. System.out.println(str1==str2); // false
用new的方式是產生不同的對象。每一次產生一個。
因此用第二種方式建立多個”abc”字串,在記憶體中 其實只存在一個對象而已. 這種寫法有利與節省記憶體空間. 同時它可以在一定程度上提高程式的運行速度,因為JVM會自動根據棧中資料的實際情況來決定是否有必要建立新對象。而對於String str = new String("abc");的代碼,則一概在堆中建立新對象,而不管其字串值是否相等,是否有必要建立新對象,從而加重了程式的負擔。
另 一方面, 要注意: 我們在使用諸如String str = "abc";的格式定義類時,總是想當然地認為,建立了String類的對象str。擔心陷阱!對象可能並沒有被建立!而可能只是指向一個先前已經建立的 對象。只有通過new()方法才能保證每次都建立一個新的對象。
由於String類的immutable性質,當String變數需要經常變換 其值時,應該考慮使用StringBuffer類,以提高程式效率。 1. 首先String不屬於8種基礎資料型別 (Elementary Data Type),String是一個對象。因為對象的預設值是null,所以String的預設值也是null;但它又是一種特殊的對象,有其它對象沒有的一些特性。
2. new String()和new String(”")都是申明一個新的Null 字元串,是空串不是null;
3. String str=”kvill”;String str=new String (”kvill”)的區別
樣本:
1. String s0="kvill";
2. String s1="kvill";
3. String s2="kv" + "ill";
4. System.out.println( s0==s1 );
5. System.out.println( s0==s2 );
結果為: true true
首先,我們要知結果為道Java 會確保一個字串常量只有一個拷貝。
因為例子中的 s0和s1中的”kvill”都是字串常量,它們在編譯期就被確定了,所以s0==s1為true;而”kv”和”ill”也都是字串常量,當一個字 符串由多個字串常量串連而成時,它自己肯定也是字串常量,所以s2也同樣在編譯期就被解析為一個字串常量,所以s2也是常量池中” kvill”的一個引用。所以我們得出s0==s1==s2;用new String() 建立的字串不是常量,不能在編譯期就確定,所以new String() 建立的字串不放入常量池中,它們有自己的地址空間。
樣本:
1. String s0="kvill";
2. String s1=new String("kvill");
3. String s2="kv" + new String("ill");
4. System.out.println( s0==s1 );
5. System.out.println( s0==s2 );
6. System.out.println( s1==s2 );
結果為: false
false false
例2中s0還是常量池 中"kvill”的應用,s1因為無法在編譯期確定,所以是運行時建立的新對象”kvill”的引用,s2因為有後半部分 new String(”ill”)所以也無法在編譯期確定,所以也是一個新建立對象”kvill”的應用;明白了這些也就知道為何得出此結果了。
4. String.intern():
再補充介紹一點:存在於.class檔案中的常量池,在運行期被JVM裝載,並且可以擴充。String的 intern()方法就是擴充常量池的 一個方法;當一個String執行個體str調用intern()方法時,Java 尋找常量池中 是否有相同Unicode的字串常量,如果有,則返回其的引用,如果沒有,則在常 量池中增加一個Unicode等於str的字串並返回它的引用;看樣本就清楚了
樣本:
1. String s0= "kvill";
2. String s1=new String("kvill");
3. String s2=new String("kvill");
4. System.out.println( s0==s1 );
5. System.out.println( "**********" );
6. s1.intern();
7. s2=s2.intern(); //把常量池中"kvill"的引用賦給s2
8. System.out.println( s0==s1);
9. System.out.println( s0==s1.intern() );
10. System.out.println( s0==s2 );
結果為: false false //雖然執行了s1.intern(),但它的傳回值沒有賦給s1 true //說明s1.intern()返回的是常量池中"kvill"的引用 true
最後我再破除一個錯誤的理解:有人說,“使用 String.intern() 方法則可以將一個 String 類的儲存到一個全域 String 表中 ,如果具有相同值的 Unicode 字串已經在這個表中,那麼該方法返回表中已有字串的地址,如果在表中沒有相同值的字串,則將自己的地址註冊到表中”如果我把他說的這個全域的 String 表理解為常量池的話,他的最後一句話,”如果在表中沒有相同值的字串,則將自己的地址註冊到表中”是錯的:
樣本:
1. String s1=new String("kvill");
2. String s2=s1.intern();
3. System.out.println( s1==s1.intern() );
4. System.out.println( s1+" "+s2 );
5. System.out.println( s2==s1.intern() );
結果: false kvill kvill true
在這個類中我們沒有聲名一個”kvill”常量,所以常量池中一開始是沒有”kvill”的,當我們調用s1.intern()後就在常量池中新添加了一 個”kvill”常量,原來的不在常量池中的”kvill”仍然存在,也就不是“將自己的地址註冊到常量池中”了。
s1==s1.intern() 為false說明原來的”kvill”仍然存在;s2現在為常量池中”kvill”的地址,所以有s2==s1.intern()為true。
5. 關於equals()和==:
這個對於String簡單來說就是比較兩字串的Unicode序列是否相當,如果相等返回true;而==是 比較兩字串的地址是否相同,也就是是否是同一個字串的引用。
6. 關於String是不可變的
這一說又要說很多,大家只 要知道String的執行個體一旦產生就不會再改變了,比如說:String str=”kv”+”ill”+” “+”ans”; 就是有4個字串常量,首先”kv”和”ill”產生了”kvill”存在記憶體中,然後”kvill”又和” ” 產生 “kvill “存在記憶體中,最後又和產生了”kvill ans”;並把這個字串的地址賦給了str,就是因為String的”不可變”產生了很多臨時變數,這也就是為什麼建議用StringBuffer的原 因了,因為StringBuffer是可改變的。
下面是一些String相關的常見問題:
String中的final用法和理解 final StringBuffer a = new StringBuffer("111"); final StringBuffer b = new StringBuffer("222"); a=b;//此句編譯不通過 final StringBuffer a = new StringBuffer("111"); a.append("222");// 編譯通過
可見,final只對引用的"值"(即記憶體位址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象 的變化,final是不負責的。
String常量池問題的幾個例子
下面是幾個常見例子的比較分析和理解:
1. String a = "a1";
2. String b = "a" + 1;
3. System.out.println((a == b)); //result = true
4. String a = "atrue";
5. String b = "a" + "true";
6. System.out.println((a == b)); //result = true
7. String a = "a3.4";
8. String b = "a" + 3.4;
9. System.out.println((a == b)); //result = true
分析:JVM對於字串常量的"+"號串連,將程式編譯期,JVM就將常量字串的"+"串連最佳化為串連後的值,拿"a" + 1來說,經編譯器最佳化後在class中就已經是a1。在編譯期其字串常量的值就確定下來,故上面程式最終的結果都為true。
1. String a = "ab";
2. String bb = "b";
3. String b = "a" + bb;
4. System.out.println((a == b)); //result = false
分析:JVM對於字串引用,由於在字串的"+"串連中,有字串引用存在,而引用的值在程式編譯期是無法確定的,即"a" + bb無法被編譯器最佳化,只有在程式運行期來動態分配並將串連後的新地址賦給b。所以上面程式的結果也就為false。
1. String a = "ab";
2. final String bb = "b";
3. String b = "a" + bb;
4. System.out.println((a == b)); //result = true
分析:和[3]中唯一不同的是bb字串加了final修飾,對於final修飾的變數,它在編譯時間被解析為常量值的一個本地拷貝儲存到自己的常量 池中或嵌入到它的位元組碼流中。所以此時的"a" + bb和"a" + "b"效果是一樣的。故上面程式的結果為true。
1. String a = "ab";
2. final String bb = getBB();
3. String b = "a" + bb;
4. System.out.println((a == b)); //result = false
5. private static String getBB() {
6. return "b";
7. }
分析:JVM對於字串引用bb,它的值在編譯期無法確定,只有在程式運行期調用方法後,將方法的傳回值和"a"來動態串連並分配地址為b,故上面 程式的結果為false。
通過上面4個例子可以得出得知:
String s = "a" + "b" + "c"; 就等價於String s = "abc"; String a = "a"; String b = "b"; String c = "c"; String s = a + b + c;
這個就不一樣了,最終結果等於:
1. StringBuffer temp = new StringBuffer();
2. temp.append(a).append(b).append(c);
3. String s = temp.toString();
由上面的分析結果,可就不難推斷出String 採用串連運算子(+)效率低下原因分析,形如這樣的代碼:
1. public class Test {
2. public static void main(String args[]) {
3. String s = null;
4. for(int i = 0; i < 100; i++) {
5. s += "a";
6. }
7. }
8. }
每做一次 + 就產生個StringBuilder對象,然後append後就扔掉。下次迴圈再到達時重新產生個StringBuilder對象,然後 append 字串,如此迴圈直至結束。如果我們直接採用 StringBuilder 對象進行 append 的話,我們可以節省 N - 1 次建立和銷毀對象的時間。所以對於在迴圈中要進行字串連線應用程式,一般都是用StringBuffer或StringBulider對象來進行 append操作。
String對象的intern方法理解和分析:
1. public class Test4 {
2. private static String a = "ab";
3. public static void main(String[] args){
4. String s1 = "a";
5. String s2 = "b";
6. String s = s1 + s2;
7. System.out.println(s == a);//false
8. System.out.println(s.intern() == a);//true
9. }
10. }
這裡用到Java裡面是一個常量池的問題。對於s1+s2操作,其實是在堆裡面重新建立了一個新的對象,s儲存的是這個新對象在堆空間的的內容,所 以s與a的值是不相等的。而當調用s.intern()方法,卻可以返回s在常量池中的地址值,因為a的值儲存在常量池中,故s.intern和a的值相等。
總結
棧中用來存放一些未經處理資料類型的局部變數資料和對象的引用(String,數組.對象等等)但不存放對象內容
堆中存放使用new關鍵字建立的對象.
字串是一個特殊封裝類,其引用是存放在棧裡的,而對象內容必鬚根據建立方式不同定(常量池和堆).有的是編譯期就已經建立好,存放在字串常 量池中,而有的是運行時才被建立.使用new關鍵字,存放在堆中。
詳細介紹Java的記憶體管理與記憶體泄露
引言:Java記憶體流失是每個Java程式員都會遇到的問題,程式在本地運行一切正常,可是布署到遠端就會出現記憶體無限制的增長,最後系統癱瘓,那麼如何最快最好的檢測程式的穩定性,防止系統崩盤,作者用自已的親身經曆與各位網友分享解決這些問題的辦法。
作為Internet最流行的程式設計語言之一,Java現正非常流行。我們的網路應用程式就主要採用Java語言開發,大體上分為用戶端、伺服器和資料庫三個層次。在進入測試過程中,我們發現有一個程式模組系統記憶體和CPU資源消耗急劇增加,持續增長到出現java.lang.OutOfMemoryError為止。經過分析Java記憶體流失是破壞系統的主要因素。這裡與大家分享我們在開發過程中遇到的Java記憶體流失的檢測和處理解決過程.
本文先介紹Java的記憶體管理,以及導致Java記憶體泄露的原因。
一. Java是如何管理記憶體
為了判斷Java中是否有記憶體泄露,我們首先必須瞭解Java是如何管理記憶體的。Java的記憶體管理就是對象的分配和釋放問題。在Java中,記憶體的分配是由程式完成的,而記憶體的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程式員不需要通過調用函數來釋放記憶體,但它只能回收無用並且不再被其它對象引用的那些對象所佔用的空間。
Java的記憶體記憶體回收機制是從程式的主要運行對象開始檢查引用鏈,當遍曆一遍後發現沒有被引用的孤立對象就作為記憶體回收。GC為了能夠正確釋放對象,必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都需要進行監控。監視對象狀態是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。
在Java中,這些無用的對象都由GC負責回收,因此程式員不需要考慮這部分的記憶體泄露。雖然,我們有幾個函數可以訪問GC,例如運行GC的函數System.gc(),但是根據Java語言規範定義,該函數不保證JVM的垃圾收集器一定會執行。因為不同的JVM實現者可能使用不同的演算法管理GC。通常GC的線程的優先順序別較低。JVM調用GC的策略也有很多種,有的是記憶體使用量到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。
二. 什麼是Java中的記憶體泄露
導致記憶體流失主要的原因是,先前申請了記憶體空間而忘記了釋放。如果程式中存在對無用對象的引用,那麼這些對象就會駐留記憶體,消耗記憶體,因為無法讓記憶體回收行程GC驗證這些對象是否不再需要。如果存在對象的引用,這個對象就被定義為"有效活動",同時不會被釋放。要確定對象所佔記憶體將被回收,我們就要務必確認該對象不再會被使用。典型的做法就是把對象資料成員設為null或者從集合中移除該對象。但當局部變數不需要時,不需明顯的設為null,因為一個方法執行完畢時,這些引用會自動被清理。
在Java中,記憶體流失就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是有被引用的,即在有向樹形圖中,存在樹枝通路可以與其相連;其次,這些對象是無
用的,即程式以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的記憶體流失,這些對象不會被GC所回收,然而它卻佔用記憶體。
這裡引用一個常看到的例子,在下面的代碼中,迴圈申請Object對象,並將所申請的對象放入一個Vector中,如果僅僅釋放對象本身,但因為Vector仍然引用該對象,所以這個對象對GC來說是不可回收的。因此,如果對象加入到Vector後,還必須從Vector中刪除,最簡單的方法就是將Vector對象設定為null。
1. Vector v = new Vector(10);
2. for (int i = 1; i < 100; i++)
3. {
4. Object o = new Object();
5. v.add(o);
6. o = null;
7. }//此時,所有的Object對象都沒有被釋放,因為變數v引用這些對象。
實際上這些對象已經是無用的,但還被引用,GC就無能為力了(事實上GC認為它還有用),這一點是導致記憶體流失最重要的原因。 再引用另一個例子來說明Java的記憶體流失。假設有一個日誌類Logger,其提供一個靜態log(String msg),任何其它類都可以調用Logger.Log(message)來將message的內容記錄到系統的記錄檔中。
Logger類有一個類型為HashMap的靜態變數temp,每次在執行log(message)的時候,都首先將message的值寫入temp中(以當前線程+目前時間為鍵),在退出之前再從temp中將以當前線程和目前時間為鍵的條目刪除。注意,這裡目前時間是不斷變化的,所以log在退出之前執行刪除條目的操作並不能刪除執行之初寫入的條目。這樣,任何一個作為參數傳給log的字串最終由於被Logger的靜態變數temp引用,而無法得到回收,這種對象保持就是我們所說的Java記憶體流失。 總的來說,記憶體管理中的記憶體流失產生的主要原因:保留下來卻永遠不再使用的對象引用。
JVM的記憶體回收機制詳解和調優
引言:gc即垃圾收集機制是指jvm用於釋放那些不再使用的對象所佔用的記憶體。java語言並不要求jvm有gc,也沒有規定gc如何工作。不過常用的jvm都有gc,而且大多數gc都使用類似的演算法管理記憶體和執行收集操作。
1.JVM的gc概述
gc即垃圾收集機制是指jvm用於釋放那些不再使用的對象所佔用的記憶體。java語言並不要求jvm有gc,也沒有規定gc如何工作。不過常用的jvm都有gc,而且大多數gc都使用類似的演算法管理記憶體和執行收集操作。
在充分理解了垃圾收集演算法和執行過程後,才能有效最佳化它的效能。有些垃圾收集專用於特殊的應用程式。比如,即時應用程式主要是為了避免垃圾收集中斷,而大多數OLTP應用程式則注重整體效率。理解了應用程式的工作負載和jvm支援的垃圾收集演算法,便可以進行最佳化配置垃圾收集器。
垃圾收集的目的在於清除不再使用的對象。gc通過確定對象是否被使用中的物件引用來確定是否收集該對象。gc首先要判斷該對象是否是時候可以收集。兩種常用的方法是引用計數和對象引用遍曆。
1.1.引用計數
引用計數儲存對特定對象的所有引用數,也就是說,當應用程式建立引用以及引用超出範圍時,jvm必須適當增減引用數。當某對象的引用數為0時,便可以進行垃圾收集。
1.2.對象引用遍曆
早期的jvm使用引用計數,現在大多數jvm採用對象引用遍曆。對象引用遍曆從一組對象開始,沿著整個對象圖上的每條連結,遞迴確定可到達(reachable)的對象。如果某對象不能從這些根對象的一個(至少一個)到達,則將它作為垃圾收集。在對象遍曆階段,gc必須記住哪些對象可以到達,以便刪除不可到達的對象,這稱為標記(marking)對象。
下一步,gc要刪除不可到達的對象。刪除時,有些gc只是簡單的掃描堆棧,刪除未標記的未標記的對象,並釋放它們的記憶體以產生新的對象,這叫做清除(sweeping)。這種方法的問題在於記憶體會分成好多小段,而它們不足以用於新的對象,但是組合起來卻很大。因此,許多gc可以重新組織記憶體中的對象,並進行壓縮(compact),形成可利用的空間。
為此,gc需要停止其他的活動活動。這種方法意味著所有與應用程式相關的工作停止,只有gc運行。結果,在響應期間增減了許多混雜請求。另外,更複雜的gc不斷增加或同時運行以減少或者清除應用程式的中斷。有的gc使用單線程完成這項工作,有的則採用多線程以增加效率。
2.幾種記憶體回收機制
2.1.標記-清除收集器
這種收集器首先遍曆對象圖並標記可到達的對象,然後掃描堆棧以尋找未標記對象並釋放它們的記憶體。這種收集器一般使用單線程工作並停止其他動作。
2.2.標記-壓縮收集器
有時也叫標記-清除-壓縮收集器,與標記-清除收集器有相同的標記階段。在第二階段,則把標記對象複製到堆棧的新域中以便壓縮堆棧。這種收集器也停止其他動作。
2.3.複製收集器
這種收集器將堆棧分為兩個域,常稱為半空間。每次僅使用一半的空間,jvm產生的新對象則放在另一半空間中。gc運行時,它把可到達對象複製到另一半空間,從而壓縮了堆棧。這種方法適用於短生存期的對象,持續複製長生存期的對象則導致效率降低。
2.4.增量收集器
增量收集器把堆棧分為多個域,每次僅從一個域收集垃圾。這會造成較小的應用程式中斷。
2.5.分代收集器
這種收集器把堆棧分為兩個或多個域,用以存放不同壽命的對象。jvm產生的新對象一般放在其中的某個域中。過一段時間,繼續存在的對象將獲得使用期並轉入更長壽命的域中。分代收集器對不同的域使用不同的演算法以最佳化效能。
2.6.並發收集器
並發收集器與應用程式同時運行。這些收集器在某點上(比如壓縮時)一般都不得不停止其他動作以完成特定的任務,但是因為其他應用程式可進行其他的後台操作,所以中斷其他處理的實際時間大大降低。
2.7.並行收集器
並行收集器使用某種傳統的演算法並使用多線程並行的執行它們的工作。在多cpu機器上使用多線程技術可以顯著的提高java應用程式的可擴充性。
3.Sun HotSpot 1.4.1 JVM堆大小的調整
Sun HotSpot 1.4.1使用分代收集器,它把堆分為三個主要的域:新域、舊域以及永久域。Jvm產生的所有新對象放在新域中。一旦對象經曆了一定數量的垃圾收集迴圈後,便獲得使用期並進入舊域。在永久域中jvm則儲存class和method對象。就配置而言,永久域是一個獨立域並且不認為是堆的一部分。
下面介紹如何控制這些域的大小。可使用-Xms和-Xmx 控制整個堆的原始大小或最大值。
下面的命令是把初始大小設定為128M:
java –Xms128m
–Xmx256m為控制新域的大小,可使用-XX:NewRatio設定新域在堆中所佔的比例。
下面的命令把整個堆設定成128m,新域比率設定成3,即新域與舊域比例為1:3,新域為堆的1/4或32M:
java –Xms128m –Xmx128m–XX:NewRatio =3可使用-XX:NewSize和-XX:MaxNewsize設定新域的初始值和最大值。
下面的命令把新域的初始值和最大值設定成64m:
java –Xms256m –Xmx256m –Xmn64m
永久域預設大小為4m.運行程式時,jvm會調整永久域的大小以滿足需要。每次調整時,jvm會對堆進行一次完全的垃圾收集。
使用-XX:MaxPerSize標誌來增加永久域搭大小。在WebLogic Server應用程式載入較多類時,經常需要增加永久域的最大值。當jvm載入類時,永久域中的對象急劇增加,從而使jvm不斷調整永久域大小。為了避免調整,可使用-XX:PerSize標誌設定初始值。
下面把永久域初始值設定成32m,最大值設定成64m.
java -Xms512m -Xmx512m -Xmn128m -XX:PermSize=32m -XX:MaxPermSize=64m
預設狀態下,HotSpot在新域中使用複製收集器。該域一般分為三個部分。第一部分為Eden,用於產生新的對象。另兩部分稱為救助空間,當Eden充滿時,收集器停止應用程式,把所有可到達對象複製到當前的from救助空間,一旦當前的from救助空間充滿,收集器則把可到達對象複製到當前的to救助空間。From和to救助空間互換角色。維持活動的對象將在救助空間不斷複製,直到它們獲得使用期並轉入舊域。使用-XX:SurvivorRatio可控制新域子空間的大小。
同NewRation一樣,SurvivorRation規定某救助域與Eden空間的比值。比如,以下命令把新網域設定成64m,Eden佔32m,每個救助域各佔16m:
java -Xms256m -Xmx256m -Xmn64m -XX:SurvivorRation =2
如前所述,預設狀態下HotSpot對新域使用複製收集器,對舊域使用標記-清除-壓縮收集器。在新域中使用複製收集器有很多意義,因為應用程式產生的大部分對象是短壽命的。理想狀態下,所有過渡對象在移出Eden空間時將被收集。如果能夠這樣的話,並且移出Eden空間的對象是長壽命的,那麼理論上可以立即把它們移進舊域,避免在救助空間反覆複製。但是,應用程式不能適合這種理想狀態,因為它們有一小部分中長壽命的對象。最好是保持這些中長壽命的對象並放在新域中,因為複製小部分的對象總比壓縮舊域廉價。為控制新域中對象的複製,可用-XX:TargetSurvivorRatio控制救助空間的比例(該值是設定救助空
間的使用比例。如救助空間位1M,該值50表示可用500K)。該值是一個百分比,預設值是50.當較大的堆棧使用較低的sruvivorratio時,應增加該值到80至90,以更好利用救助空間。用-XX:maxtenuring threshold可控制上限。
為放置所有的複製全部發生以及希望對象從eden擴充到舊域,可以把MaxTenuring Threshold設定成0.設定完成後,實際上就不再使用救助空間了,因此應把SurvivorRatio設成最大值以最大化Eden空間,設定如下:
java „ -XX:MaxTenuringThreshold=0 –XX:SurvivorRatio=50000 „
4.BEA JRockit JVM的使用
Bea WebLogic 8.1使用的新的JVM用於Intel平台。在Bea安裝完畢的目錄下可以看到有一個類似於jrockit81sp1_141_03的檔案夾。這就是Bea新JVM所在目錄。不同於HotSpot把Java位元組碼編譯成本地碼,它預先編譯成類。JRockit還提供了更細緻的功能用以觀察JVM的運行狀態,主要是獨立的GUI控制台(只能適用於使用Jrockit才能使用jrockit81sp1_141_03內建的console監控一些cpu及memory參數)或者WebLogic Server控制台。
Bea JRockit JVM支援4種垃圾收集器:
4.1.1.分代複製收集器
它與預設的分代收集器工作策略類似。對象在新域中分配,即JRockit文檔中的nursery.這種收集器最適合單cpu機上小型堆操作。
4.1.2.單空間並發收集器
該收集器使用完整堆,並與背景線程共同工作。儘管這種收集器可以消除中斷,但是收集器需花費較長的時間尋找死對象,而且處理應用程式時收集器經常運行。如果處理器不能應付應用程式產生的垃圾,它會中斷應用程式並關閉收集。
分代並發收集器 這種收集器在護理域使用排它複製收集器,在舊域中則使用並發收集器。由於它比單空間共同發生收集器中斷頻繁,因此它需要較少的記憶體,應用程式的運行效率也較高,注意,過小的護理域可以導致大量的臨時對象被擴充到舊域中。這會造成收集器超負荷運作,甚至採用排它性工作方式完成收集。
4.1.3.並行收集器
該收集器也停止其他進程的工作,但使用多線程以加速收集進程。儘管它比其他的收集器易於引起長時間的中斷,但一般能更好的利用記憶體,程式效率也較高。
預設狀態下,JRockit使用分代並發收集器。要改變收集器,可使用-Xgc:<gc_name>,對應四個收集器分別為gencopy,singlecon,gencon以及parallel.可使用-Xms和-Xmx設定堆的初始大小和最大值。要設定護理域,則使用-Xns:java –jrockit –Xms512m –Xmx512m
–Xgc:gencon –Xns128m„儘管JRockit支援-verbose:gc開關,但它輸出的資訊會因收集器的不同而異。JRockit還支援memory、load和codegen的輸出。
注意 :如果 使用JRockit JVM的話還可以使用WLS內建的console(C:\bea\jrockit81sp1_141_03\bin下)來監控一些資料,如cpu,memery等。要想能構監控必須在啟動服務時startWeblogic.cmd中加入-Xmanagement參數。
5.如何從JVM中擷取資訊來進行調整
-verbose.gc開關可顯示gc的操作內容。開啟它,可以顯示最忙和最空閑收集行為發生的時間、收集前後的記憶體大小、收集需要的時間等。開啟-xx:+ printgcdetails開關,可以詳細瞭解gc中的變化。開啟-XX: + PrintGCTimeStamps開關,可以瞭解這些垃圾收集發生的時間,自jvm啟動以後以秒計量。最後,通過-xx: + PrintHeapAtGC開關瞭解堆的更詳細的資訊。為了瞭解新域的情況,可以通過-XX:=PrintTenuringDistribution開關瞭解獲得使用期的對象權。
6.Pdm系統JVM調整
6.1.伺服器:前提記憶體1G 單CPU
可通過如下參數進行調整:-server 啟用伺服器模式(如果CPU多,伺服器機建議使用此項)
-Xms,-Xmx一般設為同樣大小。 800m
-Xmn 是將NewSize與MaxNewSize設為一致。320m
-XX:PerSize 64m
-XX:NewSize 320m 此值設大可調大新對象區,減少Full GC次數
-XX:MaxNewSize 320m
-XX:NewRato NewSize設了可不設。4
-XX: SurvivorRatio 4
-XX:userParNewGC 可用來設定並行收集
-XX:ParallelGCThreads 可用來增加並行度 4
-XXUseParallelGC 設定後可以使用並行清除收集器
-XX:UseAdaptiveSizePolicy 與上面一個聯合使用效果更好,利用它可以自動最佳化新域大小以及救助空間比值
6.2.客戶機:通過在JNLP檔案中設定參數來調整用戶端JVM
JNLP中參數:initial-heap-size和max-heap-size
這可以在framework的RequestManager中產生JNLP檔案時加入上述參數,但是這些值是要求根據客戶機的硬體狀態變化的(如客戶機的記憶體大小等)。建議這兩個參數值設為客戶機可用記憶體的60%(有待測試)。為了在動態產生JNLP時以上兩個參數值能夠隨客戶機不同而不同,可靠慮獲得客戶機系統資訊並將這些嵌到首頁index.jsp中作為串連請求的參數。
在設定了上述參數後可以通過Visualgc 來觀察記憶體回收的一些參數狀態,再做相應的調整來改善效能。一般的標準是減少fullgc的次數,最好硬體支援使用並行記憶體回收(要求多CPU)。
深入Java核心 Java記憶體配置原理精講