關(guān)于線程后值得一提的是:除非您的確需要,不要頻繁休眠。 清單 8 說明了一個不當(dāng)行為的例子:
清單 8. 頻繁休眠
while(someCondition) {
...more code here...
Thread.sleep(aFewMilliseconds);
...more code here...
}
這么做的應(yīng)用程序都會在該代碼塊處創(chuàng)建大量不必要的垃圾 并生成大量上下文切換。您不應(yīng)當(dāng)使用頻繁休眠, 而應(yīng)使用 java.util.concurrent 提供的更高級別同步類,如 BlockingQueue、Semaphore、FutureTask 或者 CountDownLatch。 這些同步類提供了一種在等待條件變?yōu)檎鏁r不消耗 CPU 的途徑。 當(dāng)您調(diào)用第三方代碼而這些代碼沒有使用監(jiān)視器的有些時候不可能達到上面的要求, 在此情況下,您所能采取的佳做法 是嘗試在進行池操作時使創(chuàng)建的垃圾量小。
分析并提高啟動性能
對于 RCP 應(yīng)用程序而言,提高其啟動性能是一項挑戰(zhàn)。 一般而言,啟動性能是由磁盤 I/O、類載入和字節(jié)碼驗證綜合而成的功能。 當(dāng)然,如果在您的包內(nèi)加入過多工作,也可能使其啟動緩慢,但是通常情況下 這并不是啟動中耗時的一部分。 啟動往往會由于許多個小的時間消耗而變得及其緩慢。 通常不會有任何一件事情消耗大量時間,而是每件事都只消耗少量時間, 但當(dāng)其累積起來后終導(dǎo)致了大量時間的消耗。
RCP 應(yīng)用程序構(gòu)建于 OSGi 之上,它是面向 Java 的動態(tài)模塊系統(tǒng)(Dynamic Module System)。OSGi 提供了一種簡單的手段以全局方式鉤取類的裝載。 有人已經(jīng)利用這種類裝載鉤子來創(chuàng)建 Java 類緩存,從而避免頻繁訪問磁盤并提高 啟動速度。這種技術(shù)很有前途,不過尚需更多研究以確定其功效。
為了提高啟動速度,Eclipse 鼓勵的另一項技術(shù)是 包按需激活(lazy bundle activation):直到某個包需要時才被裝載并激活。 一般在分析啟動性能時,我會收集所有激活包的列表以及對應(yīng)于它們?yōu)楹渭せ畹亩褩8櫺畔ⅰ?接著通覽列表,判斷我是否認為該包確實應(yīng)該在啟動時被激活。 如果我認為有個包在啟動時不需要,我會刪除它以提高啟動性能(同時看發(fā)生了什么中斷)。 一旦我知道了刪除包后導(dǎo)致何種提高效果, 我聯(lián)系該代碼的開發(fā)人員,與之討論刪除或延遲對該包的激活。
要想收集包激活和類裝載信息,可使用如 清單 9 所示的調(diào)試選項,也可在 org.eclipse.osgi 包的 .options 文件中找到, 或者看看 CVS 的近版本(請參閱 參考資料):
清單 9. 啟用 OSGi 調(diào)試選項
org.eclipse.osgi/debug=true
org.eclipse.osgi/debug/bundleTime=true
org.eclipse.osgi/debug/monitorbundles=true
org.eclipse.osgi/monitor/activation=true
org.eclipse.osgi/monitor/classes=true
不過,插件開發(fā)人員可能自行其是阻撓按需裝載。 有個例子,我曾參與一個產(chǎn)品,它有一套堆棧視圖。對它定義了一個擴展, 以便于其他人能夠貢獻自己的堆棧視圖。在啟動的時候,可能只有一個或者沒有視圖可見, 但該擴展的作者提前創(chuàng)建了這些視圖, 即使它們根本不會展現(xiàn)出來。后來把改擴展改為只顯示視圖的標題和圖標, 直到終端用戶真的嘗試看該視圖時,才激活加入到擴展中的那個包。
另外一個例子,假設(shè)您正在創(chuàng)建一個應(yīng)用程序,它有一個登錄對話框。 您的目的是僅激活用于顯示登錄對話框的包。 我曾經(jīng)看到有些應(yīng)用程序,為了顯示登錄對話框激活了所有包的 70%。
作為一種手段,我建議您開發(fā)一個 shell 游戲,它的啟動時間可以有所浮動但是總和保持相同。 用戶不必為他尚未使用到的特性付出等待時間。 這樣做的目的是只為需要付出而不是為所有東西付出時間。 如果某個應(yīng)用程序在您做了所有提高性能的努力后仍然不夠快, 那么不要忘記提高用戶在感覺上的性能。
結(jié)束語
我特別強調(diào)在您的應(yīng)用程序架構(gòu)和設(shè)計階段考慮性能。 起碼,架構(gòu)師或者首席開發(fā)人員必須知道基本的順序分析或時間復(fù)雜性(比如 Big O), 以便理解應(yīng)用程序的存儲需求或執(zhí)行時間隨應(yīng)用程序增長如何改變。 后才考慮解決性能問題是被動的 —— 也很低效 —— 因為在游戲后期幾乎已不可能再去對架構(gòu)做大的調(diào)整。
不過即使是擁有好的架構(gòu)的應(yīng)用程序也會有性能瓶頸, 您需要使用工具和技術(shù)了解并處理瓶頸。 現(xiàn)在您了解了如何度量 RCP 應(yīng)用程序性能,判定是 CPU 還是 I/O 瓶頸導(dǎo)致了速度降低, 使用一些記錄技術(shù),保持 UI 線程可響應(yīng),用 Job 回避線程誤用, 以及提高啟動性能。
理解一個富客戶機(Rich Client Platform(RCP))平臺應(yīng)用程序的完整內(nèi)存使用 會是一項腦力勞動。操作系統(tǒng)(OS)會指出應(yīng)用程序耗費了多少內(nèi)存,Java™ 平臺會指出 您已經(jīng)耗費了多少堆。操作系統(tǒng)匯報的內(nèi)存使用情況總是高于可用堆大小。 不幸的是,有時操作系統(tǒng)所報告的數(shù)目會遠遠 大于堆大小。 對于堆分析的一個挑戰(zhàn)是判斷這片 “黑暗空間” 中藏匿著什么。
一般而言:進程使用的內(nèi)存 = Java 堆 + 已編譯的本地代碼 + 字節(jié)碼 + 其他 / 本地
很不幸,JVMS 根據(jù)其發(fā)行版本和供應(yīng)商的不同,指示出的堆大小也不同。 我所運行的一個 Java 應(yīng)用程序可以給出一些例子:Sun 1.6 JDK 報告堆大小為 32.7MB , 而操作系統(tǒng)報告為 48.6MB 私有字節(jié),有 16MB 未作說明?偟膩碚f這還算不錯。 已編譯代碼和字節(jié)碼是這 16MB 的一部分。 用 IBM® 1.5 JDK 運行同一應(yīng)用程序,堆加上類加載器和已編譯代碼總共 是 39MB,而 OS 報告的大小為 45.8MB。
一般而言,您可以把問題簡化為只關(guān)注 Java 堆。 這對絕大多數(shù) Java 應(yīng)用程序而言已經(jīng)足夠了,而且也可以讓應(yīng)用程序做到大程度的改進。 如果還不夠,那么您應(yīng)該使用操作系統(tǒng)工具檢查未被 Java 堆覆蓋的本地內(nèi)存。
差異分析(Differential analysis)
處理內(nèi)存使用問題中為行之有效的一種手段是關(guān)注對象數(shù)目。 舉例而言,如果要在某個郵件應(yīng)用程序中顯示 50 條郵件消息, 那么需要多少個 MailMessage 類的實例? 50,對嗎?那么郵件詳情或其他郵件域?qū)ο竽?如果切換了文件夾,顯示新的 50 條郵件消息,又將發(fā)生什么情況呢? 您會擁有多少個對象:50 還是 100?
一旦開始進行此類分析,您會對 實例數(shù)目大大超過期望數(shù)目這一常見情形感到驚訝。注意:在您收集堆轉(zhuǎn)儲之前,確保 已經(jīng)發(fā)生了垃圾收集行為,因為您不會想去考慮那些已經(jīng)死亡的對象。 一般情況下,我會在捕獲堆轉(zhuǎn)儲前做一個 System.gc() 操作。
我并不想去描述司空見慣的一般性堆分析(請參閱 參考資料)。 相反,我將介紹差異分析(differential analysis),這是用于發(fā)現(xiàn)應(yīng)用程序中內(nèi)存泄漏的技術(shù)。
它的基本思想很簡單:
得到一個堆轉(zhuǎn)儲。
在應(yīng)用程序中多次做某件事(假設(shè)做 10 次)。
得到另一個堆轉(zhuǎn)儲。
比較兩個堆轉(zhuǎn)儲中應(yīng)用程序?qū)ο蟮臄?shù)目。
這樣可以構(gòu)建所需應(yīng)用程序?qū)ο蠹稀?隨著泄漏的發(fā)現(xiàn)和處理,將泄漏到腳本的類添加到一個列表。 這樣一來,不長時間可以構(gòu)建經(jīng)常檢查的應(yīng)用程序?qū)ο蠹稀?/p>
單元測試
我所用的另一個技術(shù)是寫單元測試,解析堆轉(zhuǎn)儲并對期望的域?qū)ο髮嵗龜?shù)目做斷言。 比如說,您可以啟動應(yīng)用程序,運行一個場景,得到一個對轉(zhuǎn)儲,接著做斷言。 下面是一個例子:在郵件應(yīng)用程序中發(fā)現(xiàn)一個內(nèi)存泄漏,當(dāng)該泄漏被處理后,我希望確定 在以后的代碼改變中不會再發(fā)生該問題,于是為此構(gòu)建了一個單元測試。這是一個資源使用 單元測試,如清單 1 所示:
清單 1. JUnit 測試用例,解析堆轉(zhuǎn)儲
public void testOpenTenMessages() throws Exception {
Heap heap = Heap.from("openMessages.phd");
assertEquals(10, heap.instancesOf("cbg/mail/ui/message/MessageController"));
assertEquals(10, heap.instancesOf("cbg/mail/ui/message/viewer/AttachmentModel"));
Heap heapAfter = Heap.from("openMessagesClosed.phd");
assertEquals(0, heapAfter.instancesOf("cbg/mail/ui/message/MessageController"));
assertEquals(0, heapAfter.instancesOf("cbg/mail/ui/message/viewer/AttachmentModel"));
}
其工作原理是:打開 10 條郵件消息,創(chuàng)建名為 openMessages.phd 的堆轉(zhuǎn)儲。 然后關(guān)閉消息并創(chuàng)建第二個堆轉(zhuǎn)儲,命名為 openMessagesClosed.phd。
針對這兩個堆轉(zhuǎn)儲文件,現(xiàn)在對內(nèi)存中所需域?qū)ο髷?shù)目做斷言。 我期望在第一個轉(zhuǎn)儲中有 10 條郵件消息(MessageControllers), 在第二個中沒有任何郵件消息。
這種自動堆分析是對不同構(gòu)建之間的變化做跟蹤的有力途徑。 和標準單元測試一樣,您可以僅在發(fā)現(xiàn)和處理內(nèi)存泄漏時才創(chuàng)建此類單元測試。 把應(yīng)用程序中的資源使用看作應(yīng)被跟蹤的另一個量度信息是有益的。 即便是知道應(yīng)用程序在運行后分配了多少個對象,也有助于構(gòu)建的發(fā)展。
不幸的是,不同的 JVM(即便是相同 JVM 的不同版本)在堆分析上有著極大的不同。 IBM JVM 改變過幾次堆分析格式。Sun 的 JVM 使用另一種格式,并且在每次發(fā)布時也做過改動。
回頁首
圖形設(shè)備接口資源的泄漏
在 Windows® 操作系統(tǒng)中,每個顏色、字體、圖形上下文(graphics context(GC))、圖像、光標或者區(qū)域都對應(yīng)于一個單獨的圖形設(shè)備接口(graphical device interface(GDI))資源。 GDI 是 Windows 的術(shù)語,不過每個 OS 都有一個對應(yīng)物。重要的是整個 OS 所擁有的 GDI 資源數(shù)目是有限的。 如果應(yīng)用程序泄漏或使用了過多的資源,將會影響到系統(tǒng)上所運行的所有應(yīng)用程序。GDI 泄漏很糟糕。
判斷 GDI 資源是否泄漏比較簡單。在 Windows OS 中,您可以使用 Task Manager 或 Process Explorer。 添加 GDI 列,觀察它是否隨時間而增長(參看圖 1)。比如說,您可能注意到每當(dāng)打開一條郵件消息, 與 javaw 進程關(guān)聯(lián)的 GDI 資源數(shù)目會增加 50,但是當(dāng)您關(guān)閉郵件消息后, GDI 資源的數(shù)目只減少 46。您每閱讀一條郵件消息,會泄漏 4 個 GDI 資源。
盡管 Task Manager 能告訴您何時 發(fā)生了泄漏, 但它不能幫您發(fā)現(xiàn)哪里 發(fā)生著泄漏。要做到這點,好的辦法是使用 Sleak,一個 SWT 開發(fā)工具(請參閱 參考資料)。 您可以啟用 SWT 所擁有的調(diào)試標記,使它跟蹤 GDI 資源的創(chuàng)建位置。 Sleak 讓您看到 GDI 資源以及它們是從哪里分配的。