posts - 188,comments - 176,trackbacks - 0
                如果您認為 Java 游戲開發(fā)人員是 Java 編程世界的一級方程式賽車手,那么您就會明白為什么他們會如此地重視程序的性能。 游戲開發(fā)人員幾乎每天都要面對的性能問題,往往超過了一般程序員考慮問題的范圍。哪里可以找到這些特殊的開發(fā)人員呢?Java 游戲社區(qū)就是一個好去處。 雖然在這個站點可能沒有很多關(guān)于服務(wù)器端的應(yīng)用,但是我們依然可以從中受益,看看這些“惜比特如金”的游戲開發(fā)人員每天所面對的,我們往往能從中得到寶貴的經(jīng)驗。讓我們開始游戲吧! 

            對象泄漏
            游戲程序員跟其他程序員一樣――他們也需要理解 Java 運行時環(huán)境的一些微妙之處,比如垃圾收集。垃圾收集可能是使您感到難于理解的較難的概念之一, 因為它并不能總是毫無遺漏地解決 Java 運行時環(huán)境中堆管理的問題。似乎有很多類似這樣的討論,它的開頭或結(jié)尾寫著:“我的問題是關(guān)于垃圾收集”。

            假如您正面遭遇內(nèi)存耗盡(out-of-memory)的錯誤。于是您使用檢測工具想要找到問題所在,但這是徒勞的。您很容易想到另外一個比較可信的原因:這是 Java 虛擬機堆管理的問題,而不會認為這是您自己的程序的緣故。但是,正如 Java 游戲社區(qū)的資深專家不止一次地解釋的,Java 虛擬機并不存在任何被證實的對象泄漏問題。實踐證明,垃圾收集器一般能夠精確地判斷哪些對象可被收集,并且重新收回它們的內(nèi)存空間給 Java 虛擬機。所以,如果您遇到了內(nèi)存耗盡的錯誤,那么這完全可能是由您的程序造成的,也就是說您的程序中存在著“無意識的對象保留(unintentional object retention)”。

            內(nèi)存泄漏與無意識的對象保留
            內(nèi)存泄漏和無意識的對象保留的區(qū)別是什么呢?對于用 Java 語言編寫的程序來說,確實沒有區(qū)別。兩者都是指在您的程序中存在一些對象引用,但實際上您并不需要引用這些對象。一個典型的例子是向一個集合中加入一些對象以便以后使用它們,但是您卻忘了在使用完以后從集合中刪除這些對象。因為集合可以無限制地擴大,并且從來不會變小,所以當您在集合中加入了太多的對象(或者是有很多的對象被集合中的元素所引用)時,您就會因為堆的空間被填滿而導(dǎo)致內(nèi)存耗盡的錯誤。垃圾收集器不能收集這些您認為已經(jīng)用完的對象,因為對于垃圾收集器來說,應(yīng)用程序仍然可以通過這個集合在任何時候訪問這些對象,所以這些對象是不可能被當作垃圾的。

            對于沒有垃圾收集的語言來說,例如 C++ ,內(nèi)存泄漏和無意識的對象保留是有區(qū)別的。C++ 程序跟 Java 程序一樣,可能產(chǎn)生無意識的對象保留。但是 C++ 程序中存在真正的內(nèi)存泄漏,即應(yīng)用程序無法訪問一些對象以至于被這些對象使用的內(nèi)存無法釋放且返還給系統(tǒng)。令人欣慰的是,在 Java 程序中,這種內(nèi)存泄漏是不可能出現(xiàn)的。所以,我們更喜歡用“無意識的對象保留”來表示這個令 Java 程序員抓破頭皮的內(nèi)存問題。這樣,我們就能區(qū)別于其他使用沒有垃圾收集語言的程序員。

            跟蹤被保留的對象
            那么當發(fā)現(xiàn)了無意識的對象保留該怎么辦呢?首先,需要確定哪些對象是被無意保留的,并且需要找到究竟是哪些對象在引用它們。然后必須安排好 應(yīng)該在哪里釋放它們。最容易的方法是使用能夠?qū)Χ旬a(chǎn)生快照的檢測工具來標識這些對象,比較堆的快照中對象的數(shù)目,跟蹤這些對象,找到引用這些對象的對象,然后強制進行垃圾收集。有了這樣一個檢測器,接下來的工作相對而言就比較簡單了: 

            等待直到系統(tǒng)達到一個穩(wěn)定的狀態(tài),這個狀態(tài)下大多數(shù)新產(chǎn)生的對象都是暫時的,符合被收集的條件;這種狀態(tài)一般在程序所有的初始化工作都完成了之后。 
            強制進行一次垃圾收集,并且對此時的堆做一份對象快照。 
            進行任何可以產(chǎn)生無意地保留的對象的操作。 
            再強制進行一次垃圾收集,然后對系統(tǒng)堆中的對象做第二次對象快照。 
            比較兩次快照,看看哪些對象的被引用數(shù)量比第一次快照時增加了。因為您在快照之前強制進行了垃圾收集,那么剩下的對象都應(yīng)該是被應(yīng)用程序所引用的對象,并且通過比較兩次快照我們可以準確地找出那些被程序保留的、新產(chǎn)生的對象。 
            根據(jù)您對應(yīng)用程序本身的理解,并且根據(jù)對兩次快照的比較,判斷出哪些對象是被無意保留的。 
            跟蹤這些對象的引用鏈,找出究竟是哪些對象在引用這些無意地保留的對象,直到您找到了那個根對象,它就是產(chǎn)生問題的根源。 

            顯式地賦空(nulling)變量
            一談到垃圾收集這個主題,總會涉及到這樣一個吸引人的討論,即顯式地賦空變量是否有助于程序的性能。賦空變量是指簡單地將 null 值顯式地賦值給這個變量,相對于讓該變量的引用失去其作用域。

          清單 1. 局部作用域

          public static String scopingExample(String string) { 
            StringBuffer sb = new StringBuffer(); 
            sb.append("hello ").append(string); 
            sb.append(", nice to see you!"); 
            return sb.toString(); 


            當該方法執(zhí)行時,運行時棧保留了一個對 StringBuffer 對象的引用,這個對象是在程序的第一行產(chǎn)生的。在這個方法的整個執(zhí)行期間,棧保存的這個對象引用將會防止該對象被當作垃圾。當這個方法執(zhí)行完畢,變量 sb 也就失去了它的作用域,相應(yīng)地運行時棧就會刪除對該 StringBuffer 對象的引用。于是不再有對該 StringBuffer 對象的引用,現(xiàn)在它就可以被當作垃圾收集了。棧刪除引用的操作就等于在該方法結(jié)束時將 null 值賦給變量 sb。

            錯誤的作用域
            既然 Java 虛擬機可以執(zhí)行等價于賦空的操作,那么顯式地賦空變量還有什么用呢?對于在正確的作用域中的變量來說,顯式地賦空變量的確沒用。但是讓我們來看看另外一個版本的 scopingExample 方法,這一次我們將把變量 sb 放在一個錯誤的作用域中。

          清單 2. 靜態(tài)作用域

          static StringBuffer sb = new StringBuffer(); 
          public static String scopingExample(String string) { 
            sb = new StringBuffer(); 
            sb.append("hello ").append(string); 
            sb.append(", nice to see you!"); 
            return sb.toString(); 


            現(xiàn)在 sb 是一個靜態(tài)變量,所以只要它所在的類還裝載在 Java 虛擬機中,它也將一直存在。該方法執(zhí)行一次,一個新的 StringBuffer 將被創(chuàng)建并且被 sb 變量引用。在這種情況下,sb 變量以前引用的 StringBuffer 對象將會死亡,成為垃圾收集的對象。也就是說,這個死亡的 StringBuffer 對象被程序保留的時間比它實際需要保留的時間長得多――如果再也沒有對該 scopingExample 方法的調(diào)用,它將會永遠保留下去。

            一個有問題的例子
            即使如此,顯式地賦空變量能夠提高性能嗎?我們會發(fā)現(xiàn)我們很難相信一個對象會或多或少對程序的性能產(chǎn)生很大影響,直到我看到了一個在 Java Games 的 Sun 工程師給出的一個例子,這個例子包含了一個不幸的大型對象。

          清單 3. 仍在靜態(tài)作用域中的對象

          private static Object bigObject;

          public static void test(int size) { 
            long startTime = System.currentTimeMillis(); 
            long numObjects = 0; 
            while (true) { 
              //bigObject = null; //explicit nulling 
              //SizableObject could simply be a large array, e.g. byte[] 
              //In the JavaGaming discussion it was a BufferedImage 
              bigObject = new SizableObject(size); 
              long endTime = System.currentTimeMillis(); 
              ++numObjects; 
              // We print stats for every two seconds 
              if (endTime - startTime >= 2000) { 
                System.out.println("Objects created per 2 seconds = " + numObjects); 
                startTime = endTime; 
                numObjects = 0; 
              } 
            } 


            這個例子有個簡單的循環(huán),創(chuàng)建一個大型對象并且將它賦給同一個變量,每隔兩秒鐘報告一次所創(chuàng)建的對象個數(shù)?,F(xiàn)在的 Java 虛擬機采用 generational 垃圾收集機制,新的對象創(chuàng)建之后放在一個內(nèi)存空間(取名 Eden)內(nèi),然后將那些在第一次垃圾收集以后仍然保留的對象轉(zhuǎn)移到另外一個內(nèi)存空間。在 Eden,即創(chuàng)建新對象時所在的新一代空間中,收集對象要比在“老一代”空間中快得多。但是如果 Eden 空間已經(jīng)滿了,沒有空間可供分配,那么就必須把 Eden 中的對象轉(zhuǎn)移到老一代空間中,騰出空間來給新創(chuàng)建的對象。如果沒有顯式地賦空變量,而且所創(chuàng)建的對象足夠大,那么 Eden 就會填滿,并且垃圾收集器就不能收集當前所引用的這個大型對象。所產(chǎn)生的后果是,這個大型對象被轉(zhuǎn)移到“老一代空間”,并且要花更多的時間來收集它。 

            通過顯式地賦空變量,Eden 就能在新對象創(chuàng)建之前獲得自由空間,這樣垃圾收集就會更快。實際上,在顯式賦空的情況下,該循環(huán)在兩秒鐘內(nèi)創(chuàng)建的對象個數(shù)是沒有顯式賦空時的5倍――但是僅當您選擇創(chuàng)建的對象要足夠大而可以填滿 Eden 時才是如此, 在 Windows 環(huán)境、Java虛擬機 1.4 的默認配置下大概需要 500KB。那就是一行賦空操作產(chǎn)生的 5 倍的性能差距。但是請注意這個性能差別產(chǎn)生的原因是變量的作用域不正確,這正是賦空操作發(fā)揮作用的地方,并且是因為所創(chuàng)建的對象非常大。

            最佳實踐
            這是一個有趣的例子,但是值得強調(diào)的是,最佳實踐是正確地設(shè)置變量的作用域,而不要顯式地賦空它們。雖然顯式賦空變量一般應(yīng)該沒有影響,但總有一些反面的例子證明這樣做會對性能產(chǎn)生巨大的負面影響。例如,迭代地或者遞歸地賦空集合內(nèi)的元素使得這些集合中的對象能夠滿足垃圾收集的條件,實際上是增加了系統(tǒng)的開銷而不是幫助垃圾收集。請記住這是個有意弄錯作用域的例子,其實質(zhì)是一個無意識的對象保留的例子。
              轉(zhuǎn)http://www.javaresearch.org
          posted on 2007-05-24 11:00 cheng 閱讀(254) 評論(0)  編輯  收藏 所屬分類: JBS
          主站蜘蛛池模板: 包头市| 澳门| 廉江市| 小金县| 迁安市| 平阳县| 简阳市| 万宁市| 寻乌县| 建瓯市| 康平县| 武强县| 潞西市| 紫金县| 美姑县| 枣阳市| 浮梁县| 达日县| 天峨县| 和林格尔县| 泰州市| 合作市| 永修县| 湘潭县| 台南县| 南靖县| 蓝田县| 南和县| 永康市| 霞浦县| 岳阳县| 西林县| 温泉县| 泰顺县| 贡山| 名山县| 长沙市| 遂平县| 莱阳市| 德兴市| 客服|