Linkedin工程师是如何优化他们的Java代码的
最近在刷各大公司的技術博客的時候,我在Linkedin的技術博客上面發(fā)現(xiàn)了一篇很不錯博文。這篇博文介紹了Linkedin信息流中間層Feed Mixer,它為Linkedin的Web主頁,大學主頁,公司主頁以及客戶端等多個分發(fā)渠道提供支撐(如下圖所示)。
在Feed Mixer里面用到了一個叫做SPR(念“super”)的庫。博文講的就是如何優(yōu)化SPR的java代碼。下面就是他們總結的優(yōu)化經驗。
1. 謹慎對待Java的循環(huán)遍歷
Java中的列表遍歷可比它看起來要麻煩多了。就以下面兩段代碼為例:
- A:
| 1 2 3 4 | privatefinal List<Bar> _bars; for(Bar bar : _bars) { ????//Do important stuff } |
- B:
| 1 2 3 4 5 | privatefinal List<Bar> _bars; for(inti = 0; i < _bars.size(); i++) { Bar bar = _bars.get(i); //Do important stuff } |
代碼A執(zhí)行的時候?會為這個抽象列表創(chuàng)建一個迭代器,而代碼B就直接使用?get(i)?來獲取元素,相對于代碼A省去了迭代器的開銷。
實際上這里還是需要一些權衡的。代碼A使用了迭代器,保證了在獲取元素的時候的時間復雜度是?O(1)?(使用了?getNext()?和?hasNext()?方法),最終的時間復雜度為?O(n)?。但是對于代碼B,循環(huán)里每次在調用?_bars.get(i)?的時候花費的時間復雜度為?O(n)??(假設這個list為一個 LinkedList),那么最終代碼B整個循環(huán)的時間復雜度就是?O(n^2)? (但如果代碼B里面的list是?ArrayList,?那?get(i)?方法的時間復雜度就是?O(1)了)。所以在決定使用哪一種遍歷的方式的時候,我們需要考慮列表的底層實現(xiàn),列表的平均長度以及所使用的內存。最后因為我們需要優(yōu)化內存,再加上?ArrayList?在大多數(shù)情況下查找的時間復雜度為?O(1)?,最后決定選擇代碼B所使用的方法。
2.在初始化的時候預估集合的大小
從Java的這篇?文檔我們可以了解到:?“一個HashMap 實例有兩個影響它性能的因素:初始大小和加載因子(load factor)。 […] 當哈希表的大小達到初始大小和加載因子的乘積的時候,哈希表會進行 rehash操作 […] 如果在一個HashMap 實例里面要存儲多個映射關系時,我們需要設置足夠大的初始化大小以便更有效地存儲映射關系而不是讓哈希表自動增長讓后rehash,造成性能瓶頸。”
在Linkedin實踐的時候,常常碰到需要遍歷一個?ArrayList?并將這些元素保存到?HashMap?里面去。將這個?HashMap?初始化預期的大小可以避免再次哈希所帶來的開銷。初始化大小可以設置為輸入的數(shù)組大小除以默認加載因子的結果值(這里取0.7):
- 優(yōu)化前的代碼:
| 1 2 3 4 5 6 7 8 9 | HashMap<String,Foo> _map; voidaddObjects(List<Foo> input) { ??_map = newHashMap<String, Foo>(); ??for(Foo f: input) ??{ ????_map.put(f.getId(), f); ??} } |
- 優(yōu)化后的代碼
| 1 2 3 4 5 6 7 8 9 | HashMap<String,Foo> _map; voidaddObjects(List<Foo> input) { _map = newHashMap<String, Foo>((int)Math.ceil(input.size() / 0.7)); for(Foo f: input) { _map.put(f.getId(), f); } } |
3. 延遲表達式的計算
在Java中,所有的方法參數(shù)會在方法調用之前,只要有方法參數(shù)是一個表達式的都會先這個表達式進行計算(從左到右)。這個規(guī)則會導致一些不必要的操作。考慮到下面一個場景:使用ComparisonChain比較兩個?Foo?對象。使用這樣的比較鏈條的一個好處就是在比較的過程中只要一個 compareTo 方法返回了一個非零值整個比較就結束了,避免了許多無謂的比較。例如現(xiàn)在這個場景中的要比較的對象最先考慮他們的score, 然后是 position, 最后就是?_bar?這個屬性了:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | publicclass Foo { privatefloat _score; privateint _position; privateBar _bar; ?? publicint compareTo (Foo other) { returnComparisonChain.start(). compare(_score, other.getScore()). compare(_position, other.getPosition()). compare(_bar.toString(), other.getBar().toString()). result; } } |
但是上面這種實現(xiàn)方式總是會先生成兩個?String?對象來保存?bar.toString()?和other.getBar().toString()?的值,即使這兩個字符串的比較可能不需要。避免這樣的開銷,可以為Bar 對象實現(xiàn)一個?comparator:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | publicclass Foo { privatefloat _score; privateint _position; privateBar _bar; privatefinal BarComparator BAR_COMPARATOR = newBarComparator(); ?? publicint compareTo (Foo other) { returnComparisonChain.start(). compare(_score, other.getScore()). compare(_position, other.getPosition()). compare(_bar, other.getBar(), BAR_COMPARATOR). result(); } privatestatic class BarComparator implementsComparator<Bar> { @Override publicint compare(Bar a, Bar b) { returna.toString().compareTo(b.toString()); } } } |
4. 提前編譯正則表達式
字符串的操作在Java中算是開銷比較大的操作。還好Java提供了一些工具讓正則表達式盡可能地高效。動態(tài)的正則表達式在實踐中比較少見。在接下來要舉的例子中,每次調用String.replaceAll()?都包含了一個常量模式應用到輸入值中去。因此我們預先編譯這個模式可以節(jié)省CPU和內存的開銷。
- 優(yōu)化前:
| 1 2 3 | privateString transform(String term) { returnoutputTerm = term.replaceAll(_regex, _replacement); } |
- 優(yōu)化后:
| 1 2 3 4 | privatefinal Pattern _pattern = Pattern.compile(_regex); privateString transform(Stringterm) { StringoutputTerm = _pattern.matcher(term).replaceAll(_replacement); } |
5. 盡可能地緩存Cache it if you can
將結果保存在緩存里也是一個避免過多開銷的方法。但緩存只適用于在相同數(shù)據(jù)集撒花姑娘嗎的相同數(shù)據(jù)操作(比如對一些配置的預處理或者一些字符串處理)。現(xiàn)在已經有多種LRU(Least Recently Used )緩存算法實現(xiàn),但是Linkedin使用的是?Guava?cache (具體原因見這里) 大致代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | privatefinal int MAX_ENTRIES = 1000; privatefinal LoadingCache<String, String> _cache; // Initializing the cache _cache = CacheBuilder.newBuilder().maximumSize(MAX_ENTRIES).build(newCacheLoader<String,String>() { @Override publicString load(String key) throwsException { returnexpensiveOperationOn(key); } } ); ?? //Using the cache String output = _cache.getUnchecked(input); |
6. String的intern方法有用,但是也有危險
String 的 intern 特性有時候可以代替緩存來使用。
從這篇文檔,我們可以知道:
“A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned”.
這個特性跟緩存很類似,但有一個限制,你不能設置最多可容納的元素數(shù)目。因此,如果這些intern的字符串沒有限制(比如字符串代表著一些唯一的id),那么它會讓內存占用飛速增長。Linkedin曾經在這上面栽過跟頭——當時是對一些鍵值使用intern方法,線下模擬的時候一切正常,但一旦部署上線,系統(tǒng)的內存占用一下就升上去了(因為大量唯一的字符串被intern了)。所以最后Linkedin選擇使用 LRU 緩存,這樣可以限制最大元素數(shù)目。
最終結果
SPR的內存占用減少了75%,進而將feed-mixer的內存占用減少了 50% (如下圖所示)。這些優(yōu)化減少了對象的生成,進而減少了GC得頻率,整個服務的延遲就減少了25%。
from:?http://www.importnew.com/14588.html
總結
以上是生活随笔為你收集整理的Linkedin工程师是如何优化他们的Java代码的的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 8的6个问题
- 下一篇: Java并发编程(1):可重入内置锁