熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

對象引用是怎樣嚴重影響垃圾收集器

2022-06-13   來源: Java核心技術 

  如果您認為 Java 游戲開發人員是 Java 編程世界的一級方程式賽車手那麼您就會明白為什麼他們會如此地重視程序的性能 游戲開發人員幾乎每天都要面對的性能問題往往超過了一般程序員考慮問題的范圍哪裡可以找到這些特殊的開發人員呢?Java 游戲社區就是一個好去處(參見 參考資料) 雖然在這個站點可能沒有很多關於服務器端的應用但是我們依然可以從中受益看看這些惜比特如金的游戲開發人員每天所面對的我們往往能從中得到寶貴的經驗讓我們開始游戲吧!
  
  對象洩漏
  
  游戲程序員跟其他程序員一樣??他們也需要理解 Java 運行時環境的一些微妙之處比如垃圾收集垃圾收集可能是使您感到難於理解的較難的概念之一 因為它並不能總是毫無遺漏地解決 Java 運行時環境中堆管理的問題似乎有很多類似這樣的討論它的開頭或結尾寫著我的問題是關於垃圾收集
  
  假如您正面遭遇內存耗盡(outofmemory)的錯誤於是您使用檢測工具想要找到問題所在但這是徒勞的您很容易想到另外一個比較可信的原因這是 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();
  
  sbappend(hello )append(string);
  
  sbappend( nice to see you!);
  
  return sbtoString();
  
  }
  
  當該方法執行時運行時棧保留了一個對 StringBuffer 對象的引用這個對象是在程序的第一行產生的在這個方法的整個執行期間棧保存的這個對象引用將會防止該對象被當作垃圾當這個方法執行完畢變量 sb 也就失去了它的作用域相應地運行時棧就會刪除對該 StringBuffer 對象的引用於是不再有對該 StringBuffer 對象的引用現在它就可以被當作垃圾收集了棧刪除引用的操作就等於在該方法結束時將 null 值賦給變量 sb
  
  錯誤的作用域
  
  既然 Java 虛擬機可以執行等價於賦空的操作那麼顯式地賦空變量還有什麼用呢?對於在正確的作用域中的變量來說顯式地賦空變量的確沒用但是讓我們來看看另外一個版本的 scopingExample 方法這一次我們將把變量 sb 放在一個錯誤的作用域中
  
  清單 靜態作用域
  
  static StringBuffer sb = new StringBuffer();
  
  public static String scopingExample(String string) {
  
  sb = new StringBuffer();
  
  sbappend(hello )append(string);
  
  sbappend( nice to see you!);
  
  return sbtoString();
  
  }
  
  現在 sb 是一個靜態變量所以只要它所在的類還裝載在 Java 虛擬機中它也將一直存在該方法執行一次一個新的 StringBuffer 將被創建並且被 sb 變量引用在這種情況下sb 變量以前引用的 StringBuffer 對象將會死亡成為垃圾收集的對象也就是說這個死亡的 StringBuffer 對象被程序保留的時間比它實際需要保留的時間長得多??如果再也沒有對該 scopingExample 方法的調用它將會永遠保留下去
  
  一個有問題的例子
  
  即使如此顯式地賦空變量能夠提高性能嗎?我們會發現我們很難相信一個對象會或多或少對程序的性能產生很大影響直到我看到了一個在 Java Games 的 Sun 工程師給出的一個例子這個例子包含了一個不幸的大型對象
  
  清單 仍在靜態作用域中的對象
  
  private static Object bigObject;
  
  public static void test(int size) {
  
  long startTime = SystemcurrentTimeMillis();
  
  long numObjects = ;
  
  while (true) {
  
  //bigObject = null; //explicit nulling
  
  //SizableObject could simply be a large array eg byte[]
  
  //In the JavaGaming discussion it was a BufferedImage
  
  bigObject = new SizableObject(size);
  
  long endTime = SystemcurrentTimeMillis();
  
  ++numObjects;
  
  // We print stats for every two seconds
  
  if (endTime startTime >= ) {
  
  Systemoutprintln(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
  • 上一篇文章:

  • 下一篇文章:
  • Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.