我一直為一家互聯(lián)網(wǎng)性質(zhì)的實(shí)驗室工作,這個(gè)實(shí)驗室采用我們公司最新的大型服務(wù)器環(huán)境為合作伙伴的產(chǎn)品和解決方案免費做性能測試,我工作的部分就是幫助他們在強大的CMT和SMP服務(wù)器上進(jìn)行性能調優(yōu)。
這些年來(lái),我已經(jīng)為不同的解決方案測試了數十種java應用。許多的產(chǎn)品都是為了解決同樣的領(lǐng)域問(wèn)題,因此這些產(chǎn)品的功能基本都是類(lèi)似的,但在可擴展性上表現的卻非常不同,其中有些不能擴展到64CPU的服務(wù)器上運行,但可以擴展到20臺服務(wù)器做集群運行,有些則只能運行在不超過(guò)2 CPU的機器上。
造成這些差別的原因在于設計產(chǎn)品時(shí)的架構愿景,所有的具備良好擴展性的java應用從需求需求階段、系統設計階段以及實(shí)現階段都為可擴展性做了考慮,所以,你所編寫(xiě)的java應用的可擴展能力完全取決于你的愿景。
可擴展性作為系統的屬性之一,是個(gè)很難定義的名詞,經(jīng)常會(huì )與性能混淆。當然,可擴展性和性能是有關(guān)系的,它的目的是為了達到高性能。但是衡量可擴展性和性能的方法是不一樣的,在這篇文章中,我們采用wikipedia中的定義:
可擴展性是系統、網(wǎng)絡(luò )或進(jìn)程的可選屬性之一,它表達的含義是可以以一種優(yōu)雅的方式來(lái)處理不斷增長(cháng)的工作,或者以一種很明白的方式進(jìn)行擴充。例如:它可以用來(lái)表示系統具備隨著(zhù)資源(典型的有硬件)的增加提升吞吐量的能力。
垂直擴展的意思是給系統中的單節點(diǎn)增加資源,典型的是給機器增加CPU或內存,垂直擴展為操作系統和應用模塊提供了更多可共用的資源,因此它使得虛擬化的技術(shù)(應該是指在一臺機器上運行多個(gè)虛擬機)能夠運行的更加有效。
水平擴展的意思是指給系統增加更多的節點(diǎn),例如為一個(gè)分布式的軟件系統增加新的機器,一個(gè)更清晰的例子是將一臺web服務(wù)器增加為三臺。隨著(zhù)計算機價(jià)格的不斷降低以及性能的不斷提升,以往需要依靠超級計算機來(lái)進(jìn)行的高性能計算的應用(例如:地震分析、生物計算等)現在可以采用這種多個(gè)低成本的應用來(lái)完成。由上百臺普通機器構成的集群可以達到傳統的基于RISC處理器的科學(xué)計算機所具備的計算能力。
這篇文章的第一部分來(lái)討論下垂直擴展Java應用。
如何讓Java EE應用垂直擴展
很多的軟件設計人員和開(kāi)發(fā)人員都認為功能是產(chǎn)品中最重要的因素,而性能和可擴展性是附加的特性和功能完成后才做的工作。他們中大部分人認為可以借助昂貴的硬件來(lái)縮小性能問(wèn)題。
但有時(shí)候他們是錯的,上個(gè)月,我們實(shí)驗室中有一個(gè)緊急的項目,合作伙伴提供的產(chǎn)品在他們客戶(hù)提供的CPU的機器上測試未達到性能的要求,因此合作伙伴希望在更多CPU(8 CPU)的機器上測試他們的產(chǎn)品,但結果卻是在8 CPU的機器上性能反而比4 CPU的機器更差。
為什么會(huì )這樣呢?首先,如果你的系統是多進(jìn)程或多線(xiàn)程的,并且已經(jīng)用盡了CPU的資源,那么在這種情況下增加CPU通常能讓?xiě)煤芎玫牡玫綌U展。
基于java技術(shù)的應用可以很簡(jiǎn)單的使用線(xiàn)程,Java語(yǔ)言不僅可以用來(lái)支持編寫(xiě)多線(xiàn)程的應用,同時(shí)JVM本身在對java應用的執行管理和內存管理上采用的也是多線(xiàn)程的方式,因此通常來(lái)說(shuō)Java應用在多CPU的機器上可以運行的更好,例如Bea weblogic、IBMWebsphere、開(kāi)源的Glassfish和Tomcat等應用服務(wù)器,運行在JavaEE應用服務(wù)器中的應用可以立刻從CMT和SMP技術(shù)中獲取到好處。
但在我的實(shí)驗室中,我發(fā)現很多的產(chǎn)品并不能充分的使用CPU,有些應用在8 CPU的服務(wù)器上只能使用到不到20%的CPU,像這類(lèi)應用即使增加CPU也提升不了多少的。
熱鎖(Hot Lock)是可擴展性的關(guān)鍵障礙
在Java程序中,用來(lái)協(xié)調線(xiàn)程的最重要的工具就是synchronized這個(gè)關(guān)鍵字了。由于java所采用的規則,包括緩存刷新和失效,Java語(yǔ)言中的synchronized塊通常都會(huì )其他平臺提供的類(lèi)似的機制更加的昂貴。即使程序只是一個(gè)運行在單處理器上的單線(xiàn)程程序,一個(gè)synchronized的方法調用也會(huì )比非同步的方法調用慢。
要檢查問(wèn)題是否為采用synchronized關(guān)鍵字造成的,只需要像JVM進(jìn)程發(fā)送一個(gè)QUIT指令(譯者注:在linux上也可以用kill -3 PID的方式)來(lái)獲取線(xiàn)程堆棧信息。如果你看到類(lèi)似下面線(xiàn)程堆棧的信息,那么就意味著(zhù)你的系統出現了熱鎖的問(wèn)題:






synchronized關(guān)鍵字強制執行器串行的執行synchronized中的動(dòng)作。如果很多線(xiàn)程競爭同樣的同步對象,那么只有一個(gè)線(xiàn)程能夠執行同步塊,而其他的線(xiàn)程就只能進(jìn)入blocked狀態(tài)了,如果此時(shí)沒(méi)有其他需要執行的線(xiàn)程,那么處理器就進(jìn)入空閑狀態(tài)了,在這種情況下,增加CPU也帶來(lái)不了多少性能提升。
熱鎖可能會(huì )導致更多線(xiàn)程的切換和系統的調用。當多個(gè)線(xiàn)程競爭同一個(gè)monitor時(shí),JVM必須維護一個(gè)競爭此monitor的線(xiàn)程隊列(同樣,這個(gè)隊列也必須同步),這也就意味著(zhù)更多的時(shí)間需要花費在JVM或OS的代碼執行上,而更少的時(shí)間是用在你的程序上的。
要避免熱鎖現象,以下的建議能帶來(lái)一些幫助:
盡可能的縮短同步塊
當你將線(xiàn)程中持有鎖的時(shí)間盡量縮短后,其他線(xiàn)程競爭鎖的時(shí)間也就變得更短。因此當你需要采用同步塊來(lái)操作共享的變量時(shí),應該將線(xiàn)程安全的代碼放在同步塊的外面,來(lái)看以下代碼的例子:
Code list 1:
上面的代碼片段是為了當更新"schema"變量時(shí)保護這個(gè)共享的變量。但獲取attribute值部分的代碼是線(xiàn)程安全的。因此我們可以將這部分移至同步塊的外面,讓同步塊變得更短一些:
Code list 2:
減小鎖的粒度
當你使用"synchronized"時(shí),有兩種粒度可選擇:"方法鎖"或"塊鎖"。如果你將"synchronized"放在方法上,那么也就意味著(zhù)鎖定了"this"對象。
Code list 3:



對比Code list 2中的代碼,這段代碼就顯得更糟糕些了,因為當調用"updateSchema"方法時(shí),它鎖定了整個(gè)
對象,為了獲得更好的粒度控制,應該僅僅鎖定"schema"變量來(lái)替代鎖定整個(gè)對象,這樣其他不同的方法就可
以保持并行執行了。
避免在static方法上加鎖
最糟糕的狀況是在static方法上加"synchronized",這樣會(huì )造成鎖定這個(gè)class的所有實(shí)例對象。



當使用Java 2D來(lái)為報表生成字體對象時(shí),開(kāi)發(fā)人員放了一個(gè)native的static鎖在"initialize"方法上,不過(guò)這是sun JDK 1.4中才會(huì )出現的,在JDK 5.0中,這個(gè)static lock就消失了。
在Java SE 5.0中使用lock free的數據結構
在Java中,"synchronized"關(guān)鍵字是一個(gè)較簡(jiǎn)單、并且相對來(lái)說(shuō)比較好用的協(xié)作機制,不過(guò)同時(shí)對于管理一個(gè)簡(jiǎn)單的操作(例如增加統計值或更新一個(gè)值)來(lái)說(shuō)就顯得比較重量級了,就像以下的代碼:
Code list 4:
以上的代碼只是用來(lái)鎖定非常簡(jiǎn)單的操作,"synchronized"塊也是非常的短。但是鎖是非常重量級(當鎖被其他線(xiàn)程持有時(shí),線(xiàn)程會(huì )去頻繁嘗試獲取鎖)的,吞吐量會(huì )下降,并且同步鎖的競爭也是很昂貴的。
幸運的是,在Java SE5.0或以上版本,你可以在不使用native代碼的情況下使用硬件級同步語(yǔ)義的wait-free、lock-free的算法。幾乎所有現代的處理器都具有檢測和防止其他處理器并發(fā)修改變量的基礎設施。這些基礎設施稱(chēng)為比較并交換,或CAS。
一個(gè)CAS操作包含三個(gè)參數 -- 一個(gè)內存地址,期待的舊的值以及新的值。 如果內存地址上的值和所期待的舊的值是同一個(gè)的話(huà),處理器將此地址的值更新為新的值;否則它就什么都不做,同時(shí)它會(huì )返回CAS操作前內存地址上的值。一個(gè)使用CAS來(lái)實(shí)現同步的例子如下:
Code list 5:
首先,我們從地址上讀取一個(gè)值,然后執行幾步操作來(lái)產(chǎn)生新的值(例子中只是做加1的操作),最后使用CAS方式來(lái)將地址中的舊值改變?yōu)樾轮?。如果在時(shí)間片段內陸址上的值未改變,那么CAS操作將成功。如果另外的線(xiàn)程同時(shí)修改了地址上的值,那么CAS操作將失敗,但會(huì )檢測到這個(gè)操作失敗,并在while循環(huán)中進(jìn)行重試。CAS最好的原因在于它是硬件級別的實(shí)現并且非常輕量級,如果100個(gè)線(xiàn)程同時(shí)執行這個(gè)increment()方法,最糟糕的情況是在increment方法執行完畢前每個(gè)線(xiàn)程最多嘗試99次。
在JavaSE5.0和以上版本的java.util.concurrent.atomic包中提供了在單個(gè)變量上lock-free和線(xiàn)程安全操作支持的類(lèi)。這些原子變量的類(lèi)都提供了比較和交換的原語(yǔ),它基于各種平臺上可用的最后的native的方式實(shí)現,這個(gè)包內提供了九種原子變量,包括:AtomicInteger;AtomicLong;AtomicReference;AtomicBoolean;array forms ofatomic integer、long、reference;和atomic marked reference和stampedreference類(lèi)。
使用atomic包非常容易,重寫(xiě)上面code list 5的代碼片段:
Code list 6:


幾乎java.util.concurrent包中所有的類(lèi)都直接或間接的采用了原子變量來(lái)替代synchronized。像
ConcurrentLinkedQueue采用了原子變量來(lái)直接實(shí)現wait-free算法,而像ConcurrentHashMap則采用
ReentrantLock來(lái)實(shí)現必要的鎖,而ReentrantLock則是采用原子變量來(lái)維護所有等待鎖的線(xiàn)程隊列。
在我們實(shí)驗室中一個(gè)最成功的關(guān)于lock free算法的案例發(fā)生在一個(gè)金融系統中,當將"Vector"數據結構替換為"ConcurrentHashMap"后,在我們的CMT機器(8核)性能提升了超過(guò)3倍。
競爭條件也會(huì )導致可擴展性出現問(wèn)題
太多的"synchronized"關(guān)鍵字會(huì )導致可擴展性出現問(wèn)題。但在某些場(chǎng)合,缺少"synchronized"也會(huì )導致系統無(wú)法垂直擴展。缺少"synchronized"會(huì )產(chǎn)生競爭場(chǎng)景,在這種場(chǎng)景下允許兩個(gè)線(xiàn)程同時(shí)修改共享的資源,這有可能會(huì )造成破壞共享數據,為什么我說(shuō)它會(huì )導致可擴展性出現問(wèn)題呢?
來(lái)看一個(gè)實(shí)際的例子。這是一個(gè)制作業(yè)的ERP系統,當在我們最新的一臺CMT服務(wù)器(2CPU、16核、128芯)上進(jìn)行性能測試時(shí),我們發(fā)現CPU的使用率超過(guò)90%,這非常讓人驚訝,因為很少有應用能夠在這款機器上擴展的這么好。但我們僅僅興奮了5分鐘,之后我們發(fā)現平均響應時(shí)間非常的慢,同時(shí)吞吐量也降到不可思議的低。那么這些CPU都在干嘛呢?它們不是在忙嗎,那么它們到底在忙些什么呢?通過(guò)OS的跟蹤工具,我們發(fā)現幾乎所有的CPU都在干同一件事-- "HashMap.get()",看起來(lái)所有的CPU都進(jìn)入了死循環(huán),之后我們在不同數量的CPU的服務(wù)器上再測試了這個(gè)應用,結果表明,服務(wù)器擁有越多CPU,那么產(chǎn)生死循環(huán)的概率就會(huì )越高。
產(chǎn)生這個(gè)死循環(huán)的根源在于對一個(gè)未保護的共享變量 --一個(gè)"HashMap"數據結構的操作。當在所有操作的方法上加了"synchronized"后,一切恢復了正常。檢查"HashMap"(JavaSE5.0)的源碼,我們發(fā)現有潛在的破壞其內部結構最終造成死循環(huán)的可能。在下面的代碼中,如果我們使得HashMap中的entries進(jìn)入循環(huán),那么"e.next()"永遠都不會(huì )為null。
Code list 7:
不僅get()方法會(huì )這樣,put()以及其他對外暴露的方法都會(huì )有這個(gè)風(fēng)險,這算jvm的bug嗎?應該說(shuō)不是的,這個(gè)現象很早以前就報告出來(lái)了(詳細見(jiàn):
非阻塞 IO vs. 阻塞IO
Java1.4中引入的java.nio包,允許開(kāi)發(fā)人員在進(jìn)行數據處理時(shí)獲取更好的性能并提供更好的擴展性。NIO提供的非阻塞IO操作允許java應用像其他底層語(yǔ)言(例如c)一樣操作IO。目前已經(jīng)有很多NIO的框架(例如Apache的Mina、Sun的Grizzly)了被廣泛的使用在很多的項目和產(chǎn)品中。
在最近的5個(gè)月內,我們實(shí)驗室有兩個(gè)Java EE項目測試對比了基于傳統的阻塞I/O構建的服務(wù)器和非阻塞I/O構建的服務(wù)器上的性能。他們選擇了Tomcat 5作為基于阻塞I/O的服務(wù)器,Glassfish作為基于非阻塞I/O的服務(wù)器。
首先,他們測試了一些簡(jiǎn)單的JSP頁(yè)面和servlets,得到如下結果:(在一臺4 CPU的服務(wù)器上)
| Concurrent Users | Average Response Time (ms) | |
| Tomcat | Glassfish | |
| 5 | 30 | 138 |
| 15 | 35 | 142 |
| 30 | 37 | 142 |
| 50 | 41 | 151 |
| 100 | 65 | 155 |
當在更多的場(chǎng)景進(jìn)行測試后,隨著(zhù)NIO的能力逐步的展現出來(lái),他們改變了觀(guān)點(diǎn),他們做了以下的測試:
1、比簡(jiǎn)單的JSP、servlet更為復雜的場(chǎng)景,包括EJB、數據庫、文件IO、JMS和事務(wù);
2、模擬更多的并發(fā)用戶(hù),從1000到10000;
3、在不同的硬件環(huán)境上進(jìn)行測試,從2 CPU、4 CPU到16 CPU。
以下的圖為在4 CPU服務(wù)器上的測試結果:
![]() |
Figure 1: Throughput in a 4CPU server
傳統的阻塞I/O為每個(gè)請求分配一個(gè)工作線(xiàn)程,這個(gè)工作線(xiàn)程負責請求的整個(gè)過(guò)程的處理,包括從網(wǎng)絡(luò )讀取請求數據、解析參數、計算或調用其他的業(yè)務(wù)邏輯、編碼結果并將其返回給請求者,然后這個(gè)線(xiàn)程將返回到線(xiàn)程池中供其他線(xiàn)程復用。Tomcat5采用的這種方式在應對完美的網(wǎng)絡(luò )環(huán)境、簡(jiǎn)單的邏輯以及小量的并發(fā)用戶(hù)時(shí)是非常高效的。
但如果請求包括了復雜的邏輯、或需要和外部的系統(例如文件系統、數據庫或消息服務(wù)器)進(jìn)行交互時(shí),工作線(xiàn)程在其處理的大部分時(shí)間都會(huì )處于等待同步的調用或網(wǎng)絡(luò )傳輸返回的狀態(tài)中,這個(gè)阻塞的線(xiàn)程會(huì )被請求持有直到請求處理完畢,但操作系統需要暫停線(xiàn)程來(lái)保證CPU能夠處理其他的請求,如果客戶(hù)端和服務(wù)器端的網(wǎng)絡(luò )狀況不太好的話(huà),網(wǎng)絡(luò )的延時(shí)會(huì )導致線(xiàn)程被阻塞更長(cháng)時(shí)間,在更糟的狀況下,當需要keep-alive的話(huà),當前的工作線(xiàn)程會(huì )在請求處理完畢后阻塞很長(cháng)一段時(shí)間,在這樣的情況下,為了更好的使用CPU,就必須增加更多的工作線(xiàn)程了。
Tomcat采用了一個(gè)線(xiàn)程池,每個(gè)請求都會(huì )被線(xiàn)程池中一個(gè)空閑的線(xiàn)程進(jìn)行處理。"maxThreads"表示Tomcat能創(chuàng )建的處理請求的最大線(xiàn)程數。如果我們把"maxThreads"設置的太小的話(huà),就不能充分的使用CPU了,更為重要的是,隨著(zhù)并發(fā)用戶(hù)的增長(cháng),會(huì )有很多請求被服務(wù)器拋棄和拒絕。在此次測試中,我們將"maxThreads"設置為了1000(這對于Tomcat來(lái)說(shuō)有些太大了),在這樣的設置下,當并發(fā)用戶(hù)增長(cháng)到較高數量時(shí),Tomcat會(huì )創(chuàng )建很多的線(xiàn)程。大量的Java線(xiàn)程會(huì )導致JVM和OS忙于執行和維護這些線(xiàn)程,而不是執行業(yè)務(wù)邏輯處理,同時(shí),太多的線(xiàn)程也會(huì )消耗更多的JVM heap內存(每個(gè)線(xiàn)程堆棧需要占用一些內存),并且會(huì )導致更為頻繁的gc。
Glassfish不需要這么多的線(xiàn)程,在非阻塞IO中,一個(gè)工作線(xiàn)程并不會(huì )綁定到一個(gè)特定的請求上,如果請求被某些原因所阻塞,那么這個(gè)線(xiàn)程將被其他的請求復用。在這樣的方式下,Glassfish可以用幾十個(gè)工作線(xiàn)程來(lái)處理幾千的并發(fā)用戶(hù)。通過(guò)限制線(xiàn)程資源,非阻塞IO擁有了更好的可擴展性,這也是Tomcat 6采用非阻塞IO的原因了。
![]() |
Figure 2: scalability test result
單線(xiàn)程任務(wù)問(wèn)題
幾個(gè)月前我們實(shí)驗室測試了一個(gè)基于JavaEE的ERP系統,它其中的一個(gè)測試場(chǎng)景是為了產(chǎn)生非常復雜的分析報告,我們在不同的服務(wù)器上測試了這個(gè)應用場(chǎng)景,發(fā)現竟然是在最便宜的AMDPC服務(wù)器上擁有最好的性能。這臺AMD的服務(wù)器只有兩個(gè)2.8HZ的CPU以及4G的內存,但它的性能竟然超過(guò)了昂貴的擁有8CPU和32G內存的SPARC服務(wù)器。
原因就在于這個(gè)場(chǎng)景是個(gè)單線(xiàn)程的任務(wù),它同時(shí)只能被一個(gè)用戶(hù)運行(并發(fā)的多用戶(hù)執行在這個(gè)案例中毫無(wú)意義),因此當運行時(shí)它只使用了一個(gè)CPU,這樣的任務(wù)是沒(méi)法擴展到多個(gè)處理器的,在大多數時(shí)候,這種場(chǎng)景下的性能僅取決于CPU的運行速度。
并行是解決這個(gè)問(wèn)題的方案。為了讓一個(gè)單線(xiàn)程的任務(wù)并行執行,你需要按順序找出這個(gè)操作的過(guò)程中從某種程度上來(lái)講不依賴(lài)的操作,然后采用多線(xiàn)程從而實(shí)現并行。在上面的案例中,客戶(hù)重新定義了"分析報告產(chǎn)生"的任務(wù),改為先生成月度報告,之后基于產(chǎn)生的這些12個(gè)月的月度報告來(lái)生成分析報告,由于最終用戶(hù)并不需要“月度報告”,因此這些“月度報告”只是臨時(shí)產(chǎn)生的結果,但"月度報告"是可以并行生成的,然后用于快速的產(chǎn)生最后的分析報告,在這樣的方式下,這個(gè)應用場(chǎng)景可以很好的擴展到4 CPU的SPARC服務(wù)器上運行,并且在性能上比在A(yíng)MD Server高80%多。
重新調整架構和重寫(xiě)代碼的解決方案是一個(gè)耗時(shí)并且容易出現錯誤的工作。在我們實(shí)驗室中的一個(gè)項目中采用了JOMP來(lái)為其單線(xiàn)程的任務(wù)獲得并行性。JOMP是一個(gè)基于線(xiàn)程的SMP并行編程的JavaAPI。就像OpenMP,JOMP也是根據編譯指示來(lái)插入并行運行的代碼片段到常規的程序中。在Java程序中,JOMP通過(guò)//omp這樣的指示方式來(lái)表示需要并行運行的部分。JOMP程序通過(guò)運行一個(gè)預編譯器來(lái)處理這些//omp的指示并生成最終的java代碼,這些java代碼再被正常的編譯和執行。JOMP支持OpenMP的大部分特性,包括共享的并行循環(huán)和并行片段,共享變量,threadlocal變量以及reduction變量。以下的代碼為JOMP程序的示例:
Code list 8:
就像大部分的并行編譯器,JOMP也是關(guān)注于loop-level和集合的并行運算,研究如何同時(shí)執行不同的迭代。為了并行化,兩個(gè)迭代之間不能產(chǎn)生任何的數據依賴(lài),這也就是說(shuō),不能依賴(lài)于其他任何一個(gè)執行后產(chǎn)生的計算結果。要編寫(xiě)一個(gè)JOMP程序并不是容易的事。首先,你必須熟練使用OpenMP的指示,同時(shí)還得熟悉JVM對于這些指示的內存模型映射,最后你需要知道在你的業(yè)務(wù)邏輯代碼的正確的地方放置正確的指示。
另外一個(gè)選擇是采用Parallel Java。ParallelJava,就像JOMP一樣,也支持OpenMP的大部分特性;但又不同于JOMP,PJ的并行結構部分是通過(guò)在代碼中調用PJ的類(lèi)來(lái)實(shí)現,而不是通過(guò)插入預編譯的指示,因此,"Parallel Java"不需要另外的預編譯過(guò)程。ParallelJava不僅對于在多CPU上并行有效,對于多節點(diǎn)的擴展能力上也同樣有效。以下的代碼是"Parallel Java"程序的示例:
Code list 9:
擴展使用更多的內存
內存是應用的重要資源。足夠的內存對于任何應用而言都是關(guān)鍵的,尤其是數據庫系統和其他I/O操作頻繁的系統。更多的內存意味著(zhù)更大的共享內存空間以及更大的數據緩沖,這也就使得應用能夠更多的從內存中讀取數據而不是緩慢的磁盤(pán)中讀取。
Javagc將程序員從繁瑣的內存分配和回收中解脫了出來(lái),從而使得程序員能夠更加高效的編寫(xiě)代碼。但gc不好的地方在于當gc運行時(shí),幾乎所有工作的線(xiàn)程都會(huì )被掛起。另外,在gc環(huán)境下,程序員缺少調度CPU來(lái)回收那些不再使用的對象的控制能力。對于那些幾乎實(shí)時(shí)的系統而言,例如電信系統和股票交易系統,這種延遲和缺少控制的現象是很大的風(fēng)險。
回到Java應用在給予更多的內存時(shí)是否可以擴展的問(wèn)題上,答案是有些時(shí)候是的。太小的內存會(huì )導致gc頻繁的執行,足夠的內存則保證JVM花費更多的時(shí)間來(lái)執行業(yè)務(wù)邏輯,而不是進(jìn)行g(shù)c。
但它并不一定是這樣的,在我們實(shí)驗室中出現的真實(shí)例子是一個(gè)構建在64位JVM上的電信系統。使用64位JVM,應用可以突破32位JVM中4GB內存的限制,測試時(shí)使用的是一臺4CPU/16G內存的服務(wù)器,其中12GB的內存分配給了java應用使用,為了提高性能,他們在初始化時(shí)就緩存了超過(guò)3,000,000個(gè)的對象到內存中,以免在運行時(shí)創(chuàng )建如此多的對象。這個(gè)產(chǎn)品在第一個(gè)小時(shí)的測試中運行的非???,但突然,系統差不多停止運行了30多分鐘,經(jīng)過(guò)檢測,發(fā)現是因為gc導致了系統停止了半個(gè)小時(shí)。
gc是從那些不再被引用的對象回收內存的過(guò)程。不被引用的對象是指應用中不再使用的對象,因為所有對于這些對象的引用都已經(jīng)不在應用的范圍中了。如果一堆巨大的活動(dòng)的對象存在在內存中(就像3,000,000個(gè)緩存的對象),gc需要花費很長(cháng)的時(shí)間來(lái)檢查這些對象,這就是為什么系統停止了如此長(cháng)乃至不可接受的時(shí)間。
在我們實(shí)驗室中測試過(guò)的以?xún)却鏋橹行牡腏ava應用中,我們發(fā)現具備有如下特征:
這樣的應用是不好做擴展的,當并發(fā)的用戶(hù)數增長(cháng)時(shí),這些應用所使用的內存也會(huì )大幅度的增長(cháng)。如果大量的活動(dòng)對象無(wú)法被及時(shí)的回收,JVM將會(huì )在gc上消耗很長(cháng)的時(shí)間,另外,如果給予了太大的內存(在64位JVM上),在運行了相對較長(cháng)的時(shí)間后,jvm會(huì )花費相當長(cháng)的一段時(shí)間在gc上,因此結論是如果給jvm分配了太多的內存的話(huà),java應用將不可擴展。在大部分場(chǎng)合下,給jvm分配3G內存(通過(guò)"-Xmx"屬性)是足夠(在windows和linux中,32位的系統最多只能分配2G的內存)的。如果你擁有更多的內存,請將這些內存分配給其他的應用,或者就將它留給OS使用,許多OS都會(huì )使用空閑的內存來(lái)作為數據的緩沖和緩存來(lái)提升IO性能。實(shí)時(shí)JVM(JSR001)可以讓開(kāi)發(fā)人員來(lái)控制內存的回收,應用基于此特性可以告訴JVM:“這個(gè)巨大的內存空間是我的緩存,我將自己來(lái)管理它,請不要自動(dòng)對它進(jìn)行回收”,這個(gè)功能特性使得Java應用也能夠擴展來(lái)支持大量的內存資源,希望JVM的提供者們能將這個(gè)特性在不久的將來(lái)帶入到免費的JVM版本中。
為了擴展這些以?xún)却鏋橹行牡膉ava應用,你需要多個(gè)jvm實(shí)例或者多臺機器節點(diǎn)。
其他垂直擴展的問(wèn)題
有些Java EE應用的擴展性問(wèn)題并不在于其本身,有些時(shí)候外部系統的限制會(huì )成為系統擴展能力的瓶頸,這些瓶頸可能包括:
這些不僅僅是Java EE應用的問(wèn)題,對于所有平臺的所有系統而言同樣如此。為了解決這些問(wèn)題,需要從系統的各個(gè)層面來(lái)從數據庫管理員、系統工程師和網(wǎng)絡(luò )分析人員處得到幫助。
這篇文章的第二個(gè)部分將來(lái)探討水平擴展的問(wèn)題。
聯(lián)系客服