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