這篇文章主要介紹為JAVA性能而設(shè)計(jì)的示例分析,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!
10年的南皮網(wǎng)站建設(shè)經(jīng)驗(yàn),針對(duì)設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對(duì)一服務(wù),響應(yīng)快,48小時(shí)及時(shí)工作處理。全網(wǎng)營(yíng)銷推廣的優(yōu)勢(shì)是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動(dòng)調(diào)整南皮建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。成都創(chuàng)新互聯(lián)從事“南皮網(wǎng)站設(shè)計(jì)”,“南皮網(wǎng)站推廣”以來,每個(gè)客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
第一部分: 接口事宜
概要
許多通常的 Java 性能問題都起源于在設(shè)計(jì)過程早期中的類設(shè)計(jì)的思想, 早在許多開發(fā)者開始考慮性能問題之前. 在這個(gè)系列中, Brian Goetz討論了通常的 Java性能上的冒險(xiǎn)以及怎么在設(shè)計(jì)時(shí)候避免它們.
許多程序員在開發(fā)周期的后期才可是考慮性能管理. 他們常常把性能優(yōu)化拖延到最后, 希望能完全避免 -- 有時(shí)候這種策略是成功的. 但是早期的設(shè)計(jì)思想可以影響性能優(yōu)化的需求及其成功. 如果性能是你的程序的一個(gè)重要指標(biāo), 那么性能管理應(yīng)該從第一天起就和設(shè)開發(fā)周期整合在一起.
這個(gè)系列探索一些早期的設(shè)計(jì)思想能夠極大影響應(yīng)用程序性能的方法. 在這篇文章中, 我專注于最通常的性能問題中的一個(gè): 臨時(shí)變量的創(chuàng)建. 一個(gè)類的對(duì)象創(chuàng)建方式常常在設(shè)計(jì)時(shí)候就確定了的 -- 但不是故意的 --, 就為后來的性能問題種下了種子.
性能問題有各種形式. 最容易調(diào)整的是那些你簡(jiǎn)單地為計(jì)算選擇了一個(gè)錯(cuò)誤的算法 -- 就象使用使用冒泡算法來對(duì)一個(gè)大數(shù)據(jù)集進(jìn)行排序, 或者在使用一個(gè)經(jīng)常使用的數(shù)據(jù)項(xiàng)時(shí)不是做緩沖, 而是每次都計(jì)算. 你可以使用概要分析來簡(jiǎn)單地找出這些瓶頸, 一旦找到了,你可以很容易地改正. 但是, 許多 Java 性能問題來自一個(gè)更深的, 更難改正的源頭 -- 一個(gè)程序組件的接口設(shè)計(jì).
今天大多數(shù)程序是由內(nèi)部開發(fā)的或者外部買來的組件構(gòu)建而成. 甚至在程序不是很大地依于已經(jīng)存在的組件時(shí), 面向?qū)ο蟮脑O(shè)計(jì)過程也鼓勵(lì)應(yīng)用程序包裝成組件, 這樣就簡(jiǎn)化了設(shè)計(jì), 開發(fā)和測(cè)試過程. 這些優(yōu)勢(shì)是不可否認(rèn)的, 你應(yīng)該認(rèn)識(shí)到這些組件實(shí)現(xiàn)的接口可能極大地影響使用它們的程序的行為和性能.
在這一點(diǎn)上, 你可能要問什么樣的接口和性能相關(guān). 一個(gè)類的接口不僅定義了這個(gè)類可以實(shí)現(xiàn)那些功能, 也可以定義它的對(duì)象創(chuàng)建行為和使用它的方法調(diào)用序列. 一個(gè)類怎樣定義它的構(gòu)造函數(shù)和方法決定了一個(gè)對(duì)象是否可以重用, 它的方法是否要?jiǎng)?chuàng)建 -- 或者要求它的客戶端創(chuàng)建 -- 中間對(duì)象, 以及一個(gè)客戶端需要調(diào)用多少方法來使用這個(gè)類.這些因素都會(huì)影響程序的性能.
注意對(duì)象的創(chuàng)建
一個(gè)最基本的 Java 性能管理原則就是: 避免大量的對(duì)象創(chuàng)建. 這不是說你應(yīng)該不創(chuàng)建任何對(duì)象而放棄面向?qū)ο蟮暮锰? 但是你必須在執(zhí)行性能相關(guān)的代碼時(shí), 在緊循環(huán)中注意對(duì)象的創(chuàng)建. 對(duì)象的創(chuàng)建是如此地高代價(jià), 以至于你應(yīng)該在要求性能的情況下避免不必要的臨時(shí)或者中間對(duì)象的創(chuàng)建.
String 類是在那些處理文本的程序中對(duì)象創(chuàng)建的主要來源. 因?yàn)?String 是不可修改的,每當(dāng)一個(gè) String 修改或創(chuàng)建, 就必須創(chuàng)建一個(gè)新的對(duì)象. 結(jié)果就是, 關(guān)注性能的程序應(yīng)該避免大量 String 的使用. 但是, 這通常是不可能的. 甚至當(dāng)你從你的代碼中完全除去對(duì) String 的依賴, 你常常會(huì)發(fā)現(xiàn)你自己在使用一些具有根據(jù) String 定義的接口的組件.所以, 你最后不得不使用 String.
例子: 正規(guī)表達(dá)式匹配
作為一個(gè)例子, 假設(shè)你寫一個(gè)叫做 MailBot 的郵件服務(wù)器. MailBot 需要處理 MIME 頭格式 -- 象發(fā)送日期或者發(fā)送者的 email 地址 -- 在每個(gè)信息的頂部. 使用一個(gè)匹配正規(guī)表達(dá)式的組件來使處理 MIME 頭的過程簡(jiǎn)單一些. MailBot 足夠聰明, 不為每個(gè)頭的行或者頭的元素創(chuàng)建一個(gè) String 對(duì)象. 相反, 它用輸入的文本填充了一個(gè)字符緩沖區(qū), 通過對(duì)緩沖區(qū)的索引來確定要處理的頭的位置. MailBot 會(huì)調(diào)用正規(guī)表達(dá)式匹配器來處理每個(gè)頭行, 所以匹配器的性能就非常重要. 我們以一個(gè)正規(guī)表達(dá)式匹配器類的拙劣的接口作為例子:
public class AwfulRegExpMatcher {
/** Create a matcher with the given regular expression and which will
operate on the given input string */
public AwfulRegExpMatcher(String regExp, String inputText);
/** Retrieve the next match of the pattern against the input text,
returning the matched text if possible or null if not */
public String getNextMatch();
}
甚至在這個(gè)類實(shí)現(xiàn)了一個(gè)有效的正規(guī)表達(dá)式匹配的算法的時(shí)候, 任何大量使用它的程序仍然難以忍受. 既然匹配器對(duì)象和輸入的文本聯(lián)系起來, 每一次你調(diào)用它, 你必須創(chuàng)建一個(gè)新的匹配器對(duì)象. 既然你的目標(biāo)是減少不必要的對(duì)象的創(chuàng)建, 那么使這個(gè)匹配器可以賾將會(huì)是一個(gè)明顯的開始.
下面的類定義演示了你的匹配器的另一個(gè)可能的接口, 允許你重用這個(gè)匹配器, 但仍然很壞.
public class BadRegExpMatcher {
public BadRegExpMatcher(String regExp);
/** Attempts to match the specified regular expression against the input
text, returning the matched text if possible or null if not */
public String match(String inputText);
/** Get the next match against the input text, or return null if no match */
public String getNextMatch();
}
忽略正規(guī)表達(dá)式匹配中的精細(xì)點(diǎn) -- 象返回匹配的子表達(dá)式, 這個(gè)看起來無害的類定義會(huì)出什么問題呢? 從功能上來看, 沒有. 但是從性能的角度來看, 許多. 首先, 匹配器需要它的調(diào)用者創(chuàng)建一個(gè) String 來代表要匹配的文本. MailBot 試圖避免創(chuàng)建 String對(duì)象, 但是當(dāng)它要找到一個(gè)要做正規(guī)表達(dá)式解析的頭時(shí), 它不得不創(chuàng)建一個(gè) String 來滿足 BadRegExpMatcher:
BadRegExpMatcher dateMatcher = new BadRegExpMatcher(...);
while (...) {
...
String headerLine = new String(myBuffer, thisHeaderStart,
thisHeaderEnd-thisHeaderStart);
String result = dateMatcher.match(headerLine);
if (result == null) { ... }
}
第二, 匹配器創(chuàng)建了結(jié)果字符串甚至當(dāng) MailBot 只關(guān)心是否匹配了, 不需要匹配的文本時(shí),這意味著要簡(jiǎn)單使用 BadRegExpMatcher 來確認(rèn)一個(gè)日期頭是否匹配一個(gè)特定的格式, 你必須創(chuàng)建兩個(gè) String 對(duì)象 -- 匹配器的輸入和匹配的結(jié)果. 兩個(gè)對(duì)象可能看起來不多,但是如果你給 MailBot 處理的每個(gè)郵件的每個(gè)頭行都創(chuàng)建兩個(gè)對(duì)象, 這會(huì)極大地影響性能. 錯(cuò)誤不在于 MailBot 的設(shè)計(jì), 而在于 BadRegExpMatcher 類的設(shè)計(jì) -- 或者使用.
注意返回一個(gè)輕量型的 Match 對(duì)象 -- 可以提供 getOffset(), getLength(), egetMatchString() 方法 -- 而不是返回一個(gè) String, 這不會(huì)很大提高性能. 因?yàn)閯?chuàng)建一個(gè) Match 對(duì)象可能比創(chuàng)建一個(gè) String 代價(jià)要小 -- 包括產(chǎn)生一個(gè) char[] 數(shù)組和復(fù)制數(shù)據(jù), 你仍然創(chuàng)建了一個(gè)中間對(duì)象, 對(duì)你的調(diào)用者來說沒有價(jià)值.
這已經(jīng)足夠壞了, BadREgExpMatcher 強(qiáng)迫你使用它想看到的輸入形式, 而不是你可以提供的更有效的形式. 但是使用 BadRegExpMathcer 還有另一個(gè)危險(xiǎn), 潛在地給 MailBot的性能帶來更大的冒險(xiǎn): 在處理郵件頭的時(shí)候, 你開始有避免使用 String 的傾向. 但是既然你被迫創(chuàng)建許多 String 對(duì)象來滿足 BadRegExpMatcher, 你可能被引誘而放棄這個(gè)目標(biāo), 更加自由地使用 String. 現(xiàn)在, 一個(gè)組件的糟糕的設(shè)計(jì)已經(jīng)影響了使用它的程序.
甚至你后來找到了一個(gè)更好的正規(guī)表達(dá)式的組件, 不需要你提供一個(gè) String, 那時(shí)你的整個(gè)程序都會(huì)受影響.
一個(gè)好一些的接口
你怎樣定義 BadRegExpMatcher, 而不引起這樣的問題呢? 首先, BadRegExpMatcher 應(yīng)該不規(guī)定它的輸入. 它應(yīng)該可以接受它的調(diào)用者能夠有效提供的各種輸入格式. 第二, 它不應(yīng)該自動(dòng)給匹配結(jié)果產(chǎn)生一個(gè) String; 應(yīng)該返回足夠的信息, 這樣調(diào)用者如果愿意的話可以生成它. (為方便著想, 它可以提供一個(gè)方法來做這件事, 但不是必須的) 這里有一個(gè)好一些的接口:
class BetterRegExpMatcher {
public BetterRegExpMatcher(...);
/** Provide matchers for multiple formats of input -- String,
character array, and subset of character array. Return -1 if no
match was made; return offset of match start if a match was
made. */
public int match(String inputText);
public int match(char[] inputText);
public int match(char[] inputText, int offset, int length);
/** Get the next match against the input text, if any */
public int getNextMatch();
/** If a match was made, returns the length of the match; between
the offset and the length, the caller should be able to
reconstruct the match text from the offset and length */
public int getMatchLength();
/** Convenience routine to get the match string, in the event the
caller happens to wants a String */
public String getMatchText();
}
新的接口減少了調(diào)用者把輸入轉(zhuǎn)換成匹配器希望的格式這個(gè)要求. MailBot 現(xiàn)在可以象下面這樣調(diào)用 match():
int resultOffset = dateMatcher.match(myBuffer, thisHeaderStart,
thisHeaderEnd-thisHeaderStart);
if (resultOffset < 0) { ... }
這就解決了不創(chuàng)建任何新對(duì)象的目標(biāo). 作為一個(gè)附加的獎(jiǎng)勵(lì), 它的接口設(shè)計(jì)風(fēng)格加到了Java 的 "lots-of-simgle-methos" 設(shè)計(jì)哲學(xué)中.
額外的對(duì)象創(chuàng)建給性能的確切的沖擊依賴于 matth() 所作的工作量. 你可以通過創(chuàng)建和計(jì)時(shí)兩個(gè)正規(guī)表達(dá)式匹配器類, 來確定一個(gè)性能差別的上限. 在 Sun JDK 1.3 中, 上面的代碼片段在 BetterRegExpMatcher 類中大約比 BadRegExpMatcher 類要快 50 倍左右. 使用一個(gè)簡(jiǎn)單的字串匹配的實(shí)現(xiàn), BetterRegExpMatcher 比相對(duì)應(yīng)的 BadRegExpMatcher 要快5倍。
交換類型
BadRegExpMatcher 強(qiáng)迫 MailBot 把輸入文本從字符數(shù)組轉(zhuǎn)換成 String, 結(jié)果是造成了一些不必要的對(duì)象的創(chuàng)建. 更具諷刺意味的是, BadRegExpMatcher 的許多實(shí)現(xiàn)都立即把 String 轉(zhuǎn)換成一個(gè)字符數(shù)組, 使它容易對(duì)輸入文本進(jìn)行訪問. 這樣不僅僅申請(qǐng)了另一齠象, 并且還意味著你做完了所有的工作, 最后的形式和開始時(shí)一樣. MailBot 和 BadRegExpMatcher都不想處理 String -- String 只是看起來象是在組件之間傳遞文本的很明顯的格式.
在上面的 BadRegExpMatcher 例子中, String 類是作為一個(gè)交換類型的. 一個(gè)交換類型是一種不管是調(diào)用者還是被調(diào)用者都不想使用或者以它作為數(shù)據(jù)格式的一種類型, 但是兩個(gè)都能很容易地轉(zhuǎn)換它或者從它轉(zhuǎn)換. 以交換類型定義接口在保持靈活性的同時(shí)減少了接口的復(fù)雜性, 但是有時(shí)簡(jiǎn)單性導(dǎo)致了高代價(jià)的性能.
一個(gè)交換類型最典型的例子是 JDBC ResultSet 接口. 它不可能象任何本地?cái)?shù)據(jù)庫提供的數(shù)據(jù)集一樣提供它的 ResultSet 接口, 但是 JDBC 驅(qū)動(dòng)通過實(shí)現(xiàn)一個(gè) ResultSet 可以很容易地把數(shù)據(jù)庫提供的本地?cái)?shù)據(jù)表示包裝起來. 同樣, 客戶端程序也不能象這樣表示數(shù)據(jù)記錄, 但是你幾乎可以沒有困難地把 ResultSet 轉(zhuǎn)換為想要的數(shù)據(jù)表示. 在 JDBC 的例子中,你接受了這個(gè)層次的花費(fèi), 因?yàn)樗鼛砹藰?biāo)準(zhǔn)化和跨數(shù)據(jù)庫實(shí)現(xiàn)的可移植性的好處. 但是,要注意交換類型帶來的性能代價(jià).
這完全不值得, 使用交換類型對(duì)性能的沖擊不容易度量. 如果你對(duì)上面調(diào)用 BadRegExpMatcher的代碼片段做測(cè)試的話, 它會(huì)在運(yùn)行時(shí)創(chuàng)建 MailBot 的輸入 String; 但是, String 的產(chǎn)生只用來滿足 BadRegExpMatcher. 如果你想評(píng)定一個(gè)組件對(duì)程序性能的真正的沖擊, 你應(yīng)該不僅僅度量它的代碼的資源使用狀況, 還有那些使用它和恢復(fù)的代碼. 這對(duì)于標(biāo)準(zhǔn)的測(cè)試工具此很難完成.
結(jié)論
不是所有的程序都關(guān)注于性能的, 不是所有的程序都有性能問題. 但是對(duì)那些關(guān)注這些的程序, 這篇文章所提到的都很重要, 因?yàn)樗鼈儾皇窃谧詈笠环昼娋涂梢孕薷牡? 既然在你編寫寫代碼使用一個(gè)類以后再修改它的接口非常困難, 那么在你的設(shè)計(jì)時(shí)期就花費(fèi)一點(diǎn)額外的時(shí)間來考慮性能特性.
在第二部分, 我會(huì)演示一些利用可修改性和不可修改性來減少不必要的對(duì)象創(chuàng)建的方法.
第二部分: 減少對(duì)象創(chuàng)建
概要
許多通常的 Java 性能問題都起源于在設(shè)計(jì)過程早期中的類設(shè)計(jì)的思想, 早在許多開發(fā)者開始考慮性能問題之前. 在這個(gè)系列中, Brian Goetz 討論了通常的 Java 性能上的冒險(xiǎn)以及怎么在設(shè)計(jì)時(shí)候避免它們. 在第二部分, 他討論了減少臨時(shí)對(duì)象創(chuàng)建的一些技術(shù)。
雖然許多程序員把性能管理一直推遲到開發(fā)過程的最后, 性能考慮應(yīng)該從第一天起就和設(shè)計(jì)周期結(jié)合在一起. 這個(gè)系列探索一些早期的設(shè)計(jì)思想能夠極大影響應(yīng)用程序性能的方法.在這篇文章里, 我繼續(xù)探索大量臨時(shí)對(duì)象創(chuàng)建的問題, 并且提供一些避免它們的一些技術(shù).
臨時(shí)對(duì)象就是一些生命周期比較短的對(duì)象, 一般用于保存其他數(shù)據(jù)而再?zèng)]有其他用途. 程序員一般用臨時(shí)變量向一個(gè)方法傳遞數(shù)據(jù)或者從一個(gè)方法返回?cái)?shù)據(jù). 第一部分探討了臨時(shí)對(duì)象是怎樣給一個(gè)程序的性能帶來負(fù)面的沖擊, 以及一些類接口的設(shè)計(jì)思想怎樣提供了臨時(shí)對(duì)象的創(chuàng)建. 避免了那些接口的創(chuàng)建, 你就能極大地減少那些影響你的程序性能的臨時(shí)對(duì)象創(chuàng)建的需求,
只是對(duì) String 說不嗎?
當(dāng)它要?jiǎng)?chuàng)建臨時(shí)變量時(shí), String 類是最大的罪人中的一個(gè). 為了演示, 在第一部分我寫了一個(gè)正規(guī)表達(dá)式匹配的例子, 通過和一個(gè)類似的但是經(jīng)過仔細(xì)設(shè)計(jì)的接口相比較, 演示了看起來無害的接口是怎樣引起大量對(duì)象的創(chuàng)建, 而慢上幾倍. 這里是原來的和好一些的類的接口:
BadRegExpMatcher
public class BadRegExpMatcher {
public BadRegExpMatcher(String regExp);
/** Attempts to match the specified regular expression against the input
text, returning the matched text if possible or null if not */
public String match(String inputText);
}
BetterRegExpMatcher
class BetterRegExpMatcher {
public BetterRegExpMatcher(...);
/** Provide matchers for multiple formats of input -- String,
character array, and subset of character array. Return -1 if no
match was made; return offset of match start if a match was
made. */
public int match(String inputText);
public int match(char[] inputText);
public int match(char[] inputText, int offset, int length);
/** If a match was made, returns the length of the match; between
the offset and the length, the caller should be able to
reconstruct the match text from the offset and length */
public int getMatchLength();
/** Convenience routine to get the match string, in the event the
caller happens to wants a String */
public String getMatchText();
}
大量使用 BadREgExpMatcher 的程序比使用 BtterRegExpMatcher 的要慢好多. 首先,調(diào)用者不得不創(chuàng)建一個(gè) String 傳入 match(), 接著 match() 又創(chuàng)建了一個(gè) String 來返回匹配的文本. 結(jié)果是每次調(diào)用都有兩個(gè)對(duì)象創(chuàng)建, 看起來不多, 但是如果要經(jīng)常調(diào)用match(), 這些對(duì)象創(chuàng)建帶給性能的代價(jià)就太打了. BadRegExpMatcher 的性能問題不是在它的實(shí)現(xiàn)中, 而是它的接口; 象它定義的接口, 沒有辦法避免一些臨時(shí)變量的創(chuàng)建.
BetterRegExpMatcher 的 match() 用原類型(整數(shù)和字符數(shù)組)代替了 String 對(duì)象; 不需要?jiǎng)?chuàng)建中間對(duì)象來在調(diào)用者和 match() 之間傳遞信息.
既然在設(shè)計(jì)時(shí)候避免性能問題要比寫完整個(gè)系統(tǒng)以后再修改要容易一些, 你應(yīng)該注意你的類中控制對(duì)象創(chuàng)建的方法. 在 RegExpMatcher 的例子中, 它的方法要求和返回 String 對(duì)象, 就應(yīng)該為潛在的性能冒險(xiǎn)提個(gè)警告信號(hào). 因?yàn)?String 類是不可變的, 除了最常用以外, 所有的 String 參數(shù)在每次調(diào)用處理函數(shù)時(shí)都需要?jiǎng)?chuàng)建一個(gè)新的 String.
不可變性對(duì)于性能來說是否很壞?
因?yàn)?String 經(jīng)常和大量的對(duì)象創(chuàng)建聯(lián)系在一起, 一般來說歸咎于它的不可變性. 許多程序員認(rèn)為不可變的對(duì)象與生俱來對(duì)性能沒有好處. 但是, 事實(shí)多少會(huì)更復(fù)雜一些. 實(shí)際上, 不可變性有時(shí)候提供了性能上的優(yōu)勢(shì), 可變性的對(duì)象有時(shí)候?qū)е滦阅軉栴}. 不管可變性對(duì)性能來說有幫助或者有害, 依賴于對(duì)象是怎么使用的.
程序經(jīng)常處理和修改文本字符串 -- 和不可變性非常不匹配. 每次你想處理一個(gè) String --想查找和解析出前綴或者子串, 變小寫或者大寫, 或者把兩個(gè)字符串合并 -- 你必須創(chuàng)建一個(gè)新的 String 對(duì)象. (在合并的情況下, 編譯器也會(huì)隱藏地創(chuàng)建一個(gè) StringBuffer() 對(duì)象)
另一個(gè)方面, 一個(gè)不可變的對(duì)象的一個(gè)引用可以自由共享, 而不用擔(dān)心被引用的對(duì)象要被修改, 這個(gè)比可變對(duì)象提供性能優(yōu)勢(shì), 就象下一節(jié)例子所說的.
可變的對(duì)象有它們自己的臨時(shí)對(duì)象問題.
在 RegExpMatcher 的例子中, 你看見了 當(dāng)一個(gè)方法返回一個(gè) String 類型時(shí), 它通常需要新建一個(gè) String 對(duì)象. BadRegExpMatcher 的一個(gè)問題就是 match() 返回一個(gè)對(duì)象而不是一個(gè)原類型 -- 但是只因?yàn)橐粋€(gè)方法返回一個(gè)對(duì)象, 不意味著必須有一個(gè)新對(duì)象創(chuàng)建. 考慮一下 java.awt 中的幾何類, 象 Point 和 Rectangle. 一個(gè) Rectangle只是四個(gè)整數(shù)(x, y, 寬度, 長(zhǎng)度)的容器. AWT Component 類存儲(chǔ)組件的位置, 通過getBounds()作為一個(gè)Rectangle 返回
public class Component {
...
public Rectangle getBounds();
}
在上面的例子中, getBounds() 只是一個(gè)存儲(chǔ)元 -- 它只使一些 Component 內(nèi)部的一些狀態(tài)信息可用. getBounds() 需要?jiǎng)?chuàng)建它返回的 Rectangle 嗎? 可能. 考慮一下下面getBounds() 可能的實(shí)現(xiàn).
public class Component {
...
protected Rectangle myBounds;
public Rectangle getBounds() { return myBounds; }
}
當(dāng)一個(gè)調(diào)用者調(diào)用上面例子中的 getBounds(), 沒有新對(duì)象創(chuàng)建 -- 因?yàn)榻M件已經(jīng)知道它在哪里 -- 所以 getBounds() 效率很高. 但是 Rectangle 的可變性又有了其他問題. 當(dāng)一個(gè)調(diào)用者運(yùn)行一下程序會(huì)發(fā)生什么呢?
Rectangle r = component.getBounds();
...
r.height *= 2;
因?yàn)?Rectangle 是可變的, 它在 Component 不知道的情況下使 Component 移動(dòng). 對(duì)象AWT 這樣的 GUI 工具箱來說, 這是個(gè)災(zāi)難, 因?yàn)楫?dāng)一個(gè)組件移動(dòng)以后, 屏幕需要重繪, 件監(jiān)聽器需要被通知, 等等. 所以上面的實(shí)現(xiàn) Component.getBounds() 的代碼看起來很危險(xiǎn). 一個(gè)安全一點(diǎn)的實(shí)現(xiàn)就象下面這樣:
public Rectangle getBounds() {
return new Rectangle(myBounds.x, myBounds.y,
myBounds.height, myBounds.width);
}
但是現(xiàn)在, 每一個(gè) getBounds() 的調(diào)用都創(chuàng)建一個(gè)新對(duì)象, 就象 RegExpMatcher 一樣.實(shí)際上, 下面的代碼片段創(chuàng)建了 4 個(gè)臨時(shí)對(duì)象:
int x = component.getBounds().x;
int y = component.getBounds().y;
int h = component.getBounds().height;
int w = component.getBounds().width;
在 String 的情況中, 對(duì)象創(chuàng)建是必要的, 因?yàn)?String 是不可變的. 但在這個(gè)例子中,對(duì)象的創(chuàng)建也是必要的, 因?yàn)?Rectangle 是可變的. 我們使用 String 避免了這個(gè)問題,在我們的接口中沒有使用對(duì)象. 雖然在 RegExpMatcher 的情況下很好, 這個(gè)方法不總是可行的或者是希望的. 幸運(yùn)的是, 你可以在實(shí)際類的時(shí)候可以使用一些技巧, 來免除太多小對(duì)象的問題, 而不是完全避免小對(duì)象.
減少對(duì)象的技巧 1: 加上好的存取函數(shù)
在 Swing 工具箱的初始版本中, 對(duì)象小對(duì)象的臨時(shí)創(chuàng)建, 象 Point, Rectangle 和 Dimension極大地阻礙了性能. 把它們放在一個(gè) Point 或者 Rectangle 中來一次返回多個(gè)值, 看起來更有效, 實(shí)際上, 對(duì)象的創(chuàng)建比多個(gè)方法調(diào)用代價(jià)更高. 在 Swing 的最后發(fā)布之前, 通過給 Component 和其他一些類加一些新的存取方法, 問題就簡(jiǎn)單地解決了, 就象下面這樣:
public int getX() { return myBounds.x; }
public int getY() { return myBounds.y; }
public int getHeight() { return myBounds.height; }
public int getWidth() { return myBounds.width; }
現(xiàn)在一個(gè)調(diào)用者可以這樣獲取邊界而不用創(chuàng)建對(duì)象:
int x = component.getX();
int y = component.getY();
int h = component.getHeight();
int w = component.getWidth();
getBounds() 的舊形式仍然支持; 好的存取方法簡(jiǎn)單地提供了有效的方法來達(dá)到相同的目的. 結(jié)果是, Rectangle 的接口全部在 Component 中使用. 當(dāng)修改 Swing 包支持和使用這樣的存取函數(shù)后, 在許多 Swing 操作中比以前要快到兩倍. 這很好, 因?yàn)?GUI 代碼非常注意性能 -- 用戶等待發(fā)生一些事, 希望 UI 操作瞬間完成.
使用這個(gè)技術(shù)不好的地方就是你的對(duì)象提供了更多的方法, 有多于一個(gè)的方法來得到相同的信息, 就使文檔更大更復(fù)雜, 可能使用戶害怕. 但是就象 Swing 的例子顯示的, 在關(guān)注性能的情況下, 這樣的優(yōu)化技術(shù)是有效的.
技巧 2: 利用可變性
除了給 Component 加上原類型的存儲(chǔ)函數(shù) -- 象上面討論的 getX() 函數(shù) -- 以外, Java 2 在 AWT 和 Swing 中也使用了另一種技術(shù)來減少對(duì)象創(chuàng)建, 允許一個(gè)調(diào)用者把邊界作為一個(gè) Rectangle 得到, 但是不需要任何臨時(shí)對(duì)象的創(chuàng)建.
public Rectangle getBounds(Rectangle returnVal) {
returnVal.x = myBounds.x;
returnVal.y = myBounds.y;
returnVal.height = myBounds.height;
returnVal.width = myBounds.width;
return returnVal;
}
調(diào)用者仍然需要?jiǎng)?chuàng)建一個(gè) Rectangle 對(duì)象, 但它可以在后來的調(diào)用中重用. 如果一個(gè)調(diào)用者在一系列的 Component 中循環(huán), 可以只創(chuàng)建一個(gè) Rectangle 對(duì)象, 在每個(gè) Component 中重用. 注意這個(gè)技術(shù)只用于可變性對(duì)象; 你不能用這種方法消除 String 的創(chuàng)建.
技巧 3: 得到兩個(gè)中的最好的.
一個(gè)解決在簡(jiǎn)單類(象 Point 之類)的對(duì)象創(chuàng)建的問題, 更好的方法是使 Point 對(duì)象不可? 但是定義一個(gè)可變的子類, 就象下面這樣:
public class Point {
protected int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public final int getX() { return x; }
public final int getY() { return y; }
}
public class MutablePoint extends Point {
public final void setX(int x) { this.x = x; }
public final void setY(int y) { this.y = y; }
}
public class Shape {
private MutablePoint myLocation;
public Shape(int x, int y) { myLocation = new MutablePoint(x, y); }
public Point getLocation() { return (Point) myLocation; }
}
在上面的例子中, Shape 可以安全返回一個(gè) myLocation 的引用, 因?yàn)檎{(diào)用者試圖修改域或者調(diào)用設(shè)置函數(shù)會(huì)失敗. (當(dāng)然, 調(diào)用者仍然可以把 Point 轉(zhuǎn)換為 MutablePoint, 但這明顯不安全, 這樣的調(diào)用者可能得到他們想要的) C++ 程序員可能注意到了這個(gè)技巧很象 C++ 中返回一個(gè) Rectangle 的常量引用(cong Rectangle&) -- 一個(gè) Java 不支持的特點(diǎn).
這個(gè)技巧 -- 返回一個(gè)具有可變的和不可變的類, 只允許讀的對(duì)象, 而不創(chuàng)建新對(duì)象 --在 Java 1.3 類庫 java.math.BigInteger 類中使用. MutableBigInteger 類不可見 --它是一個(gè)只在 java.math 類庫中內(nèi)部使用的私有類型. 但是既然 BigInteger 的一些方法(象 gcd()) 在許多數(shù)學(xué)操作中都有, 在一個(gè)地方操作比創(chuàng)建上百個(gè)臨時(shí)變量性能提高非常大.
結(jié)論
所有的性能優(yōu)化的建議中, 值得記住的是有許多程序的性能可以完全接受的情況. 在這些情況下, 不值得犧牲可讀性, 可維護(hù)性, 抽象, 或者其他可取的程序?qū)傩詠慝@得性能. 但是, 既然許多性能問題的種子在設(shè)計(jì)時(shí)就種下了, 要注意到設(shè)計(jì)思想潛在地對(duì)性能的沖?當(dāng)你設(shè)計(jì)的類在關(guān)注性能的情況性愛使用, 你可以有效地使用這里提到的技巧來減少臨時(shí)對(duì)象的創(chuàng)建,
在第三部分中, 我將看看分布式的應(yīng)用程序中特有的性能問題, 怎樣在設(shè)計(jì)時(shí)候找出和消除它們。
第三部分: 遠(yuǎn)程接口
概述
許多 Java 的通常性能問題來源于設(shè)計(jì)過程早期的類設(shè)計(jì)想法中, 早在開發(fā)者開始考慮性能問題之前. 在這個(gè)系列中, Brian Goetz 討論了一些通常的 Java 性能的冒險(xiǎn), 解釋了怎樣在設(shè)計(jì)時(shí)間避免它們. 在這篇文章中, 它檢驗(yàn)了遠(yuǎn)程應(yīng)用程序中的特定的性能問題.
遠(yuǎn)程調(diào)用的概念
在分布式的應(yīng)用程序中, 一個(gè)運(yùn)行在一個(gè)系統(tǒng)中的對(duì)象可以調(diào)用另一個(gè)系統(tǒng)中的一個(gè)對(duì)象的方法. 這個(gè)通過很多使遠(yuǎn)程對(duì)象表現(xiàn)為本地的結(jié)構(gòu)的幫助而實(shí)現(xiàn). 要訪問一個(gè)遠(yuǎn)程對(duì)象,你首先要找到它, 可以通過使用目錄或者命名服務(wù)來實(shí)現(xiàn), 象 RMI 注冊(cè), JNDI, 或者 CORBA命名服務(wù).
當(dāng)你通過目錄服務(wù)得到一個(gè)遠(yuǎn)程對(duì)象的引用時(shí), 你并沒有得到那個(gè)對(duì)象的實(shí)際的引用, 而是一個(gè)實(shí)現(xiàn)了和遠(yuǎn)程對(duì)象同樣接口的stub對(duì)象的引用. 當(dāng)你調(diào)用一個(gè)stub對(duì)象的方法時(shí), 對(duì)象把方法的所有參數(shù)匯集起來 -- 把它們轉(zhuǎn)化成一個(gè)字節(jié)流的表現(xiàn)形式, 類似于序列化過程. 這個(gè)stub對(duì)象把匯集的參數(shù)通過網(wǎng)絡(luò)傳遞給一個(gè)skeleton對(duì)象, 把參數(shù)分解出來, 你想調(diào)用的實(shí)際的對(duì)象的方法. 然后這個(gè)方法向skeleton對(duì)象返回一個(gè)值, skeleton對(duì)象把它傳送給stub對(duì)象, stub對(duì)象把它分解出來, 傳遞給調(diào)用者. Phew! 一個(gè)單獨(dú)的調(diào)用要做這么多的工作. 很明顯, 除去表面的相似性, 一個(gè)遠(yuǎn)程方法調(diào)用比本地方法調(diào)用更大.
以上描述瀏覽了一些對(duì)于程序性能非常重要的細(xì)節(jié). 當(dāng)一個(gè)遠(yuǎn)程方法返回的不是一個(gè)原類? 而是一個(gè)對(duì)象時(shí), 會(huì)發(fā)生什么? 不一定. 如果返回的對(duì)象是一種支持遠(yuǎn)程方法調(diào)用的類型, 它就創(chuàng)建一個(gè)中stub對(duì)象和一個(gè)skeleton對(duì)象, 在這種情況下需要在注冊(cè)表中查找一個(gè)遠(yuǎn)潭韻,這顯然是一個(gè)高代價(jià)的操作. (遠(yuǎn)程對(duì)象支持一種分布式的垃圾回收的形式, 包括了每一個(gè)參與的 JVM 維護(hù)一個(gè)線程來和其他 JVM 的維護(hù)線程進(jìn)行通訊, 來回傳遞引用信息). 如果返回的對(duì)象不支持遠(yuǎn)程調(diào)用, 這個(gè)對(duì)象所有的域和引用的對(duì)象都要匯集起來, 這也是一個(gè)代價(jià)的操作.
遠(yuǎn)程和本地方法調(diào)用的性能比較
遠(yuǎn)程對(duì)象訪問的性能特征和本地的不一樣:遠(yuǎn)程對(duì)象的創(chuàng)建比本地對(duì)象創(chuàng)建代價(jià)要高. 不僅僅是當(dāng)它不存在時(shí)要?jiǎng)?chuàng)建它, 而且stub對(duì)和skeleton對(duì)象也要?jiǎng)?chuàng)建, 還要互相感知.
遠(yuǎn)程方法調(diào)用還包括網(wǎng)絡(luò)的傳遞 -- 匯集起來的參數(shù)必須發(fā)送到遠(yuǎn)程系統(tǒng), 而且響應(yīng)也需匯集起來, 在調(diào)用程序重新得到控制權(quán)之前發(fā)送回來. 匯集, 分解, 網(wǎng)絡(luò)延時(shí), 實(shí)際的遠(yuǎn)調(diào)用所導(dǎo)致的延遲都加在一起; 客戶端通常是等待所有這些而步驟完成. 一個(gè)遠(yuǎn)程調(diào)用也大地依賴于底層網(wǎng)絡(luò)的延時(shí).
不同的數(shù)據(jù)類型有不同的匯集開支. 匯集原類型相對(duì)來說花費(fèi)少一些; 匯集簡(jiǎn)單的對(duì)象, Point 或者 String 要多一些; 匯集遠(yuǎn)程對(duì)象要多得多, 而匯集那些引用非常多的對(duì)象的對(duì)象(象 collection 等)要更多. 這和本地調(diào)用完全矛盾, 因?yàn)閭鬟f一個(gè)簡(jiǎn)單對(duì)象的引用比一個(gè)復(fù)雜對(duì)象的引用花費(fèi)多.
接口設(shè)計(jì)是關(guān)鍵
設(shè)計(jì)不好的遠(yuǎn)程接口可能完全消除一個(gè)程序的性能. 不幸的是, 對(duì)本地對(duì)象來說好的接口的特性對(duì)遠(yuǎn)程對(duì)象可能不適合. 大量的臨時(shí)對(duì)象創(chuàng)建, 就象在本系列的第一, 二部分討論,也能阻礙分布式的應(yīng)用程序, 但是大量的傳遞更是一個(gè)性能問題. 所以, 調(diào)用一個(gè)在一個(gè)時(shí)對(duì)象(比如一個(gè) Point)中返回多個(gè)值的方法比多次調(diào)用來分別得到它們可能更有效.
實(shí)際遠(yuǎn)程應(yīng)用程序的一些重要的性能指導(dǎo):
提防不必要的數(shù)據(jù)傳遞. 如果一個(gè)對(duì)象要同時(shí)得到幾個(gè)相關(guān)的項(xiàng), 如果可能的話, 在一個(gè)遠(yuǎn)程調(diào)用中實(shí)現(xiàn)可能容易一些.
當(dāng)調(diào)用者可能不必要保持一個(gè)遠(yuǎn)程對(duì)象的引用時(shí), 提防返回遠(yuǎn)程的對(duì)象.當(dāng)遠(yuǎn)程對(duì)象不需要一個(gè)對(duì)象的拷貝時(shí), 提防傳遞復(fù)雜對(duì)象.
幸運(yùn)的是, 你可以通過簡(jiǎn)單查看遠(yuǎn)程對(duì)象的接口來找出所有的問題. 要求做任何高層動(dòng)作的方法調(diào)用序列可以從類接口中明顯看到. 如果你看到一個(gè)通常的高層操作需要許多連續(xù)的遠(yuǎn)程方法調(diào)用, 這就是一個(gè)警告信號(hào), 可能你需要重新查看一下類接口.
減少遠(yuǎn)程調(diào)用代價(jià)的技巧
一個(gè)例子, 考慮下面假定的管理一個(gè)組織目錄的應(yīng)用程序: 一個(gè)遠(yuǎn)程的 Directory 對(duì)象包含了 DirectoryEntry 對(duì)象的引用, 表現(xiàn)了電話簿的入口.
public interface Directory extends Remote {
DirectoryEntry[] getEntries();
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
public interface DirectoryEntry extends Remote {
String getName();
String getPhoneNumber();
String getEmailAddress();
}
現(xiàn)在假設(shè)你想在一個(gè) GUI email 程序中使用 Directory 的東西. 程序首先調(diào)用getEntries() 來得到入口的列表, 接著在每個(gè)入口中調(diào)用 getName(), 計(jì)算結(jié)果的列表,當(dāng)用戶選擇一個(gè)時(shí), 應(yīng)用程序在相應(yīng)的入口調(diào)用 getEmailAdress() 來得到 email 地址.
在你能夠?qū)懸环?email 之前有多少遠(yuǎn)程方法調(diào)用必須發(fā)生? 你必須調(diào)用 getEntries() 一次, 地址簿中每個(gè)入口調(diào)用一次 getName(), 一次 getEmailAddress(). 所以如果在地址中有 N 個(gè)入口, 你必須進(jìn)行 N + 2 次遠(yuǎn)程調(diào)用. 注意你也需要?jiǎng)?chuàng)建 N + 1 個(gè)遠(yuǎn)程對(duì)象引用, 也是一個(gè)代價(jià)很高的操作. 如果你的地址簿有許多入口的話, 不僅僅是打開 email 窗口的時(shí)候非常慢, 也造成了網(wǎng)絡(luò)阻塞, 給你的目錄服務(wù)程序造成高負(fù)載, 導(dǎo)致可擴(kuò)展性的問題.
現(xiàn)在考慮增強(qiáng)的 Directory 接口:
public interface Directory extends Remote {
String[] getNames();
DirectoryEntry[] getEntries();
DirectoryEntry getEntryByName(String name);
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
這將減少多少你的 email 程序所造成的花費(fèi)呢? 現(xiàn)在你可以調(diào)用 Directory.getNames()一次就可以同時(shí)得到所有的名字, 只需要給你想要發(fā)送 email 的容器調(diào)用 getEntryByName() .這個(gè)過程需要 3 個(gè)遠(yuǎn)程方法調(diào)用, 而不是 N + 2, 和兩個(gè)遠(yuǎn)程對(duì)象, 而不是 N + 1 個(gè).如果地址簿有再多一點(diǎn)的名字, 這個(gè)調(diào)用的減少在程序的響應(yīng)和網(wǎng)絡(luò)負(fù)載和系統(tǒng)負(fù)載有很大的不同.
用來減少遠(yuǎn)程調(diào)用和引用傳遞的代價(jià)的技術(shù)叫做使用次要對(duì)象標(biāo)識(shí)符. 使用一個(gè)對(duì)象的標(biāo)屬性, -- 在這個(gè)例子中, 是 name -- 而不是傳回一個(gè)遠(yuǎn)程對(duì)象, 作為對(duì)象的一個(gè)輕量級(jí)曄斗?次要標(biāo)識(shí)符包含了它描述的對(duì)象足夠的信息, 這樣你只需要獲取你實(shí)際需要的遠(yuǎn)程對(duì)象.在這個(gè)目錄系統(tǒng)的例子中, 一個(gè)人的名字是一個(gè)好的次要標(biāo)識(shí)符. 在另一個(gè)例子中, 一個(gè)安全皮包管理系統(tǒng), 一個(gè)采購標(biāo)識(shí)號(hào)可能是一個(gè)好的次要標(biāo)識(shí)符.
另一個(gè)減少遠(yuǎn)程調(diào)用數(shù)量的技巧是塊獲取. 你可以進(jìn)一步給 Directory 接口加個(gè)方法, 來一次獲取多個(gè)需要的 DirectoryEntry 對(duì)象:
public interface Directory extends Remote {
String[] getNames();
DirectoryEntry[] getEntries();
DirectoryEntry getEntryByName(String name);
DirectoryEntry[] getEntriesByName(String names[]);
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
現(xiàn)在你不僅可以得到需要的遠(yuǎn)程 DirectoryEntry , 也可以用單獨(dú)一個(gè)遠(yuǎn)程方法調(diào)用得到要的所有的入口. 雖然這并不減少匯集的代價(jià), 但極大地較少了網(wǎng)絡(luò)往返的次數(shù). 如果網(wǎng)延遲很重要的話, 就可以產(chǎn)生一個(gè)響應(yīng)更快的系統(tǒng)(也能減少這個(gè)網(wǎng)絡(luò)的使用).
照亮去向 RMI 層次的路徑的第三的技巧是不把 DirectoryEntry 作為一個(gè)遠(yuǎn)程對(duì)象, 而把它定義為一個(gè)通常的對(duì)象, 帶有訪問 name, address, email address 和其他域的訪問函數(shù).(在 CORBA 系統(tǒng)中, 我可能要使用類似的 object-by-value 機(jī)制.) 然后, 當(dāng) email 應(yīng)用程序調(diào)用 getEntryName() 時(shí), 它會(huì)獲取一個(gè) entry 對(duì)象的值 -- 不需要?jiǎng)?chuàng)建一個(gè)stub對(duì)象或者skeleton對(duì)象, getEmailAddress() 的調(diào)用也是一個(gè)本地的調(diào)用而不是一個(gè)遠(yuǎn)程的.
當(dāng)然, 所有這些技巧都都依賴于對(duì)遠(yuǎn)程對(duì)象實(shí)際上是怎樣使用的理解上的, 但是對(duì)于這個(gè)理解, 你甚至不需要看一看遠(yuǎn)程類的實(shí)現(xiàn)就可以找出一些潛在的嚴(yán)重性能問題.
以上是“為JAVA性能而設(shè)計(jì)的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對(duì)大家有幫助,更多相關(guān)知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道!
網(wǎng)頁名稱:為JAVA性能而設(shè)計(jì)的示例分析
網(wǎng)頁地址:http://aaarwkj.com/article6/pegjog.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供企業(yè)建站、虛擬主機(jī)、用戶體驗(yàn)、電子商務(wù)、、小程序開發(fā)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)