Twitter 工程師談 JVM 調優

文牛武人發表於2016-03-26

一. 調優需要關注的幾個方面

  • 記憶體調優
  • CPU 使用調優
  • 鎖競爭調優
  • I/O 調優

二. Twitter 最大的敵人:延遲

導致延遲的幾個原因?

  • 最大影響因素是 GC
  • 其他的有:鎖和執行緒排程、I/O、演算法資料結構選取不當效率低

三. 記憶體效能調優

(1)記憶體佔用調優

OutOfMemoryError 異常原因:可能真的資料量太大、可能要資料顯示的太多、可能記憶體洩露

資料量太大觀察及解決:

  • 檢視 GC 日誌, 看 Full GC 前後記憶體變化, 變化不大說明確實資料量太大
  • 嘗試增加 JVM 的記憶體使用
  • 考慮這些資料是否真的需要都在記憶體中嗎? 可以考慮使用: LRU 演算法換入換出等, 弱引用(Soft References)

資料臃腫(Fat data)

  • 當你想做一些奇怪的事情時候回發生資料佔用太大問題,比如:把整個社交圖譜載入到單個 JVM 例項上、載入全部使用者的後設資料到單個 JVM 例項上
  • 在 Twitter 這樣大的規模下減少內部資料呈現工作

資料臃腫原因:

(1)物件頭(JVM 物件頭一般佔用兩個機器碼,在 32-bit JVM 上佔用 64bit, 在 64-bit JVM 上佔用 128bit 即 16 bytes, 例如:new java.lang.Object() 佔用 16 bytes; new byte[0] 佔用 24 bytes)  更多物件頭內容參考:http://blog.csdn.net/wenniuwuren/article/details/50939410

(2)填充補全

看個例子

public static class D {  
    byte d1;  
}  

public static class E extends D {  
    byte e1;  
}

new D() 佔用 24 bytes 空間, new E() 佔用 32 bytes 空間。 具體空間計算參考:http://blog.csdn.net/wenniuwuren/article/details/50958892

現在一般是 64-bit 的 JVM,64-bit 的指標會導致 CPU 快取相比 32-bit 指標減少很多, 所以建議 JVM 引數加入 -XX:+UseCompressedOops 採用指標壓縮將 64-bit 指標壓縮為 32-bit, 但是卻又能使用 64-bit 的記憶體空間, 達到一舉兩得的作用。另外,建議最大堆小於 30G。

儘量別使用原始型別物件的包裝類

在 Scala 2.7.7 中:Seq[Int] 存 Integer,Array[Int] 存 int, 第一個空間佔用 (24 + 32*length) bytes,第二個空間佔用 (24 + 4*length) bytes。

在 Scala 2.8 中修復了這個問題, 從這我們可以看出:

  • 你不清楚你所使用類庫的效能特徵(比如能用 int 就用 int)
  • 除非在效能分析工具下執行, 否則你可能永遠不知道這個問題

Map 空間佔用(Map footprints)

  • Guava MapMaker.makeMap() 佔用 2272 bytes
  • MapMaker.concurrencyLevel(1).makeMap() 佔用 352 bytes

小心使用 Thread Local

典型的問題線上程池 m*n 的資源相關,如 200 執行緒池使用了 50 個連線,最終有 10000 個連線快取

考慮使用同步物件或者每次新建一個物件

四. 與延遲做鬥爭

效能三角

圖1:記憶體佔用下降,延遲下降,吞吐量上升

圖2:壓縮(Compactness,即減小記憶體佔用)率上升,吐量上升,響應速度上升

新生代是如何工作的?

  • 所有新物件分配在 Eden 代,因為新生代 GC 有壓縮,所以記憶體分配用指標碰撞
  • 當 Eden 滿的時候,進行一次 stop-the-world 的 Minor GC,存活下來的放到 Survivor
  • 經過幾次 Minor GC,還存活下來的物件會被提升(tenured)到老年代

理想化得新生代操作

  • Eden 代足夠容納超過一組併發的請求和響應物件(這樣沒有 stop-the-world,吞吐量會比較高)
  • 每個 Survivor 空間足夠容納活躍物件和有年齡的物件(減少過早提升到老年代)
  • 提升閾值正好能讓存活時間長的物件早點提升到老年代(給 Survivor 騰出空間)

從新生代開始調優

  • 列印詳細 GC 日誌, 如開啟 JVM 引數:-XX:+PrintGCDetails,-XX:+PrintGCDateStamps,-XX:+PrintHeapAtGC,-XX:+PrintTenuringDistribution 等等…
  • 關注 Survivor 大小,設定合適的 Survivor 大小
  • 關注提升閾值,使長期存活物件快速提升到老年代

(1)-XX:+PrintHeapAtGC

Heap after GC invocations=1 (full 0):  
 par new generation   total 943744K, used 54474K [0x0000000757000000, 0x0000000797000000, 0x0000000797000000)  
  eden space 838912K,   0% used [0x0000000757000000, 0x0000000757000000, 0x000000078a340000)  
  from space 104832K,  51% used [0x00000007909a0000, 0x0000000793ed2ae0, 0x0000000797000000)  
  to   space 104832K,   0% used [0x000000078a340000, 0x000000078a340000, 0x00000007909a0000)  
 concurrent mark-sweep generation total 1560576K, used 0K [0x0000000797000000, 0x00000007f6400000, 0x00000007f6400000)  
 concurrent-mark-sweep perm gen total 159744K, used 38069K [0x00000007f6400000, 0x0000000800000000, 0x0000000800000000)  
}

(2)-XX:+PrintTenuringDistribution

Desired survivor size 53673984 bytes, new threshold 4 (max 6)  
- age   1:    9165552 bytes,    9165552 total  
- age   2:    2493880 bytes,   11659432 total  
- age   3:    6817176 bytes,   18476608 total  
- age   4:   36258736 bytes,   54735344 total  
: 899459K->74786K(943744K), 0.0654030 secs] 1225769K->401096K(2504320K), 0.0657530 secs] [Times: user=0.55 sys=0.00, real=0.07 secs]

CMS 調優

  • CMS 收集器需要更多的記憶體, 儘量多分配就對了
  • 減少碎片、避免 Full GC
  • -XX:CMSInitiatingOccupancyFraction=n n一般設定為 75-80(太早啟動降低吞吐量,太晚啟動導致 concurrent mode failed)

響應速度還是太慢?

  • Minor GC 時有太多存活物件,嘗試減少新生代空間,減少 Survivor 空間,減少晉升閾值
  • 太多執行緒。嘗試找到最小的併發層次或者增加更多 JVM 例項
  • 嘗試使用 Volatile 而不是 synchronized 減少鎖競爭,嘗試使用 Atomic* 的原子類

用分配 slab 應對 CMS 的碎片問題

Apache 的 Cassandra 內部使用 slab 分配。每個 slab 大小為 2MB,使用 CAS 複製 byte[] 到裡面,使用 Cassandra 前開銷為 30-60 秒每小時, 使用後在3天零十小時開銷 5 秒。

使用分配 slab 的方式有一些侷限性:在快取滿的時候才把快取內容寫進磁碟,而且物件需要轉化為二進位制等問題。

相關文章