如果您認為 Java 游戲開發人員是 Java 編程世界的一級方程式賽車手
那麼您就會明白為什麼他們會如此地重視程序的性能
游戲開發人員幾乎每天都要面對的性能問題
往往超過了一般程序員考慮問題的范圍
哪裡可以找到這些特殊的開發人員呢?Java 游戲社區就是一個好去處(參見 參考資料)
雖然在這個站點可能沒有很多關於服務器端的應用
但是我們依然可以從中受益
看看這些
惜比特如金
的游戲開發人員每天所面對的
我們往往能從中得到寶貴的經驗
讓我們開始游戲吧!
對象洩漏 游戲程序員跟其他程序員一樣??他們也需要理解 Java 運行時環境的一些微妙之處
比如垃圾收集
垃圾收集可能是使您感到難於理解的較難的概念之一
因為它並不能總是毫無遺漏地解決 Java 運行時環境中堆管理的問題
似乎有很多類似這樣的討論
它的開頭或結尾寫著
我的問題是關於垃圾收集
假如您正面遭遇內存耗盡(out
of
memory)的錯誤
於是您使用檢測工具想要找到問題所在
但這是徒勞的
您很容易想到另外一個比較可信的原因
這是 Java 虛擬機堆管理的問題
而不會認為這是您自己的程序的緣故
但是
正如 Java 游戲社區的資深專家不止一次地解釋的
Java 虛擬機並不存在任何被證實的對象洩漏問題
實踐證明
垃圾收集器一般能夠精確地判斷哪些對象可被收集
並且重新收回它們的內存空間給 Java 虛擬機
所以
如果您遇到了內存耗盡的錯誤
那麼這完全可能是由您的程序造成的
也就是說您的程序中存在著
無意識的對象保留(unintentional object retention)
內存洩漏與無意識的對象保留 內存洩漏和無意識的對象保留的區別是什麼呢?對於用 Java 語言編寫的程序來說
確實沒有區別
兩者都是指在您的程序中存在一些對象引用
但實際上您並不需要引用這些對象
一個典型的例子是向一個集合中加入一些對象以便以後使用它們
但是您卻忘了在使用完以後從集合中刪除這些對象
因為集合可以無限制地擴大
並且從來不會變小
所以當您在集合中加入了太多的對象(或者是有很多的對象被集合中的元素所引用)時
您就會因為堆的空間被填滿而導致內存耗盡的錯誤
垃圾收集器不能收集這些您認為已經用完的對象
因為對於垃圾收集器來說
應用程序仍然可以通過這個集合在任何時候訪問這些對象
所以這些對象是不可能被當作垃圾的
對於沒有垃圾收集的語言來說
例如 C++
內存洩漏和無意識的對象保留是有區別的
C++ 程序跟 Java 程序一樣
可能產生無意識的對象保留
但是 C++ 程序中存在真正的內存洩漏
即應用程序無法訪問一些對象以至於被這些對象使用的內存無法釋放且返還給系統
令人欣慰的是
在 Java 程序中
這種內存洩漏是不可能出現的
所以
我們更喜歡用
無意識的對象保留
來表示這個令 Java 程序員抓破頭皮的內存問題
這樣
我們就能區別於其他使用沒有垃圾收集語言的程序員
跟蹤被保留的對象 那麼當發現了無意識的對象保留該怎麼辦呢?首先
需要確定哪些對象是被無意保留的
並且需要找到究竟是哪些對象在引用它們
然後必須安排好 應該在哪裡釋放它們
最容易的方法是使用能夠對堆產生快照的檢測工具來標識這些對象
比較堆的快照中對象的數目
跟蹤這些對象
找到引用這些對象的對象
然後強制進行垃圾收集
有了這樣一個檢測器
接下來的工作相對而言就比較簡單了:
等待直到系統達到一個穩定的狀態
這個狀態下大多數新產生的對象都是暫時的
符合被收集的條件
這種狀態一般在程序所有的初始化工作都完成了之後
強制進行一次垃圾收集
並且對此時的堆做一份對象快照
進行任何可以產生無意地保留的對象的操作
再強制進行一次垃圾收集
然後對系統堆中的對象做第二次對象快照
比較兩次快照
看看哪些對象的被引用數量比第一次快照時增加了
因為您在快照之前強制進行了垃圾收集
那麼剩下的對象都應該是被應用程序所引用的對象
並且通過比較兩次快照我們可以准確地找出那些被程序保留的
新產生的對象
根據您對應用程序本身的理解
並且根據對兩次快照的比較
判斷出哪些對象是被無意保留的
跟蹤這些對象的引用鏈
找出究竟是哪些對象在引用這些無意地保留的對象
直到您找到了那個根對象
它就是產生問題的根源
顯式地賦空(nulling)變量 一談到垃圾收集這個主題
總會涉及到這樣一個吸引人的討論
即顯式地賦空變量是否有助於程序的性能
賦空變量是指簡單地將 null 值顯式地賦值給這個變量
相對於讓該變量的引用失去其作用域
清單
局部作用域
public static String scopingExample(String string) {
StringBuffer sb = new StringBuffer();
sb
append(
hello
)
append(string);
sb
append(
nice to see you!
);
return sb
toString();
}
當該方法執行時
運行時棧保留了一個對 StringBuffer 對象的引用
這個對象是在程序的第一行產生的
在這個方法的整個執行期間
棧保存的這個對象引用將會防止該對象被當作垃圾
當這個方法執行完畢
變量 sb 也就失去了它的作用域
相應地運行時棧就會刪除對該 StringBuffer 對象的引用
於是不再有對該 StringBuffer 對象的引用
現在它就可以被當作垃圾收集了
棧刪除引用的操作就等於在該方法結束時將 null 值賦給變量 sb
錯誤的作用域
既然 Java 虛擬機可以執行等價於賦空的操作
那麼顯式地賦空變量還有什麼用呢?對於在正確的作用域中的變量來說
顯式地賦空變量的確沒用
但是讓我們來看看另外一個版本的 scopingExample 方法
這一次我們將把變量 sb 放在一個錯誤的作用域中
清單
靜態作用域
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();
}
現在 sb 是一個靜態變量
所以只要它所在的類還裝載在 Java 虛擬機中
它也將一直存在
該方法執行一次
一個新的 StringBuffer 將被創建並且被 sb 變量引用
在這種情況下
sb 變量以前引用的 StringBuffer 對象將會死亡
成為垃圾收集的對象
也就是說
這個死亡的 StringBuffer 對象被程序保留的時間比它實際需要保留的時間長得多??如果再也沒有對該 scopingExample 方法的調用
它將會永遠保留下去
一個有問題的例子
即使如此
顯式地賦空變量能夠提高性能嗎?我們會發現我們很難相信一個對象會或多或少對程序的性能產生很大影響
直到我看到了一個在 Java Games 的 Sun 工程師給出的一個例子
這個例子包含了一個不幸的大型對象
清單
仍在靜態作用域中的對象
private static Object bigObject;
public static void test(int size) {
long startTime = System
currentTimeMillis();
long numObjects =
;
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 >=
) {
System
out
println(
Objects created per
seconds =
+ numObjects);
startTime = endTime;
numObjects =
;
}
}
}
這個例子有個簡單的循環
創建一個大型對象並且將它賦給同一個變量
每隔兩秒鐘報告一次所創建的對象個數
現在的 Java 虛擬機采用 generational 垃圾收集機制
新的對象創建之後放在一個內存空間(取名 Eden)內
然後將那些在第一次垃圾收集以後仍然保留的對象轉移到另外一個內存空間
在 Eden
即創建新對象時所在的新一代空間中
收集對象要比在
老一代
空間中快得多
但是如果 Eden 空間已經滿了
沒有空間可供分配
那麼就必須把 Eden 中的對象轉移到老一代空間中
騰出空間來給新創建的對象
如果沒有顯式地賦空變量
而且所創建的對象足夠大
那麼 Eden 就會填滿
並且垃圾收集器就不能收集當前所引用的這個大型對象
所產生的後果是
這個大型對象被轉移到
老一代空間
並且要花更多的時間來收集它
通過顯式地賦空變量
Eden 就能在新對象創建之前獲得自由空間
這樣垃圾收集就會更快
實際上
在顯式賦空的情況下
該循環在兩秒鐘內創建的對象個數是沒有顯式賦空時的
倍??但是僅當您選擇創建的對象要足夠大而可以填滿 Eden 時才是如此
在 Windows 環境
Java虛擬機
的默認配置下大概需要
KB
那就是一行賦空操作產生的
倍的性能差距
但是請注意這個性能差別產生的原因是變量的作用域不正確
這正是賦空操作發揮作用的地方
並且是因為所創建的對象非常大
最佳實踐 這是一個有趣的例子
但是值得強調的是
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26746.html