多核平臺下的Java優化

現在多核CPU是主流。利用多核技術,可以有效發揮硬件的能力,提升吞吐量,對於Java程序,可以實現併發垃圾收集。但是Java利用多核技術也帶來了一些問題,主要是多線程共享內存引起了。目前內存和CPU之間的帶寬是一個主要瓶頸,每個核可以獨享一部分高速緩存,可以提高性能。JVM是利用操作系統的“輕量級進程”實現線程,所以線程每操作一次共享內存,都無法在高速緩存中命中,是一次開銷較大的系統調用。所以區別於普通的優化,針對多核平臺,需要進行一些特殊的優化。

  代碼優化

  線程數要大於等於核數

  如果使用多線程,只有運行的線程數比核數大,纔有可能榨乾CPU資源,否則會有若干核閒置。要注意的是,如果線程數目太多,就會佔用過多內存,導致性能不升反降。JVM的垃圾回收也是需要線程的,所以這裏的線程數包含JVM自己的線程。

  儘量減少共享數據寫操作

  每個線程有自己的工作內存,在這個區域內,系統可以毫無顧忌的優化,如果去讀共享內存區域,性能也不會下降。但是一旦線程想寫共享內存(使用volatile關鍵字),就會插入很多內存屏障操作(Memory Barrier或者Memory Fence)指令,保證處理器不亂序執行。相比寫本地線程自有的變量,性能下降很多。處理方法是儘量減少共享數據,這樣也符合”數據耦合”的設計原則。

  使用synchronize關鍵字

  在Java1.5中,synchronize是性能低效的。因爲這是一個重量級操作,需要調用操作接口,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock對象,性能更高一些。但是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在Java1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化餘地。

  使用樂觀策略

  傳統的同步併發策略是悲觀的。表現語義爲:多線程操作一個對象的時候,總覺得會有兩個線程在同時操作,所以需要鎖起來。樂觀策略是,假設平時就一個線程訪問,當出現了衝突的時候,再重試。這樣更高效一些。Java的AtomicInteger就是使用了這個策略。

  使用線程本地變量(ThreadLocal)

  使用ThreadLocal可以生成線程本地對象的副本,不會和其他線程共享。當該線程終止的時候,其本地變量可以全部回收。

  類中Field的排序

  可以將一個類會頻繁訪問到的幾個field放在一起,這樣他們就有更多的可能性被一起加入高速緩存。同時最好把他們放在頭部。基本變量和引用變量不要交錯排放。

  批量處理數組

  現在處理器可以用一條指令來處理一個數組中的多條記錄,例如可以同時向一個byte數組中讀或者寫store記錄。所以要儘量使用System.arraycopy ()這樣的批量接口,而不是自己操作數組。

  JVM優化

  啓用大內存頁

  現在一個操作系統默認頁是4K。如果你的heap是4GB,就意味着要執行1024*1024次分配操作。所以最好能把頁調大。這個配額設計操作系統,單改 Jvm 是不行的。Linux上的配置有點複雜,不詳述。

  在Java1.6中UseLargePages是默認開啓的,LasrgePageSzieInBytes被設置成了4M。筆者看到一些情況下配置成了128MB,在官方的性能測試中更是配置到256MB。

  啓用壓縮指針

  Java的64的性能比32慢,原因是因爲其指針由32位擴展到64位,雖然尋址空間從4GB擴大到256 TB,但導致性能的下降,並佔用了更多的內存。所以對指針進行壓縮。壓縮後的指針最多支持32GB內存,並且可以獲得32位JVM的性能。

  在JDK6 update 23默認開啓了,之前的版本可以使用-XX:+UseCompressedOops來啓動配置。

  性能可以看這個評測,性能的提升是很可觀。

  啓用NUMA

  numa是一個CPU的特性。SMP架構下,CPU的核是對稱,但是他們共享一條系統總線。所以CPU多了,總線就會成爲瓶頸。在NUMA架構下,若干CPU組成一個組,組之間有點對點的通訊,相互獨立。啓動它可以提高性能。

  NUMA需要硬件,操作系統,JVM同時啓用,才能啓用。Linux可以用numactl來配置numa,JVM通過-XX:+UseNUMA來啓用。

  激進優化特性

  在Java1.6中,激進優化(AggressiveOpts)是默認開啓的。激進優化是一般有一些下一個版本纔會發佈的優化選項。但是有可能造成不穩定。前段時間以訛傳訛的JDK7的 Bug,就是開啓這個選項後測到的。

  逃逸分析

  讓一個對象在一個方法內創建後,如果他傳遞出去,就可以稱爲方法逃逸;如果傳遞到別的線程,成爲線程逃逸。如果能知道一個對象沒有逃逸,就可以把它分配在棧而不是堆上,節約GC的時間。同時可以將這個對象拆散,直接使用其成員變量,有利於利用高速緩存。如果一個對象沒有線程逃逸,就可以取消其中一切同步操作,很大的提高性能。

  但是逃逸分析是很有難度的,因爲花了cpu去對一個對象去分析,要是他不逃逸,就無法優化,之前的分析血本無歸。所以不能使用複雜的算法,同時現在的 JVM 也沒有實現棧上分配。所以開啓之後,性能也可能下降。

  可以使用-XX:+DoEscapeAnalysis來開啓逃逸分析。

  高吞吐量GC配置

  對於高吞吐量,在年輕態可以使用Parallel Scavenge,年老態可以使用Parallel Old垃圾收集器。

  使用-XX:+UseParallelOldGC開啓

  可以將-XX:ParallelGCThreads根據CPU的個數進行調整。可以是CPU數的1/2或者5/8

  低延遲GC配置

  對於低延遲的應用,在年輕態可以使用ParNew,年老態可以使用CMS垃圾收集器。

  可以使用-XX:+UseConcMarkSweepGC和-XX:+UseParNewGC打開。

  可以將-XX:ParallelGCThreads 根據CPU的個數進行調整。可以是CPU數的1/2或者5/8

  可以調整-XX:MaxTenuringThreshold(晉升年老代年齡)調高,默認是15。這樣可以減少年老代GC的壓力

  可以-XX:TargetSurvivorRatio,調整Survivor的佔用比率。默認50%。調高可以提供Survivor區的利用率

  可以調整-XX:SurvivorRatio,調整Eden和Survivor的比重。默認是8。這個比重越小,Survivor越大,對象可以在年輕態呆更多時間。


發佈了24 篇原創文章 · 獲贊 14 · 訪問量 39萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章