新聞中心
最近在刷各大公司的技術(shù)博客的時(shí)候,我在Linkedin的技術(shù)博客上面發(fā)現(xiàn)了一篇很不錯(cuò)博文。這篇博文介紹了Linkedin信息流中間層Feed Mixer,它為L(zhǎng)inkedin的Web主頁(yè),大學(xué)主頁(yè),公司主頁(yè)以及客戶端等多個(gè)分發(fā)渠道提供支撐(如下圖所示)。

創(chuàng)新互聯(lián)公司是一家專注于成都網(wǎng)站建設(shè)、成都網(wǎng)站制作與策劃設(shè)計(jì),左權(quán)網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)公司做網(wǎng)站,專注于網(wǎng)站建設(shè)十載,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:左權(quán)等地區(qū)。左權(quán)做網(wǎng)站價(jià)格咨詢:028-86922220
在Feed Mixer里面用到了一個(gè)叫做SPR(念“super”)的庫(kù)。博文講的就是如何優(yōu)化SPR的java代碼。下面就是他們總結(jié)的優(yōu)化經(jīng)驗(yàn)。
1. 謹(jǐn)慎對(duì)待Java的循環(huán)遍歷
Java中的列表遍歷可比它看起來(lái)要麻煩多了。就以下面兩段代碼為例:
A
- private final List
_bars; - for(Bar bar : _bars) {
- //Do important stuff
- }
B
- private final List
_bars; - for(int i = 0; i < _bars.size(); i++) {
- Bar bar = _bars.get(i);
- //Do important stuff
- }
代碼A執(zhí)行的時(shí)候 會(huì)為這個(gè)抽象列表創(chuàng)建一個(gè)迭代器,而代碼B就直接使用 get(i) 來(lái)獲取元素,相對(duì)于代碼A省去了迭代器的開銷。
實(shí)際上這里還是需要一些權(quán)衡的。代碼A使用了迭代器,保證了在獲取元素的時(shí)候的時(shí)間復(fù)雜度是 O(1)(使用了 getNext() 和 hasNext() 方法),最終的時(shí)間復(fù)雜度為 O(n) 。但是對(duì)于代碼B,循環(huán)里每次在調(diào)用 _bars.get(i) 的時(shí)候花費(fèi)的時(shí)間復(fù)雜度為 O(n) (假設(shè)這個(gè)list為一個(gè) LinkedList),那么最終代碼B整個(gè)循環(huán)的時(shí)間復(fù)雜度就是 O(n^2) (但如果代碼B里面的list是 ArrayList, 那 get(i) 方法的時(shí)間復(fù)雜度就是 O(1)了)。所以在決定使用哪一種遍歷的方式的時(shí)候,我們需要考慮列表的底層實(shí)現(xiàn),列表的平均長(zhǎng)度以及所使用的內(nèi)存。最后因?yàn)槲覀冃枰獌?yōu)化內(nèi)存,再加上 ArrayList 在大多數(shù)情況下查找的時(shí)間復(fù)雜度為 O(1) ,最后決定選擇代碼B所使用的方法。
2.在初始化的時(shí)候預(yù)估集合的大小
從Java的這篇 文檔我們可以了解到: “一個(gè)HashMap 實(shí)例有兩個(gè)影響它性能的因素:初始大小和加載因子(load factor)。 […] 當(dāng)哈希表的大小達(dá)到初始大小和加載因子的乘積的時(shí)候,哈希表會(huì)進(jìn)行 rehash操作 […] 如果在一個(gè)HashMap 實(shí)例里面要存儲(chǔ)多個(gè)映射關(guān)系時(shí),我們需要設(shè)置足夠大的初始化大小以便更有效地存儲(chǔ)映射關(guān)系而不是讓哈希表自動(dòng)增長(zhǎng)讓后rehash,造成性能瓶頸?!?/p>
在Linkedin實(shí)踐的時(shí)候,常常碰到需要遍歷一個(gè) ArrayList 并將這些元素保存到 HashMap 里面去。將這個(gè) HashMap 初始化預(yù)期的大小可以避免再次哈希所帶來(lái)的開銷。初始化大小可以設(shè)置為輸入的數(shù)組大小除以默認(rèn)加載因子的結(jié)果值(這里取0.7):
優(yōu)化前的代碼:
- HashMap
_map; - void addObjects(List
input) - {
- _map = new HashMap
(); - for(Foo f: input)
- {
- _map.put(f.getId(), f);
- }
- }
優(yōu)化后的代碼
- HashMap
_map; - void addObjects(List
input) - {
- _map = new HashMap
((int)Math.ceil(input.size() / 0.7)); - for(Foo f: input)
- {
- _map.put(f.getId(), f);
- }
- }
3. 延遲表達(dá)式的計(jì)算
在Java中,所有的方法參數(shù)會(huì)在方法調(diào)用之前,只要有方法參數(shù)是一個(gè)表達(dá)式的都會(huì)先這個(gè)表達(dá)式進(jìn)行計(jì)算(從左到右)。這個(gè)規(guī)則會(huì)導(dǎo)致一些不必要的操作。考慮到下面一個(gè)場(chǎng)景:使用ComparisonChain比較兩個(gè) Foo 對(duì)象。使用這樣的比較鏈條的一個(gè)好處就是在比較的過(guò)程中只要一個(gè) compareTo 方法返回了一個(gè)非零值整個(gè)比較就結(jié)束了,避免了許多無(wú)謂的比較。例如現(xiàn)在這個(gè)場(chǎng)景中的要比較的對(duì)象最先考慮他們的score, 然后是 position, 最后就是 _bar 這個(gè)屬性了:
- public class Foo {
- private float _score;
- private int _position;
- private Bar _bar;
- public int compareTo (Foo other) {
- return ComparisonChain.start().
- compare(_score, other.getScore()).
- compare(_position, other.getPosition()).
- compare(_bar.toString(), other.getBar().toString()).
- result;
- }
- }
但是上面這種實(shí)現(xiàn)方式總是會(huì)先生成兩個(gè) String 對(duì)象來(lái)保存 bar.toString()和other.getBar().toString() 的值,即使這兩個(gè)字符串的比較可能不需要。避免這樣的開銷,可以為Bar 對(duì)象實(shí)現(xiàn)一個(gè) comparator:
- public class Foo {
- private float _score;
- private int _position;
- private Bar _bar;
- private final BarComparator BAR_COMPARATOR = new BarComparator();
- public int compareTo (Foo other) {
- return ComparisonChain.start().
- compare(_score, other.getScore()).
- compare(_position, other.getPosition()).
- compare(_bar, other.getBar(), BAR_COMPARATOR).
- result();
- }
- private static class BarComparator implements Comparator
{ - @Override
- public int compare(Bar a, Bar b) {
- return a.toString().compareTo(b.toString());
- }
- }
- }
4. 提前編譯正則表達(dá)式
字符串的操作在Java中算是開銷比較大的操作。還好Java提供了一些工具讓正則表達(dá)式盡可能地高效。動(dòng)態(tài)的正則表達(dá)式在實(shí)踐中比較少見。在接下來(lái)要舉的例子中,每次調(diào)用 String.replaceAll() 都包含了一個(gè)常量模式應(yīng)用到輸入值中去。因此我們預(yù)先編譯這個(gè)模式可以節(jié)省CPU和內(nèi)存的開銷。
優(yōu)化前:
- private String transform(String term) {
- return outputTerm = term.replaceAll(_regex, _replacement);
- }
優(yōu)化后:
- private final Pattern _pattern = Pattern.compile(_regex);
- private String transform(String term) {
- String outputTerm = _pattern.matcher(term).replaceAll(_replacement);
- }
5. 盡可能地緩存Cache it if you can
將結(jié)果保存在緩存里也是一個(gè)避免過(guò)多開銷的方法。但緩存只適用于在相同數(shù)據(jù)集撒花姑娘嗎的相同數(shù)據(jù)操作(比如對(duì)一些配置的預(yù)處理或者一些字符串處理)。現(xiàn)在已經(jīng)有多種LRU(Least Recently Used )緩存算法實(shí)現(xiàn),但是Linkedin使用的是 Guava cache (具體原因見這里) 大致代碼如下:
- private final int MAX_ENTRIES = 1000;
- private final LoadingCache
_cache; - // Initializing the cache
- _cache = CacheBuilder.newBuilder().maximumSize(MAX_ENTRIES).build(new CacheLoader
() { - @Override
- public String load(String key) throws Exception {
- return expensiveOperationOn(key);
- }
- }
- );
- //Using the cache
- String output = _cache.getUnchecked(input);
6. String的intern方法有用,但是也有危險(xiǎn)
String 的 intern 特性有時(shí)候可以代替緩存來(lái)使用。
從這篇文檔,我們可以知道:
“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”.
這個(gè)特性跟緩存很類似,但有一個(gè)限制,你不能設(shè)置最多可容納的元素?cái)?shù)目。因此,如果這些intern的字符串沒(méi)有限制(比如字符串代表著一些唯一的 id),那么它會(huì)讓內(nèi)存占用飛速增長(zhǎng)。Linkedin曾經(jīng)在這上面栽過(guò)跟頭——當(dāng)時(shí)是對(duì)一些鍵值使用intern方法,線下模擬的時(shí)候一切正常,但一旦 部署上線,系統(tǒng)的內(nèi)存占用一下就升上去了(因?yàn)榇罅课ㄒ坏淖址籭ntern了)。所以最后Linkedin選擇使用 LRU 緩存,這樣可以限制最大元素?cái)?shù)目。
最終結(jié)果
SPR的內(nèi)存占用減少了75%,進(jìn)而將feed-mixer的內(nèi)存占用減少了 50% (如下圖所示)。這些優(yōu)化減少了對(duì)象的生成,進(jìn)而減少了GC得頻率,整個(gè)服務(wù)的延遲就減少了25%。
本文由 greenrobot 翻譯自Linkedin
網(wǎng)頁(yè)名稱:Linkedin 工程師如何優(yōu)化他們的 Java 代碼
本文鏈接:http://fisionsoft.com.cn/article/cdoijeo.html


咨詢
建站咨詢
