<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    from:http://blog.csdn.net/tanxiang21/article/details/16859781
    1.語法糖 數(shù)字下劃線

     1  2  3  4  5  6  7  8  9
    package com.java7developer.chapter1;
    import java.util.Collection;
    import java.util.HashMap;
    public class Coin {
    int test = 123_567;
    long test1 = 100_000L;
    }
     來自CODE的代碼片
    Coin.java

    2.switch語句中的String

     1  2  3  4
    public void printDay(String dayOfWeek){
    case "Sunday":System.out.println("ddd");break;
    default:System.out.println("sss");break;
    }
     來自CODE的代碼片
    snippet_file_0.txt

    3.multicatch

      1   2   3   4   5   6   7   8   9  10  11  12  13  14
    public Configuration getConfig(String fileName) {
    Configuration cfg = null;
    try {
    String fileText = getFile(fileName);
    cfg = verifyConfig(parseConfig(fileText));
    } catch (FileNotFoundException | ParseException | ConfigurationException e) {
    System.err.println("Config file '" + fileName
    + "' is missing or malformed");
    } catch (IOException iox) {
    System.err.println("Error while processing file '" + fileName + "'");
    }
    return cfg;
    }
     來自CODE的代碼片
    snippet_file_0.txt

    4.final重拋

    對比上份代碼
     1  2  3  4  5  6
    try {
    String fileText = getFile(fileName);
    cfg = verifyConfig(parseConfig(fileText));
    } catch (final Exception e) {
    throw e;
    }
     來自CODE的代碼片
    snippet_file_0.txt

    5.try-with-resources(TWR) AutoCloseable

      1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36
    package com.java7developer.chapter1;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.MalformedURLException;
    import java.net.URL;
    public class Java7ResourcesExample {
    private void run() throws IOException {
    File file = new File("foo");
    URL url = null;
    try {
    url = new URL("http://www.google.com/");
    } catch (MalformedURLException e) {
    }
    try (OutputStream out = new FileOutputStream(file);
    InputStream is = url.openStream()) {
    byte[] buf = new byte[4096];
    int len;
    while ((len = is.read(buf)) > 0) {
    out.write(buf, 0, len);
    }
    }
    }
    public static void main(String[] args) throws IOException {
    Java7ResourcesExample instance = new Java7ResourcesExample();
    instance.run();
    }
    }
     來自CODE的代碼片
    Java7ResourcesExample.java

    6.鉆石語法

     1
    HashMap<String, String> a = new HashMap<>();
     來自CODE的代碼片
    Java7-新特性-鉆石語法

    7.變參 消失的警告 @SafeVarargs

      1   2   3   4   5   6   7   8   9  10  11  12  13  14  15
    public class Coin {
    int test = 123_567;
    long test1 = 100_000L;
    @SafeVarargs
    public static <T> Collection<T> doSomething(T... entries){
    return null;
    }
    public static void main(String[] args) {
    HashMap<String, String> a = new HashMap<>();
    HashMap<String, String> b = new HashMap<>();
    doSomething(a,b);
    }
    }
    posted @ 2015-03-06 16:35 小馬歌 閱讀(240) | 評論 (0)編輯 收藏
     

    from:http://www.lifebackup.cn/timsort-java7.html

    2012年09月25日 21:57:48

    1. 為什么寫這篇文章

    這篇文章的根源是在產(chǎn)品中發(fā)現(xiàn)了一個詭異的bug:只能在產(chǎn)品環(huán)境下重現(xiàn),在我的本地開發(fā)環(huán)境無法重現(xiàn),而雙方的代碼沒有任何區(qū)別。最后用remote debug的方法找到異常所在:

    Exception in thread "main" java.lang.IllegalArgumentException: Comparison 
    method violates its general contract!

    Google了這個錯誤,是由于Java 7內(nèi)置的新排序算法導致的。這才猛然想起產(chǎn)品的編譯環(huán)境最近升級到了Java 7。

    2. 結(jié)論

    在Java 6中Arrays.sort()和Collections.sort()使用的是MergeSort,而在Java 7中,內(nèi)部實現(xiàn)換成了TimSort,其對對象間比較的實現(xiàn)要求更加嚴格:

    Comparator的實現(xiàn)必須保證以下幾點(出自這兒):

    a). sgn(compare(x, y)) == -sgn(compare(y, x)) 
    b). (compare(x, y)>0) && (compare(y, z)>0) 意味著 compare(x, z)>0 
    c). compare(x, y)==0 意味著對于任意的z:sgn(compare(x, z))==sgn(compare(y, z)) 均成立

    而我們的代碼中,某個compare()實現(xiàn)片段是這樣的:

    public int compare(ComparatorTest o1, ComparatorTest o2) { 
        return o1.getValue() > o2.getValue() ? 1 : -1; 
    }

    這就違背了a)原則:假設X的value為1,Y的value也為1;那么compare(X, Y) ≠ –compare(Y, X) 
    PS: TimSort不僅內(nèi)置在各種JDK 7的版本,也存在于Android SDK中(盡管其并沒有使用JDK 7)。

    3. 解決方案

    3.1) 更改內(nèi)部實現(xiàn):例如對于上個例子,就需要更改為

    public int compare(ComparatorTest o1, ComparatorTest o2) { 
        return o1.getValue() == o2.getValue() ? 0 :  
                    (o1.getValue() > o2.getValue() ? 1 : -1); 
    }

    3.2) Java 7預留了一個接口以便于用戶繼續(xù)使用Java 6的排序算法:在啟動參數(shù)中(例如eclipse.ini)添加-Djava.util.Arrays.useLegacyMergeSort=true

    3.3) 將這個IllegalArgumentException手動捕獲住(不推薦)

    4. TimSort在Java 7中的實現(xiàn)

    那么為什么Java 7會將TimSort作為排序的默認實現(xiàn),甚至在某種程度上犧牲它的兼容性(在stackoverflow上有大量的問題是關于這個新異常的)呢?接下來我們不妨來看一看它的實現(xiàn)。

    首先建議大家先讀一下這篇文章以簡要理解TimSort的思想。

    4.1) 如果傳入的Comparator為空,則使用ComparableTimSort的sort實現(xiàn)。

     image

    4.2) 傳入的待排序數(shù)組若小于MIN_MERGE(Java實現(xiàn)中為32,Python實現(xiàn)中為64),則

    a) 從數(shù)組開始處找到一組連接升序或嚴格降序(找到后翻轉(zhuǎn))的數(shù) 
    b) Binary Sort:使用二分查找的方法將后續(xù)的數(shù)插入之前的已排序數(shù)組

    image

    4.3) 開始真正的TimSort過程:

    4.3.1) 選取minRun大小,之后待排序數(shù)組將被分成以minRun大小為區(qū)塊的一塊塊子數(shù)組

    a) 如果數(shù)組大小為2的N次冪,則返回16(MIN_MERGE / 2) 
    b) 其他情況下,逐位向右位移(即除以2),直到找到介于16和32間的一個數(shù)

    image

    4.3.2) 類似于4.2.a找到初始的一組升序數(shù)列 
    4.3.3) 若這組區(qū)塊大小小于minRun,則將后續(xù)的數(shù)補足(采用binary sort插入這個數(shù)組) 
    4.3.4) 為后續(xù)merge各區(qū)塊作準備:記錄當前已排序的各區(qū)塊的大小 
    4.3.5) 對當前的各區(qū)塊進行merge,merge會滿足以下原則(假設X,Y,Z為相鄰的三個區(qū)塊):

    a) 只對相鄰的區(qū)塊merge 
    b) 若當前區(qū)塊數(shù)僅為2,If X<=Y,將X和Y merge 
    b) 若當前區(qū)塊數(shù)>=3,If X<=Y+Z,將X和Y merge,直到同時滿足X>Y+Z和Y>Z

    image

    4.3.6) 重復4.3.2 ~ 4.3.5,直到將待排序數(shù)組排序完 
    4.3.7) Final Merge:如果此時還有區(qū)塊未merge,則合并它們

    image

    5. Demo

    這一節(jié)用一個具體的例子來演示整個算法的演進過程:

    *注意*:為了演示方便,我將TimSort中的minRun直接設置為2,否則我不能用很小的數(shù)組演示。。。同時把MIN_MERGE也改成2(默認為32),這樣避免直接進入binary sort。

    初始數(shù)組為[7,5,1,2,6,8,10,12,4,3,9,11,13,15,16,14] 
    => 尋找連續(xù)的降序或升序序列 (4.3.2) 
    [1,5,7] [2,6,8,10,12,4,3,9,11,13,15,16,14] 
    => 入棧 (4.3.4) 
    當前的棧區(qū)塊為[3] 
    => 進入merge循環(huán) (4.3.5) 
    do not merge因為棧大小僅為1 
    => 尋找連續(xù)的降序或升序序列 (4.3.2) 
    [1,5,7] [2,6,8,10,12] [4,3,9,11,13,15,16,14] 
    => 入棧 (4.3.4) 
    當前的棧區(qū)塊為[3, 5] 
    => 進入merge循環(huán) (4.3.5) 
    merge因為runLen[0]<=runLen[1] 
    1) gallopRight:尋找run1的第一個元素應當插入run0中哪個位置(”2”應當插入”1”之后),然后就可以忽略之前run0的元素(都比run1的第一個元素小) 
    2) gallopLeft:尋找run0的最后一個元素應當插入run1中哪個位置(”7”應當插入”8”之前),然后就可以忽略之后run1的元素(都比run0的最后一個元素大) 
    這樣需要排序的元素就僅剩下[5,7] [2,6],然后進行mergeLow 
    完成之后的結(jié)果: 
    [1,2,5,6,7,8,10,12] [4,3,9,11,13,15,16,14] 
    => 入棧 (4.3.4) 
    當前的棧區(qū)塊為[8] 
    退出當前merge循環(huán)因為棧中的區(qū)塊僅為1 
    => 尋找連續(xù)的降序或升序序列 (4.3.2) 
    [1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16,14] 
    => 入棧 (4.3.4) 
    當前的棧區(qū)塊大小為[8,2] 
    => 進入merge循環(huán) (4.3.5) 
    do not merge因為runLen[0]>runLen[1] 
    => 尋找連續(xù)的降序或升序序列 (4.3.2) 
    [1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16] [14] 
    => 入棧 (4.3.4) 
    當前的棧區(qū)塊為[8,2,5] 
    => 
    do not merege run1與run2因為不滿足runLen[0]<=runLen[1]+runLen[2] 
    merge run2與run3因為runLen[1]<=runLen[2] 
    1) gallopRight:發(fā)現(xiàn)run1和run2就已經(jīng)排好序 
    完成之后的結(jié)果: 
    [1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14] 
    => 入棧 (4.3.4) 
    當前入棧的區(qū)塊大小為[8,7] 
    退出merge循環(huán)因為runLen[0]>runLen[1] 
    => 尋找連續(xù)的降序或升序序列 (4.3.2) 
    最后只剩下[14]這個元素:[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14] 
    => 入棧 (4.3.4) 
    當前入棧的區(qū)塊大小為[8,7,1] 
    => 進入merge循環(huán) (4.3.5) 
    merge因為runLen[0]<=runLen[1]+runLen[2] 
    因為runLen[0]>runLen[2],所以將run1和run2先合并。(否則將run0和run1先合并) 
    1) gallopRight & 2) gallopLeft 
    這樣需要排序的元素剩下[13,15] [14],然后進行mergeHigh 
    完成之后的結(jié)果: 
    [1,2,5,6,7,8,10,12] [3,4,9,11,13,14,15,16] 當前入棧的區(qū)塊為[8,8] 
    => 
    繼續(xù)merge因為runLen[0]<=runLen[1] 
    1) gallopRight & 2) gallopLeft 
    需要排序的元素剩下[5,6,7,8,10,12] [3,4,9,11],然后進行mergeHigh 
    完成之后的結(jié)果: 
    [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] 當前入棧的區(qū)塊大小為[16] 
    => 
    不需要final merge因為當前棧大小為1 
    => 
    結(jié)束

    6. 如何重現(xiàn)文章開始提到的Exception

    這一節(jié)將剝離復雜的業(yè)務邏輯,用一個最簡單的例子(不修改TimSort.java內(nèi)置的各種參數(shù))重現(xiàn)文章開始提到的Exception。因為盡管google出來的結(jié)果中非常多的人提到了這個Exception及解決方案,但并沒有人給出一個可以重現(xiàn)的例子和測試數(shù)據(jù)。另一方面,我也想從其他角度來加深對這個問題的理解。

    構(gòu)造測試數(shù)據(jù)的過程是個反人類的過程:( 大家不要學我。。

    以下是能重現(xiàn)這個問題的代碼:

    public class ReproJava7Exception { 
        public static void main(String[] args) { 
            int[] sample = new int[] 
                  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 
                    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-2,1,0,-2,0,0,0,0}; 
            List<Integer> list = new ArrayList<Integer>(); 
            for (int i : sample) 
                list.add(i); 
            // use the native TimSort in JDK 7 
            Collections.sort(list, new Comparator<Integer>() { 
                @Override 
                public int compare(Integer o1, Integer o2) { 
                    // miss the o1 = o2 case on purpose 
                    return o1 > o2 ? 1 : -1; 
                } 
            }); 
        } 
    }

    7. Sample Code

    這篇文章的所有代碼可以到github:https://github.com/Huang-Wei/understanding-timsort-java7下載。

    8. References

    http://en.wikipedia.org/wiki/Timsort 
    http://www.geneffects.com/briarskin/theory/binary/index.html 
    http://docs.oracle.com/javase/6/docs/api/java/util/Comparator.html#compare%28T,%20T%29 
    http://www.oracle.com/technetwork/java/javase/compatibility-417013.html#source

    分類:01囈語 | 標簽:  |

    9條評論

    1. hute說道:

      太好了,找了一天.感謝.

    2. hute說道:

      重現(xiàn)bug的方法確實很逆天,作者花了很大力氣吧.

    3. superpippo說道:

      @hute 所以說這個過程是很反人類的。。。

    4. cjnetwork說道:

      你好,我測試了一下你提供的測試代碼,發(fā)現(xiàn)還是不能復現(xiàn)這個異常情況,能否幫忙一下呢。

      以下是我的jdk:
      java version “1.7.0-ea”
      Java(TM) SE Runtime Environment (build 1.7.0-ea-b45)
      Java HotSpot(TM) Client VM (build 14.0-b10, mixed mode, sharing)

    5. superpippo說道:

      @cjnetwork 我在jdk 1.7.0_17上用ReproJava7Exception.java能重現(xiàn)這個問題
      我猜測你不能重現(xiàn)有兩種可能:
      1) 你的這個jdk版本有點詭異(1.7.0-ea),嘗試升到最1.7的正式版本
      2) 確認你在編譯時候使用的是JDK7,而不是JDK6 – 因為有可能在Eclipse或別的IDE中并沒有設置正確。你可以寫一段switch string的小例子看看有沒有編譯錯誤

    6. ghsau說道:

      compare實現(xiàn)只需要這樣:
      return o1.getValue() – o2.getValue();
      不需要自己判斷.

    posted @ 2015-02-26 18:46 小馬歌 閱讀(290) | 評論 (0)編輯 收藏
     
         摘要: Python2.3中開始使用的timsort應該說算是聲名在外了,不管是在穩(wěn)定性還是在速度上都十分的驚人。前一段剛剛看了《Python CookBook》中的一些章節(jié),對timsort產(chǎn)生了一些興趣。于是在網(wǎng)上看到了這邊文章,講的相當清楚明了,于是產(chǎn)生了翻譯的念頭,也于是有了這篇文章。這應該算是我翻譯的第一篇技術文章,真正做一次才明白能看懂和能翻譯出來還是有蠻大的差距的。翻譯質(zhì)量不可謂不差,諸位如...  閱讀全文
    posted @ 2015-02-26 18:30 小馬歌 閱讀(442) | 評論 (0)編輯 收藏
     
         摘要: 概要這個類在 Oracle 的官方文檔里是查不到的,但是確實在 OpenJDK 的源代碼里出現(xiàn)了,Arrays 中的 sort 函數(shù)用到了這個用于排序的類。它將歸并排序(merge sort) 與插入排序(insertion sort) 結(jié)合,并進行了一些優(yōu)化。對于已經(jīng)部分排序的數(shù)組,時間復雜度遠低于 O(n log(n)),最好可達&n...  閱讀全文
    posted @ 2015-02-26 15:59 小馬歌 閱讀(499) | 評論 (0)編輯 收藏
     
    from:http://www.infoq.com/cn/articles/netty-version-upgrade-history-thread-part

    1. 背景

    1.1. Netty 3.X系列版本現(xiàn)狀

    根據(jù)對Netty社區(qū)部分用戶的調(diào)查,結(jié)合Netty在其它開源項目中的使用情況,我們可以看出目前Netty商用的主流版本集中在3.X和4.X上,其中以Netty 3.X系列版本使用最為廣泛。

    Netty社區(qū)非常活躍,3.X系列版本從2011年2月7日發(fā)布的netty-3.2.4 Final版本到2014年12月17日發(fā)布的netty-3.10.0 Final版本,版本跨度達3年多,期間共推出了61個Final版本。

    1.2. 升級還是堅守老版本

    相比于其它開源項目,Netty用戶的版本升級之路更加艱辛,最根本的原因就是Netty 4對Netty 3沒有做到很好的前向兼容。

    由于版本不兼容,大多數(shù)老版本使用者的想法就是既然升級這么麻煩,我暫時又不需要使用到Netty 4的新特性,當前版本還挺穩(wěn)定,就暫時先不升級,以后看看再說。

    堅守老版本還有很多其它的理由,例如考慮到線上系統(tǒng)的穩(wěn)定性、對新版本的熟悉程度等。無論如何升級Netty都是一件大事,特別是對Netty有直接強依賴的產(chǎn)品。

    從上面的分析可以看出,堅守老版本似乎是個不錯的選擇;但是,“理想是美好的,現(xiàn)實卻是殘酷的”,堅守老版本并非總是那么容易,下面我們就看下被迫升級的案例。

    1.3. “被迫”升級到Netty 4.X

    除了為了使用新特性而主動進行的版本升級,大多數(shù)升級都是“被迫的”。下面我們對這些升級原因進行分析。

    1. 公司的開源軟件管理策略:對于那些大廠,不同部門和產(chǎn)品線依賴的開源軟件版本經(jīng)常不同,為了對開源依賴進行統(tǒng)一管理,降低安全、維護和管理成本,往往會指定優(yōu)選的軟件版本。由于Netty 4.X 系列版本已經(jīng)非常成熟,因為,很多公司都優(yōu)選Netty 4.X版本。
    2. 維護成本:無論是依賴Netty 3.X,還是Netty4.X,往往需要在原框架之上做定制。例如,客戶端的短連重連、心跳檢測、流控等。分別對Netty 4.X和3.X版本實現(xiàn)兩套定制框架,開發(fā)和維護成本都非常高。根據(jù)開源軟件的使用策略,當存在版本沖突的時候,往往會選擇升級到更高的版本。對于Netty,依然遵循這個規(guī)則。
    3. 新特性:Netty 4.X相比于Netty 3.X,提供了很多新的特性,例如優(yōu)化的內(nèi)存管理池、對MQTT協(xié)議的支持等。如果用戶需要使用這些新特性,最簡便的做法就是升級Netty到4.X系列版本。
    4. 更優(yōu)異的性能:Netty 4.X版本相比于3.X老版本,優(yōu)化了內(nèi)存池,減少了GC的頻率、降低了內(nèi)存消耗;通過優(yōu)化Rector線程池模型,用戶的開發(fā)更加簡單,線程調(diào)度也更加高效。

    1.4. 升級不當付出的代價

    表面上看,類庫包路徑的修改、API的重構(gòu)等似乎是升級的重頭戲,大家往往把注意力放到這些“明槍”上,但真正隱藏和致命的卻是“暗箭”。如果對Netty底層的事件調(diào)度機制和線程模型不熟悉,往往就會“中槍”。

    本文以幾個比較典型的真實案例為例,通過問題描述、問題定位和問題總結(jié),讓這些隱藏的“暗箭”不再傷人。

    由于Netty 4線程模型改變導致的升級事故還有很多,限于篇幅,本文不一一枚舉,這些問題萬變不離其宗,只要抓住線程模型這個關鍵點,所謂的疑難雜癥都將迎刃而解。

    2. Netty升級之后遭遇內(nèi)存泄露

    2.1. 問題描述

    隨著JVM虛擬機和JIT即時編譯技術的發(fā)展,對象的分配和回收是個非常輕量級的工作。但是對于緩沖區(qū)Buffer,情況卻稍有不同,特別是對于堆外直接內(nèi)存的分配和回收,是一件耗時的操作。為了盡量重用緩沖區(qū),Netty4.X提供了基于內(nèi)存池的緩沖區(qū)重用機制。性能測試表明,采用內(nèi)存池的ByteBuf相比于朝生夕滅的ByteBuf,性能高23倍左右(性能數(shù)據(jù)與使用場景強相關)。

    業(yè)務應用的特點是高并發(fā)、短流程,大多數(shù)對象都是朝生夕滅的短生命周期對象。為了減少內(nèi)存的拷貝,用戶期望在序列化的時候直接將對象編碼到PooledByteBuf里,這樣就不需要為每個業(yè)務消息都重新申請和釋放內(nèi)存。

    業(yè)務的相關代碼示例如下:

    //在業(yè)務線程中初始化內(nèi)存池分配器,分配非堆內(nèi)存  ByteBufAllocator allocator = new PooledByteBufAllocator(true);  ByteBuf buffer = allocator.ioBuffer(1024); //構(gòu)造訂購請求消息并賦值,業(yè)務邏輯省略 SubInfoReq infoReq = new SubInfoReq (); infoReq.setXXX(......); //將對象編碼到ByteBuf中 codec.encode(buffer, info); //調(diào)用ChannelHandlerContext進行消息發(fā)送 ctx.writeAndFlush(buffer);

    業(yè)務代碼升級Netty版本并重構(gòu)之后,運行一段時間,Java進程就會宕機,查看系統(tǒng)運行日志發(fā)現(xiàn)系統(tǒng)發(fā)生了內(nèi)存泄露(示例堆棧):

    圖2-1 OOM內(nèi)存溢出堆棧

    對內(nèi)存進行監(jiān)控(切換使用堆內(nèi)存池,方便對內(nèi)存進行監(jiān)控),發(fā)現(xiàn)堆內(nèi)存一直飆升,如下所示(示例堆內(nèi)存監(jiān)控):

    圖2-2 堆內(nèi)存監(jiān)控

    2.2. 問題定位

    使用jmap -dump:format=b,file=netty.bin PID 將堆內(nèi)存dump出來,通過IBM的HeapAnalyzer工具進行分析,發(fā)現(xiàn)ByteBuf發(fā)生了泄露。

    因為使用了內(nèi)存池,所以首先懷疑是不是申請的ByteBuf沒有被釋放導致?查看代碼,發(fā)現(xiàn)消息發(fā)送完成之后,Netty底層已經(jīng)調(diào)用ReferenceCountUtil.release(message)對內(nèi)存進行了釋放。這是怎么回事呢?難道Netty 4.X的內(nèi)存池有Bug,調(diào)用release操作釋放內(nèi)存失敗?

    考慮到Netty 內(nèi)存池自身Bug的可能性不大,首先從業(yè)務的使用方式入手分析:

    1. 內(nèi)存的分配是在業(yè)務代碼中進行,由于使用到了業(yè)務線程池做I/O操作和業(yè)務操作的隔離,實際上內(nèi)存是在業(yè)務線程中分配的;
    2. 內(nèi)存的釋放操作是在outbound中進行,按照Netty 3的線程模型,downstream(對應Netty 4的outbound,Netty 4取消了upstream和downstream)的handler也是由業(yè)務調(diào)用者線程執(zhí)行的,也就是說釋放跟分配在同一個業(yè)務線程中進行。

    初次排查并沒有發(fā)現(xiàn)導致內(nèi)存泄露的根因,一籌莫展之際開始查看Netty的內(nèi)存池分配器PooledByteBufAllocator的Doc和源碼實現(xiàn),發(fā)現(xiàn)內(nèi)存池實際是基于線程上下文實現(xiàn)的,相關代碼如下:

    final ThreadLocal<PoolThreadCache> threadCache = new ThreadLocal<PoolThreadCache>() {         private final AtomicInteger index = new AtomicInteger();         @Override         protected PoolThreadCache initialValue() {             final int idx = index.getAndIncrement();             final PoolArena<byte[]> heapArena;             final PoolArena<ByteBuffer> directArena;             if (heapArenas != null) {                 heapArena = heapArenas[Math.abs(idx % heapArenas.length)];             } else {                 heapArena = null;             }             if (directArenas != null) {                 directArena = directArenas[Math.abs(idx % directArenas.length)];             } else {                 directArena = null;             }             return new PoolThreadCache(heapArena, directArena);         }

    也就是說內(nèi)存的申請和釋放必須在同一線程上下文中,不能跨線程。跨線程之后實際操作的就不是同一塊內(nèi)存區(qū)域,這會導致很多嚴重的問題,內(nèi)存泄露便是其中之一。內(nèi)存在A線程申請,切換到B線程釋放,實際是無法正確回收的。

    通過對Netty內(nèi)存池的源碼分析,問題基本鎖定。保險起見進行簡單驗證,通過對單條業(yè)務消息進行Debug,發(fā)現(xiàn)執(zhí)行釋放的果然不是業(yè)務線程,而是Netty的NioEventLoop線程:當某個消息被完全發(fā)送成功之后,會通過ReferenceCountUtil.release(message)方法釋放已經(jīng)發(fā)送成功的ByteBuf。

    問題定位出來之后,繼續(xù)溯源,發(fā)現(xiàn)Netty 4修改了Netty 3的線程模型:在Netty 3的時候,upstream是在I/O線程里執(zhí)行的,而downstream是在業(yè)務線程里執(zhí)行。當Netty從網(wǎng)絡讀取一個數(shù)據(jù)報投遞給業(yè)務handler的時候,handler是在I/O線程里執(zhí)行;而當我們在業(yè)務線程中調(diào)用write和writeAndFlush向網(wǎng)絡發(fā)送消息的時候,handler是在業(yè)務線程里執(zhí)行,直到最后一個Header handler將消息寫入到發(fā)送隊列中,業(yè)務線程才返回。

    Netty4修改了這一模型,在Netty 4里inbound(對應Netty 3的upstream)和outbound(對應Netty 3的downstream)都是在NioEventLoop(I/O線程)中執(zhí)行。當我們在業(yè)務線程里通過ChannelHandlerContext.write發(fā)送消息的時候,Netty 4在將消息發(fā)送事件調(diào)度到ChannelPipeline的時候,首先將待發(fā)送的消息封裝成一個Task,然后放到NioEventLoop的任務隊列中,由NioEventLoop線程異步執(zhí)行。后續(xù)所有handler的調(diào)度和執(zhí)行,包括消息的發(fā)送、I/O事件的通知,都由NioEventLoop線程負責處理。

    下面我們分別通過對比Netty 3和Netty 4的消息接收和發(fā)送流程,來理解兩個版本線程模型的差異:

    Netty 3的I/O事件處理流程:

    圖2-3 Netty 3 I/O事件處理線程模型

    Netty 4的I/O消息處理流程:

    圖2-4 Netty 4 I/O事件處理線程模型

    2.3. 問題總結(jié)

    Netty 4.X版本新增的內(nèi)存池確實非常高效,但是如果使用不當則會導致各種嚴重的問題。諸如內(nèi)存泄露這類問題,功能測試并沒有異常,如果相關接口沒有進行壓測或者穩(wěn)定性測試而直接上線,則會導致嚴重的線上問題。

    內(nèi)存池PooledByteBuf的使用建議:

    1. 申請之后一定要記得釋放,Netty自身Socket讀取和發(fā)送的ByteBuf系統(tǒng)會自動釋放,用戶不需要做二次釋放;如果用戶使用Netty的內(nèi)存池在應用中做ByteBuf的對象池使用,則需要自己主動釋放;
    2. 避免錯誤的釋放:跨線程釋放、重復釋放等都是非法操作,要避免。特別是跨線程申請和釋放,往往具有隱蔽性,問題定位難度較大;
    3. 防止隱式的申請和分配:之前曾經(jīng)發(fā)生過一個案例,為了解決內(nèi)存池跨線程申請和釋放問題,有用戶對內(nèi)存池做了二次包裝,以實現(xiàn)多線程操作時,內(nèi)存始終由包裝的管理線程申請和釋放,這樣可以屏蔽用戶業(yè)務線程模型和訪問方式的差異。誰知運行一段時間之后再次發(fā)生了內(nèi)存泄露,最后發(fā)現(xiàn)原來調(diào)用ByteBuf的write操作時,如果內(nèi)存容量不足,會自動進行容量擴展。擴展操作由業(yè)務線程執(zhí)行,這就繞過了內(nèi)存池管理線程,發(fā)生了“引用逃逸”。該Bug只有在ByteBuf容量動態(tài)擴展的時候才發(fā)生,因此,上線很長一段時間沒有發(fā)生,直到某一天......因此,大家在使用Netty 4.X的內(nèi)存池時要格外當心,特別是做二次封裝時,一定要對內(nèi)存池的實現(xiàn)細節(jié)有深刻的理解。

    3. Netty升級之后遭遇數(shù)據(jù)被篡改

    3.1. 問題描述

    某業(yè)務產(chǎn)品,Netty3.X升級到4.X之后,系統(tǒng)運行過程中,偶現(xiàn)服務端發(fā)送給客戶端的應答數(shù)據(jù)被莫名“篡改”。

    業(yè)務服務端的處理流程如下:

    1. 將解碼后的業(yè)務消息封裝成Task,投遞到后端的業(yè)務線程池中執(zhí)行;
    2. 業(yè)務線程處理業(yè)務邏輯,完成之后構(gòu)造應答消息發(fā)送給客戶端;
    3. 業(yè)務應答消息的編碼通過繼承Netty的CodeC框架實現(xiàn),即Encoder ChannelHandler;
    4. 調(diào)用Netty的消息發(fā)送接口之后,流程繼續(xù),根據(jù)業(yè)務場景,可能會繼續(xù)操作原發(fā)送的業(yè)務對象。

    業(yè)務相關代碼示例如下:

    //構(gòu)造訂購應答消息 SubInfoResp infoResp = new SubInfoResp(); //根據(jù)業(yè)務邏輯,對應答消息賦值 infoResp.setResultCode(0); infoResp.setXXX(); 后續(xù)賦值操作省略...... //調(diào)用ChannelHandlerContext進行消息發(fā)送 ctx.writeAndFlush(infoResp); //消息發(fā)送完成之后,后續(xù)根據(jù)業(yè)務流程進行分支處理,修改infoResp對象 infoResp.setXXX(); 后續(xù)代碼省略......

    3.2. 問題定位

    首先對應答消息被非法“篡改”的原因進行分析,經(jīng)過定位發(fā)現(xiàn)當發(fā)生問題時,被“篡改”的內(nèi)容是調(diào)用writeAndFlush接口之后,由后續(xù)業(yè)務分支代碼修改應答消息導致的。由于修改操作發(fā)生在writeAndFlush操作之后,按照Netty 3.X的線程模型不應該出現(xiàn)該問題。

    在Netty3中,downstream是在業(yè)務線程里執(zhí)行的,也就是說對SubInfoResp的編碼操作是在業(yè)務線程中執(zhí)行的,當編碼后的ByteBuf對象被投遞到消息發(fā)送隊列之后,業(yè)務線程才會返回并繼續(xù)執(zhí)行后續(xù)的業(yè)務邏輯,此時修改應答消息是不會改變已完成編碼的ByteBuf對象的,所以肯定不會出現(xiàn)應答消息被篡改的問題。

    初步分析應該是由于線程模型發(fā)生變更導致的問題,隨后查驗了Netty 4的線程模型,果然發(fā)生了變化:當調(diào)用outbound向外發(fā)送消息的時候,Netty會將發(fā)送事件封裝成Task,投遞到NioEventLoop的任務隊列中異步執(zhí)行,相關代碼如下:

    @Override  public void invokeWrite(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {         if (msg == null) {             throw new NullPointerException("msg");         }         validatePromise(ctx, promise, true);         if (executor.inEventLoop()) {             invokeWriteNow(ctx, msg, promise);         } else {             AbstractChannel channel = (AbstractChannel) ctx.channel();             int size = channel.estimatorHandle().size(msg);             if (size > 0) {                 ChannelOutboundBuffer buffer = channel.unsafe().outboundBuffer();                 // Check for null as it may be set to null if the channel is closed already                 if (buffer != null) {                     buffer.incrementPendingOutboundBytes(size);                 }             }             safeExecuteOutbound(WriteTask.newInstance(ctx, msg, size, promise), promise, msg);         }     }

    通過上述代碼可以看出,Netty首先對當前的操作的線程進行判斷,如果操作本身就是由NioEventLoop線程執(zhí)行,則調(diào)用寫操作;否則,執(zhí)行線程安全的寫操作,即將寫事件封裝成Task,放入到任務隊列中由Netty的I/O線程執(zhí)行,業(yè)務調(diào)用返回,流程繼續(xù)執(zhí)行。

    通過源碼分析,問題根源已經(jīng)很清楚:系統(tǒng)升級到Netty 4之后,線程模型發(fā)生變化,響應消息的編碼由NioEventLoop線程異步執(zhí)行,業(yè)務線程返回。這時存在兩種可能:

    1. 如果編碼操作先于修改應答消息的業(yè)務邏輯執(zhí)行,則運行結(jié)果正確;
    2. 如果編碼操作在修改應答消息的業(yè)務邏輯之后執(zhí)行,則運行結(jié)果錯誤。

    由于線程的執(zhí)行先后順序無法預測,因此該問題隱藏的相當深。如果對Netty 4和Netty3的線程模型不了解,就會掉入陷阱。

    Netty 3版本業(yè)務邏輯沒有問題,流程如下:

    圖3-1 升級之前的業(yè)務流程線程模型

    升級到Netty 4版本之后,業(yè)務流程由于Netty線程模型的變更而發(fā)生改變,導致業(yè)務邏輯發(fā)生問題:

    圖3-2 升級之后的業(yè)務處理流程發(fā)生改變

    3.3. 問題總結(jié)

    很多讀者在進行Netty 版本升級的時候,只關注到了包路徑、類和API的變更,并沒有注意到隱藏在背后的“暗箭”- 線程模型變更。

    升級到Netty 4的用戶需要根據(jù)新的線程模型對已有的系統(tǒng)進行評估,重點需要關注outbound的ChannelHandler,如果它的正確性依賴于Netty 3的線程模型,則很可能在新的線程模型中出問題,可能是功能問題或者其它問題。

    4. Netty升級之后性能嚴重下降

    4.1. 問題描述

    相信很多Netty用戶都看過如下相關報告:

    在Twitter,Netty 4 GC開銷降為五分之一:Netty 3使用Java對象表示I/O事件,這樣簡單,但會產(chǎn)生大量的垃圾,尤其是在我們這樣的規(guī)模下。Netty 4在新版本中對此做出了更改,取代生存周期短的事件對象,而以定義在生存周期長的通道對象上的方法處理I/O事件。它還有一個使用池的專用緩沖區(qū)分配器。

    每當收到新信息或者用戶發(fā)送信息到遠程端,Netty 3均會創(chuàng)建一個新的堆緩沖區(qū)。這意味著,對應每一個新的緩沖區(qū),都會有一個‘new byte[capacity]’。這些緩沖區(qū)會導致GC壓力,并消耗內(nèi)存帶寬:為了安全起見,新的字節(jié)數(shù)組分配時會用零填充,這會消耗內(nèi)存帶寬。然而,用零填充的數(shù)組很可能會再次用實際的數(shù)據(jù)填充,這又會消耗同樣的內(nèi)存帶寬。如果Java虛擬機(JVM)提供了創(chuàng)建新字節(jié)數(shù)組而又無需用零填充的方式,那么我們本來就可以將內(nèi)存帶寬消耗減少50%,但是目前沒有那樣一種方式。

    在Netty 4中,代碼定義了粒度更細的API,用來處理不同的事件類型,而不是創(chuàng)建事件對象。它還實現(xiàn)了一個新緩沖池,那是一個純Java版本的 jemalloc (Facebook也在用)。現(xiàn)在,Netty不會再因為用零填充緩沖區(qū)而浪費內(nèi)存帶寬了。

    我們比較了兩個分別建立在Netty 3和4基礎上echo協(xié)議服務器。(Echo非常簡單,這樣,任何垃圾的產(chǎn)生都是Netty的原因,而不是協(xié)議的原因)。我使它們服務于相同的分布式echo協(xié)議客戶端,來自這些客戶端的16384個并發(fā)連接重復發(fā)送256字節(jié)的隨機負載,幾乎使千兆以太網(wǎng)飽和。

    根據(jù)測試結(jié)果,Netty 4:

    • GC中斷頻率是原來的1/5: 45.5 vs. 9.2次/分鐘
    • 垃圾生成速度是原來的1/5: 207.11 vs 41.81 MiB/秒

    正是看到了相關的Netty 4性能提升報告,很多用戶選擇了升級。事后一些用戶反饋Netty 4并沒有跟產(chǎn)品帶來預期的性能提升,有些甚至還發(fā)生了非常嚴重的性能下降,下面我們就以某業(yè)務產(chǎn)品的失敗升級經(jīng)歷為案例,詳細分析下導致性能下降的原因。

    4.2. 問題定位

    首先通過JMC等性能分析工具對性能熱點進行分析,示例如下(信息安全等原因,只給出分析過程示例截圖):

    圖4-1 JMC性能監(jiān)控分析

    通過對熱點方法的分析,發(fā)現(xiàn)在消息發(fā)送過程中,有兩處熱點:

    1. 消息發(fā)送性能統(tǒng)計相關Handler;
    2. 編碼Handler。

    對使用Netty 3版本的業(yè)務產(chǎn)品進行性能對比測試,發(fā)現(xiàn)上述兩個Handler也是熱點方法。既然都是熱點,為啥切換到Netty4之后性能下降這么厲害呢?

    通過方法的調(diào)用樹分析發(fā)現(xiàn)了兩個版本的差異:在Netty 3中,上述兩個熱點方法都是由業(yè)務線程負責執(zhí)行;而在Netty 4中,則是由NioEventLoop(I/O)線程執(zhí)行。對于某個鏈路,業(yè)務是擁有多個線程的線程池,而NioEventLoop只有一個,所以執(zhí)行效率更低,返回給客戶端的應答時延就大。時延增大之后,自然導致系統(tǒng)并發(fā)量降低,性能下降。

    找出問題根因之后,針對Netty 4的線程模型對業(yè)務進行專項優(yōu)化,性能達到預期,遠超過了Netty 3老版本的性能。

    Netty 3的業(yè)務線程調(diào)度模型圖如下所示:充分利用了業(yè)務多線程并行編碼和Handler處理的優(yōu)勢,周期T內(nèi)可以處理N條業(yè)務消息。

    圖4-2 Netty 3業(yè)務調(diào)度性能模型

    切換到Netty 4之后,業(yè)務耗時Handler被I/O線程串行執(zhí)行,因此性能發(fā)生比較大的下降:

    圖4-3 Netty 4業(yè)務調(diào)度性能模型

    4.3. 問題總結(jié)

    該問題的根因還是由于Netty 4的線程模型變更引起,線程模型變更之后,不僅影響業(yè)務的功能,甚至對性能也會造成很大的影響。

    對Netty的升級需要從功能、兼容性和性能等多個角度進行綜合考慮,切不可只盯著API變更這個芝麻,而丟掉了性能這個西瓜。API的變更會導致編譯錯誤,但是性能下降卻隱藏于無形之中,稍不留意就會中招。

    對于講究快速交付、敏捷開發(fā)和灰度發(fā)布的互聯(lián)網(wǎng)應用,升級的時候更應該要當心。

    5. Netty升級之后上下文丟失

    5.1. 問題描述

    為了提升業(yè)務的二次定制能力,降低對接口的侵入性,業(yè)務使用線程變量進行消息上下文的傳遞。例如消息發(fā)送源地址信息、消息Id、會話Id等。

    業(yè)務同時使用到了一些第三方開源容器,也提供了線程級變量上下文的能力。業(yè)務通過容器上下文獲取第三方容器的系統(tǒng)變量信息。

    升級到Netty 4之后,業(yè)務繼承自Netty的ChannelHandler發(fā)生了空指針異常,無論是業(yè)務自定義的線程上下文、還是第三方容器的線程上下文,都獲取不到傳遞的變量值。

    5.2. 問題定位

    首先檢查代碼,看業(yè)務是否傳遞了相關變量,確認業(yè)務傳遞之后懷疑跟Netty 版本升級相關,調(diào)試發(fā)現(xiàn),業(yè)務ChannelHandler獲取的線程上下文對象和之前業(yè)務傳遞的上下文不是同一個。這就說明執(zhí)行ChannelHandler的線程跟處理業(yè)務的線程不是同一個線程!

    查看Netty 4線程模型的相關Doc發(fā)現(xiàn),Netty修改了outbound的線程模型,正好影響了業(yè)務消息發(fā)送時的線程上下文傳遞,最終導致線程變量丟失。

    5.3. 問題總結(jié)

    通常業(yè)務的線程模型有如下幾種:

    1. 業(yè)務自定義線程池/線程組處理業(yè)務,例如使用JDK 1.5提供的ExecutorService;
    2. 使用J2EE Web容器自帶的線程模型,常見的如JBoss和Tomcat的HTTP接入線程等;
    3. 隱式的使用其它第三方框架的線程模型,例如使用NIO框架進行協(xié)議處理,業(yè)務代碼隱式使用的就是NIO框架的線程模型,除非業(yè)務明確的實現(xiàn)自定義線程模型。

    在實踐中我們發(fā)現(xiàn)很多業(yè)務使用了第三方框架,但是只熟悉API和功能,對線程模型并不清楚。某個類庫由哪個線程調(diào)用,糊里糊涂。為了方便變量傳遞,又隨意的使用線程變量,實際對背后第三方類庫的線程模型產(chǎn)生了強依賴。當容器或者第三方類庫升級之后,如果線程模型發(fā)生了變更,則原有功能就會發(fā)生問題。

    鑒于此,在實際工作中,盡量不要強依賴第三方類庫的線程模型,如果確實無法避免,則必須對它的線程模型有深入和清晰的了解。當?shù)谌筋悗焐壷螅枰獧z查線程模型是否發(fā)生變更,如果發(fā)生變化,相關的代碼也需要考慮同步升級。

    6. Netty3.X VS Netty4.X 之線程模型

    通過對三個具有典型性的升級失敗案例進行分析和總結(jié),我們發(fā)現(xiàn)有個共性:都是線程模型改變?nèi)堑牡?

    下面小節(jié)我們就詳細得對Netty3和Netty4版本的I/O線程模型進行對比,以方便大家掌握兩者的差異,在升級和使用中盡量少踩雷。

    6.1 Netty 3.X 版本線程模型

    Netty 3.X的I/O操作線程模型比較復雜,它的處理模型包括兩部分:

    1. Inbound:主要包括鏈路建立事件、鏈路激活事件、讀事件、I/O異常事件、鏈路關閉事件等;
    2. Outbound:主要包括寫事件、連接事件、監(jiān)聽綁定事件、刷新事件等。

    我們首先分析下Inbound操作的線程模型:

    圖6-1 Netty 3 Inbound操作線程模型

    從上圖可以看出,Inbound操作的主要處理流程如下:

    1. I/O線程(Work線程)將消息從TCP緩沖區(qū)讀取到SocketChannel的接收緩沖區(qū)中;
    2. 由I/O線程負責生成相應的事件,觸發(fā)事件向上執(zhí)行,調(diào)度到ChannelPipeline中;
    3. I/O線程調(diào)度執(zhí)行ChannelPipeline中Handler鏈的對應方法,直到業(yè)務實現(xiàn)的Last Handler;
    4. Last Handler將消息封裝成Runnable,放入到業(yè)務線程池中執(zhí)行,I/O線程返回,繼續(xù)讀/寫等I/O操作;
    5. 業(yè)務線程池從任務隊列中彈出消息,并發(fā)執(zhí)行業(yè)務邏輯。

    通過對Netty 3的Inbound操作進行分析我們可以看出,Inbound的Handler都是由Netty的I/O Work線程負責執(zhí)行。

    下面我們繼續(xù)分析Outbound操作的線程模型:

    圖6-2 Netty 3 Outbound操作線程模型

    從上圖可以看出,Outbound操作的主要處理流程如下:

    業(yè)務線程發(fā)起Channel Write操作,發(fā)送消息;

    1. Netty將寫操作封裝成寫事件,觸發(fā)事件向下傳播;
    2. 寫事件被調(diào)度到ChannelPipeline中,由業(yè)務線程按照Handler Chain串行調(diào)用支持Downstream事件的Channel Handler;
    3. 執(zhí)行到系統(tǒng)最后一個ChannelHandler,將編碼后的消息Push到發(fā)送隊列中,業(yè)務線程返回;
    4. Netty的I/O線程從發(fā)送消息隊列中取出消息,調(diào)用SocketChannel的write方法進行消息發(fā)送。

    6.2 Netty 4.X 版本線程模型

    相比于Netty 3.X系列版本,Netty 4.X的I/O操作線程模型比較簡答,它的原理圖如下所示:

    圖6-3 Netty 4 Inbound和Outbound操作線程模型

    從上圖可以看出,Outbound操作的主要處理流程如下:

    1. I/O線程NioEventLoop從SocketChannel中讀取數(shù)據(jù)報,將ByteBuf投遞到ChannelPipeline,觸發(fā)ChannelRead事件;
    2. I/O線程NioEventLoop調(diào)用ChannelHandler鏈,直到將消息投遞到業(yè)務線程,然后I/O線程返回,繼續(xù)后續(xù)的讀寫操作;
    3. 業(yè)務線程調(diào)用ChannelHandlerContext.write(Object msg)方法進行消息發(fā)送;
    4. 如果是由業(yè)務線程發(fā)起的寫操作,ChannelHandlerInvoker將發(fā)送消息封裝成Task,放入到I/O線程NioEventLoop的任務隊列中,由NioEventLoop在循環(huán)中統(tǒng)一調(diào)度和執(zhí)行。放入任務隊列之后,業(yè)務線程返回;
    5. I/O線程NioEventLoop調(diào)用ChannelHandler鏈,進行消息發(fā)送,處理Outbound事件,直到將消息放入發(fā)送隊列,然后喚醒Selector,進而執(zhí)行寫操作。

    通過流程分析,我們發(fā)現(xiàn)Netty 4修改了線程模型,無論是Inbound還是Outbound操作,統(tǒng)一由I/O線程NioEventLoop調(diào)度執(zhí)行。

    6.3. 線程模型對比

    在進行新老版本線程模型PK之前,首先還是要熟悉下串行化設計的理念:

    我們知道當系統(tǒng)在運行過程中,如果頻繁的進行線程上下文切換,會帶來額外的性能損耗。多線程并發(fā)執(zhí)行某個業(yè)務流程,業(yè)務開發(fā)者還需要時刻對線程安全保持警惕,哪些數(shù)據(jù)可能會被并發(fā)修改,如何保護?這不僅降低了開發(fā)效率,也會帶來額外的性能損耗。

    為了解決上述問題,Netty 4采用了串行化設計理念,從消息的讀取、編碼以及后續(xù)Handler的執(zhí)行,始終都由I/O線程NioEventLoop負責,這就意外著整個流程不會進行線程上下文的切換,數(shù)據(jù)也不會面臨被并發(fā)修改的風險,對于用戶而言,甚至不需要了解Netty的線程細節(jié),這確實是個非常好的設計理念,它的工作原理圖如下:

    圖6-4 Netty 4的串行化設計理念

    一個NioEventLoop聚合了一個多路復用器Selector,因此可以處理成百上千的客戶端連接,Netty的處理策略是每當有一個新的客戶端接入,則從NioEventLoop線程組中順序獲取一個可用的NioEventLoop,當?shù)竭_數(shù)組上限之后,重新返回到0,通過這種方式,可以基本保證各個NioEventLoop的負載均衡。一個客戶端連接只注冊到一個NioEventLoop上,這樣就避免了多個I/O線程去并發(fā)操作它。

    Netty通過串行化設計理念降低了用戶的開發(fā)難度,提升了處理性能。利用線程組實現(xiàn)了多個串行化線程水平并行執(zhí)行,線程之間并沒有交集,這樣既可以充分利用多核提升并行處理能力,同時避免了線程上下文的切換和并發(fā)保護帶來的額外性能損耗。

    了解完了Netty 4的串行化設計理念之后,我們繼續(xù)看Netty 3線程模型存在的問題,總結(jié)起來,它的主要問題如下:

    1. Inbound和Outbound實質(zhì)都是I/O相關的操作,它們的線程模型竟然不統(tǒng)一,這給用戶帶來了更多的學習和使用成本;
    2. Outbound操作由業(yè)務線程執(zhí)行,通常業(yè)務會使用線程池并行處理業(yè)務消息,這就意味著在某一個時刻會有多個業(yè)務線程同時操作ChannelHandler,我們需要對ChannelHandler進行并發(fā)保護,通常需要加鎖。如果同步塊的范圍不當,可能會導致嚴重的性能瓶頸,這對開發(fā)者的技能要求非常高,降低了開發(fā)效率;
    3. Outbound操作過程中,例如消息編碼異常,會產(chǎn)生Exception,它會被轉(zhuǎn)換成Inbound的Exception并通知到ChannelPipeline,這就意味著業(yè)務線程發(fā)起了Inbound操作!它打破了Inbound操作由I/O線程操作的模型,如果開發(fā)者按照Inbound操作只會由一個I/O線程執(zhí)行的約束進行設計,則會發(fā)生線程并發(fā)訪問安全問題。由于該場景只在特定異常時發(fā)生,因此錯誤非常隱蔽!一旦在生產(chǎn)環(huán)境中發(fā)生此類線程并發(fā)問題,定位難度和成本都非常大。

    講了這么多,似乎Netty 4 完勝 Netty 3的線程模型,其實并不盡然。在特定的場景下,Netty 3的性能可能更高,就如本文第4章節(jié)所講,如果編碼和其它Outbound操作非常耗時,由多個業(yè)務線程并發(fā)執(zhí)行,性能肯定高于單個NioEventLoop線程。

    但是,這種性能優(yōu)勢不是不可逆轉(zhuǎn)的,如果我們修改業(yè)務代碼,將耗時的Handler操作前置,Outbound操作不做復雜業(yè)務邏輯處理,性能同樣不輸于Netty 3,但是考慮內(nèi)存池優(yōu)化、不會反復創(chuàng)建Event、不需要對Handler加鎖等Netty 4的優(yōu)化,整體性能Netty 4版本肯定會更高。

    總而言之,如果用戶真正熟悉并掌握了Netty 4的線程模型和功能類庫,相信不僅僅開發(fā)會更加簡單,性能也會更優(yōu)!

    6.4. 思考

    就Netty 而言,掌握線程模型的重要性不亞于熟悉它的API和功能。很多時候我遇到的功能、性能等問題,都是由于缺乏對它線程模型和原理的理解導致的,結(jié)果我們就以訛傳訛,認為Netty 4版本不如3好用等。

    不能說所有開源軟件的版本升級一定都勝過老版本,就Netty而言,我認為Netty 4版本相比于老的Netty 3,確實是歷史的一大進步。

    7. 作者簡介

    李林鋒,2007年畢業(yè)于東北大學,2008年進入華為公司從事高性能通信軟件的設計和開發(fā)工作,有7年NIO設計和開發(fā)經(jīng)驗,精通Netty、Mina等NIO框架和平臺中間件,現(xiàn)任華為軟件平臺架構(gòu)部架構(gòu)師,《Netty權(quán)威指南》作者。

    聯(lián)系方式:新浪微博 Nettying 微信:Nettying 微信公眾號:Netty之家


    感謝郭蕾對本文的策劃和審校。

    給InfoQ中文站投稿或者參與內(nèi)容翻譯工作,請郵件至editors@cn.infoq.com。也歡迎大家通過新浪微博(@InfoQ)或者騰訊微博(@InfoQ)關注我們,并與我們的編輯和其他讀者朋友交流。

    posted @ 2015-02-10 12:03 小馬歌 閱讀(931) | 評論 (0)編輯 收藏
     

    這個帖子是關于JAVA中鮮為人知的特性的后續(xù)更新,如果想得到下次在線討論的更新,請通過郵件訂閱,并且不要忘了在評論區(qū)留下你的意見和建議。


        Java是一個安全的開發(fā)工具,它阻止開發(fā)人員犯很多低級的錯誤,而大部份的錯誤都是基于內(nèi)存管理方面的。如果你想搞破壞,可以使用Unsafe這個類。這個類是屬于sun.* API中的類,并且它不是J2SE中真正的一部份,因此你可能找不到任何的官方文檔,更可悲的是,它也沒有比較好的代碼文檔。


        實例化sun.misc.Unsafe

        如果你嘗試創(chuàng)建Unsafe類的實例,基于以下兩種原因是不被允許的。

        1)、Unsafe類的構(gòu)造函數(shù)是私有的;

        2)、雖然它有靜態(tài)的getUnsafe()方法,但是如果你嘗試調(diào)用Unsafe.getUnsafe(),會得到一個SecutiryException。這個類只有被JDK信任的類實例化。

        但是這總會是有變通的解決辦法的,一個簡單的方式就是使用反射進行實例化:

    1. Field f = Unsafe.class.getDeclaredField("theUnsafe"); //Internal reference  
    2. f.setAccessible(true);  
    3. Unsafe unsafe = (Unsafe) f.get(null);  

        注:IDE如Eclipse對會這樣的使用報錯,不過不用擔心,直接運行代碼就行,可以正常運行的。

        (譯者注:還有一種解決方案,就是將Eclipse中這種限制獲取由錯誤,修改為警告,具體操作為將Windows->Preference...->Java->Compiler->Errors/Warnings中的"Deprecated and restricted API",級別由Error修改為Warning就可以了)

        現(xiàn)在進入主題,使用這個對象我們可以做如下“有趣的”事情。


        使用sun.misc.Unsafe

        1)、突破限制創(chuàng)建實例

        通過allocateInstance()方法,你可以創(chuàng)建一個類的實例,但是卻不需要調(diào)用它的構(gòu)造函數(shù)、初使化代碼、各種JVM安全檢查以及其它的一些底層的東西。即使構(gòu)造函數(shù)是私有,我們也可以通過這個方法創(chuàng)建它的實例。

        (這個對單例模式情有獨鐘的程序員來說將會是一個噩夢,它們沒有辦法阻止這種方式調(diào)用大笑

        看下面一個實例(注:為了配合這個主題,譯者將原實例中的public構(gòu)造函數(shù)修改為了私有的):

     

    1. public class UnsafeDemo {  
    2.     public static void main(String[] args) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, InstantiationException {  
    3.         Field f = Unsafe.class.getDeclaredField("theUnsafe"); // Internal reference  
    4.         f.setAccessible(true);  
    5.         Unsafe unsafe = (Unsafe) f.get(null);  
    6.   
    7.         // This creates an instance of player class without any initialization  
    8.         Player p = (Player) unsafe.allocateInstance(Player.class);  
    9.         System.out.println(p.getAge()); // Print 0  
    10.   
    11.         p.setAge(45); // Let's now set age 45 to un-initialized object  
    12.         System.out.println(p.getAge()); // Print 45  
    13.     }  
    14. }  
    15.   
    16. class Player {  
    17.     private int age = 12;  
    18.   
    19.     private Player() {  
    20.         this.age = 50;  
    21.     }  
    22.   
    23.     public int getAge() {  
    24.         return this.age;  
    25.     }  
    26.   
    27.     public void setAge(int age) {  
    28.         this.age = age;  
    29.     }  
    30. }  


        2)、使用直接獲取內(nèi)存的方式實現(xiàn)淺克隆

        如何實現(xiàn)淺克隆?在clone(){...}方法中調(diào)用super.clone(),對嗎?這里存在的問題是首先你必須繼續(xù)Cloneable接口,并且在所有你需要做淺克隆的對象中實現(xiàn)clone()方法,對于一個懶懶的程序員來說,這個工作量太大了。

        我不推薦上面的做法而是直接使用Unsafe,我們可以僅使用幾行代碼就實現(xiàn)淺克隆,并且它可以像某些工具類一樣用于任意類的克隆。

        這個戲法就是把一個對象的字節(jié)碼拷貝到內(nèi)存的另外一個地方,然后再將這個對象轉(zhuǎn)換為被克隆的對象類型。

        

        3)、來自黑客的密碼安全

        這個好似很有趣吧?實事就是這樣的。開發(fā)人員創(chuàng)建密碼或者是保證密碼到字符串中,然后在應用程序的代碼中使用這些密碼,使用過后,聰明的程序員會把字符串的引用設為NULL,因此它就不會被引用著并且很容易被垃圾收集器給回收掉。

        但是從你將引用設為NULL到被垃圾收集器收集的這個時間段之內(nèi)(原文:But from the time, you made the reference null to the time garbage collector kicks in),它是處于字符串池中的,并且在你系統(tǒng)中進行一個復雜的攻擊(原文:And a sophisticated attack on your system),也是可以讀取到你的內(nèi)存區(qū)域并且獲得密碼,雖然機會很小,但是總是存在的。

        這就是為什么建議使用char[]數(shù)組存放密碼,當使用完過后,你可以迭代處理當前數(shù)組,修改/清空這些字符。

        另外一個方式就是使用魔術類Unsafe。你可以創(chuàng)建另外一個和當前密碼字符串具有相同長度的臨時字符串,將臨時密碼中的每個字符都設值為"?"或者"*"(任何字符都可以),當你完成密碼的邏輯后,你只需要簡單的將臨時密碼中的字節(jié)數(shù)組拷貝到原始的密碼串中,這就是使用臨時密碼覆蓋真實的密碼。

        示例代碼可能會是這樣:

    1. String password = new String("l00k@myHor$e");  
    2. String fake = new String(password.replaceAll(".", "?"));  
    3. System.out.println(password); // l00k@myHor$e  
    4. System.out.println(fake); // ????????????  
    5.    
    6. getUnsafe().copyMemory(fake, 0L, null, toAddress(password), sizeOf(password));  
    7.    
    8. System.out.println(password); // ????????????  
    9. System.out.println(fake); // ????????????  

        運行時動態(tài)創(chuàng)建類

        我們可以在運行時運態(tài)的創(chuàng)建類,例如通過編譯后的.class文件,操作方式就是將.class文件讀取到字節(jié)數(shù)據(jù)組中,并將其傳到defineClass方法中。

    1. //Sample code to craeet classes  
    2. byte[] classContents = getClassContent();  
    3. Class c = getUnsafe().defineClass(null, classContents, 0, classContents.length);  
    4. c.getMethod("a").invoke(c.newInstance(), null);  
    5.    
    6. //Method to read .class file  
    7. private static byte[] getClassContent() throws Exception {  
    8.     File f = new File("/home/mishadoff/tmp/A.class");  
    9.     FileInputStream input = new FileInputStream(f);  
    10.     byte[] content = new byte[(int)f.length()];  
    11.     input.read(content);  
    12.     input.close();  
    13.     return content;  
    14. }  
        

        4)、超大數(shù)組

        從所周知,常量Integer.MAX_VALUE是JAVA中數(shù)組長度的最大值,如果你想創(chuàng)建一個非常大的數(shù)組(雖然在通常的應用中不可能會用上),可以通過對內(nèi)存進行直接分配實現(xiàn)。

        下面這個示例將會創(chuàng)建分配一段連續(xù)的內(nèi)存(數(shù)組),它的容易是允許最大容量的兩倍。

    1. class SuperArray {  
    2.     private final static int BYTE = 1;  
    3.     private long size;  
    4.     private long address;  
    5.        
    6.     public SuperArray(long size) {  
    7.         this.size = size;  
    8.         //得到分配內(nèi)存的起始地址  
    9.         address = getUnsafe().allocateMemory(size * BYTE);  
    10.     }  
    11.     public void set(long i, byte value) {  
    12.         getUnsafe().putByte(address + i * BYTE, value);  
    13.     }  
    14.     public int get(long idx) {  
    15.         return getUnsafe().getByte(address + idx * BYTE);  
    16.     }  
    17.     public long size() {  
    18.         return size;  
    19.     }  
    20. }  
        應用示例

    1. long SUPER_SIZE = (long)Integer.MAX_VALUE * 2;  
    2. SuperArray array = new SuperArray(SUPER_SIZE);  
    3. System.out.println("Array size:" + array.size()); // 4294967294  
    4. for (int i = 0; i < 100; i++) {  
    5.     array.set((long)Integer.MAX_VALUE + i, (byte)3);  
    6.     sum += array.get((long)Integer.MAX_VALUE + i);  
    7. }  
    8. System.out.println("Sum of 100 elements:" + sum);  // 300  

        但請注意這可能會導致JVM掛掉。

        

        結(jié)束語

        sun.misc.Unsafe provides almost unlimited capabilities for exploring and modification of VM’s runtime data structures. Despite the fact that these capabilities are almost inapplicable in Java development itself, Unsafe is a great tool for anyone who want to study HotSpot VM without C++ code debugging or need to create ad hoc profiling instruments.

        sun.misc.Unsafe提供了可以隨意查看及修改JVM中運行時的數(shù)據(jù)結(jié)構(gòu),盡管這些功能在JAVA開發(fā)本身是不適用的,Unsafe是一個用于研究學習HotSpot虛擬機非常棒的工具,因為它不需要調(diào)用C++代碼,或者需要創(chuàng)建即時分析的工具。


        參考

        http://mishadoff.github.io/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/



    posted @ 2015-02-04 15:12 小馬歌 閱讀(280) | 評論 (0)編輯 收藏
     
    環(huán)境:
    os: MAC OS X 10.10.1 yosemite
    target build OPENJDK:jdk8
    Xcode:6.1
    LLVM Version:

    Apple LLVM version 6.0 (clang-600.0.54) (based on LLVM 3.5svn)

    Target: x86_64-apple-darwin14.0.0

    Thread model: posix

    步驟:
    1 國內(nèi)hg拉取代碼不靠譜,到這里打包下載,118M  
    http://download.java.net/openjdk/jdk8/

    安裝X11,系統(tǒng)默認好像是沒有這個東西,需要下載安裝XQuartz,然后link下 sudo ln -s /usr/X11/include/X11 /usr/include/X11

    sudo ln -s /usr/bin/llvm-g++ /Applications/Xcode.app/Contents/Developer/usr/bin/llvm-g++  
    sudo ln -s /usr/bin/llvm-gcc /Applications/Xcode.app/Contents/Developer/usr/bin/llvm-gcc
    4 安裝
    Xcode的Command line tools、freetype
    ./configure --enable-debug --with-target-bits=64
    6 cd jdk8;
    unset JAVA_HOME
    unset CLASSPATH 
    make CC=clang COMPILER_WARNINGS_FATAL=false LFLAGS='-Xlinker -lstdc++' USE_CLANG=true LP64=1 LANG=C ALT_BOOTDIR=/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home ARCH_DATA_MODEL=64 HOTSPOT_BUILD_JOBS=8 ALT_FREETYPE_HEADERS_PATH=/Users/mungo/Downloads/work/homebrew-master/Cellar/freetype/2.5.5/include ALT_FREETYPE_LIB_PATH=/Users/mungo/Downloads/work/homebrew-master/Cellar/freetype/2.5.5/lib
    7 驗證結(jié)果:

    ./build/macosx-x86_64-normal-server-fastdebug/jdk/bin/java -version

    openjdk version "1.8.0-internal-fastdebug"

    OpenJDK Runtime Environment (build 1.8.0-internal-fastdebug-mungo_2015_01_29_16_11-b00)

    OpenJDK 64-Bit Server VM (build 25.0-b70-fastdebug, mixed mode)


    出現(xiàn)的問題:

    clang: error: unknown argument: '-fcheck-new' [-Wunused-command-line-argument-hard-error-in-future]  

             于是直接打開YourOpenJDK/hotspot/make/bsd/makefiles/gcc.make,把這行(line 193)給注釋掉
    2 relocInfo.hpp錯誤。 將接口的默認值去掉,把默認值放到的方法參數(shù)里即可。hotspot/src/share/vm/code/relocInfo.hpp 
    + inline friend relocInfo prefix_relocInfo(int datalen);

    +inline relocInfo prefix_relocInfo(int datalen = 0) {
       assert(relocInfo::fits_into_immediate(datalen), "datalen in limits");
       return relocInfo(relocInfo::data_prefix_tag, relocInfo::RAW_BITS, relocInfo::datalen_tag | datalen);
     }

    參考文章:
    http://hllvm.group.iteye.com/group/topic/39814
    http://yueyemaitian.iteye.com/blog/2038304

    另:openjdk 7 hotspot我可以編譯成功,但是最后打包的時候,出現(xiàn) 執(zhí)行cp 錯誤.沒找到原因

    posted @ 2015-01-30 10:06 小馬歌 閱讀(631) | 評論 (0)編輯 收藏
     
         摘要: 這是我的WWDC2013系列筆記中的一篇,完整的筆記列表請參看這篇總覽。本文僅作為個人記錄使用,也歡迎在許可協(xié)議范圍內(nèi)轉(zhuǎn)載或使用,但是還煩請保留原文鏈接,謝謝您的理解合作。如果您覺得本站對您能有幫助,您可以使用RSS或郵件方式訂閱本站,這樣您將能在第一時間獲取本站信息。本文涉及到的WWDC2013 Session有Session 204 What's New with MultitaskingSe...  閱讀全文
    posted @ 2015-01-19 18:11 小馬歌 閱讀(207) | 評論 (0)編輯 收藏
     

    接近一周沒更新《Java線程》專欄了,主要是這周工作上比較忙,生活上也比較忙,呵呵,進入正題,上一篇講述了并發(fā)包下的Lock,Lock可以更好的解決線程同步問題,使之更面向?qū)ο螅⑶襌eadWriteLock在處理同步時更強大,那么同樣,線程間僅僅互斥是不夠的,還需要通信,本篇的內(nèi)容是基于上篇之上,使用Lock如何處理線程通信。

            那么引入本篇的主角,Condition,Condition 將 Object 監(jiān)視器方法(wait、notify 和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意 Lock 實現(xiàn)組合使用,為每個對象提供多個等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監(jiān)視器方法的使用。下面將之前寫過的一個線程通信的例子替換成用Condition實現(xiàn)(Java線程(三)),代碼如下:

    1. public class ThreadTest2 {  
    2.     public static void main(String[] args) {  
    3.         final Business business = new Business();  
    4.         new Thread(new Runnable() {  
    5.             @Override  
    6.             public void run() {  
    7.                 threadExecute(business, "sub");  
    8.             }  
    9.         }).start();  
    10.         threadExecute(business, "main");  
    11.     }     
    12.     public static void threadExecute(Business business, String threadType) {  
    13.         for(int i = 0; i < 100; i++) {  
    14.             try {  
    15.                 if("main".equals(threadType)) {  
    16.                     business.main(i);  
    17.                 } else {  
    18.                     business.sub(i);  
    19.                 }  
    20.             } catch (InterruptedException e) {  
    21.                 e.printStackTrace();  
    22.             }  
    23.         }  
    24.     }  
    25. }  
    26. class Business {  
    27.     private boolean bool = true;  
    28.     private Lock lock = new ReentrantLock();  
    29.     private Condition condition = lock.newCondition();   
    30.     public /*synchronized*/ void main(int loop) throws InterruptedException {  
    31.         lock.lock();  
    32.         try {  
    33.             while(bool) {                 
    34.                 condition.await();//this.wait();  
    35.             }  
    36.             for(int i = 0; i < 100; i++) {  
    37.                 System.out.println("main thread seq of " + i + ", loop of " + loop);  
    38.             }  
    39.             bool = true;  
    40.             condition.signal();//this.notify();  
    41.         } finally {  
    42.             lock.unlock();  
    43.         }  
    44.     }     
    45.     public /*synchronized*/ void sub(int loop) throws InterruptedException {  
    46.         lock.lock();  
    47.         try {  
    48.             while(!bool) {  
    49.                 condition.await();//this.wait();  
    50.             }  
    51.             for(int i = 0; i < 10; i++) {  
    52.                 System.out.println("sub thread seq of " + i + ", loop of " + loop);  
    53.             }  
    54.             bool = false;  
    55.             condition.signal();//this.notify();  
    56.         } finally {  
    57.             lock.unlock();  
    58.         }  
    59.     }  
    60. }  
            在Condition中,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll(),傳統(tǒng)線程的通信方式,Condition都可以實現(xiàn),這里注意,Condition是被綁定到Lock上的,要創(chuàng)建一個Lock的Condition必須用newCondition()方法。

            這樣看來,Condition和傳統(tǒng)的線程通信沒什么區(qū)別,Condition的強大之處在于它可以為多個線程間建立不同的Condition,下面引入API中的一段代碼,加以說明。

    1. class BoundedBuffer {  
    2.    final Lock lock = new ReentrantLock();//鎖對象  
    3.    final Condition notFull  = lock.newCondition();//寫線程條件   
    4.    final Condition notEmpty = lock.newCondition();//讀線程條件   
    5.   
    6.    final Object[] items = new Object[100];//緩存隊列  
    7.    int putptr/*寫索引*/, takeptr/*讀索引*/, count/*隊列中存在的數(shù)據(jù)個數(shù)*/;  
    8.   
    9.    public void put(Object x) throws InterruptedException {  
    10.      lock.lock();  
    11.      try {  
    12.        while (count == items.length)//如果隊列滿了   
    13.          notFull.await();//阻塞寫線程  
    14.        items[putptr] = x;//賦值   
    15.        if (++putptr == items.length) putptr = 0;//如果寫索引寫到隊列的最后一個位置了,那么置為0  
    16.        ++count;//個數(shù)++  
    17.        notEmpty.signal();//喚醒讀線程  
    18.      } finally {  
    19.        lock.unlock();  
    20.      }  
    21.    }  
    22.   
    23.    public Object take() throws InterruptedException {  
    24.      lock.lock();  
    25.      try {  
    26.        while (count == 0)//如果隊列為空  
    27.          notEmpty.await();//阻塞讀線程  
    28.        Object x = items[takeptr];//取值   
    29.        if (++takeptr == items.length) takeptr = 0;//如果讀索引讀到隊列的最后一個位置了,那么置為0  
    30.        --count;//個數(shù)--  
    31.        notFull.signal();//喚醒寫線程  
    32.        return x;  
    33.      } finally {  
    34.        lock.unlock();  
    35.      }  
    36.    }   
    37.  }  
            這是一個處于多線程工作環(huán)境下的緩存區(qū),緩存區(qū)提供了兩個方法,put和take,put是存數(shù)據(jù),take是取數(shù)據(jù),內(nèi)部有個緩存隊列,具體變量和方法說明見代碼,這個緩存區(qū)類實現(xiàn)的功能:有多個線程往里面存數(shù)據(jù)和從里面取數(shù)據(jù),其緩存隊列(先進先出后進后出)能緩存的最大數(shù)值是100,多個線程間是互斥的,當緩存隊列中存儲的值達到100時,將寫線程阻塞,并喚醒讀線程,當緩存隊列中存儲的值為0時,將讀線程阻塞,并喚醒寫線程,下面分析一下代碼的執(zhí)行過程:

            1. 一個寫線程執(zhí)行,調(diào)用put方法;

            2. 判斷count是否為100,顯然沒有100;

            3. 繼續(xù)執(zhí)行,存入值;

            4. 判斷當前寫入的索引位置++后,是否和100相等,相等將寫入索引值變?yōu)?,并將count+1;

            5. 僅喚醒讀線程阻塞隊列中的一個;

            6. 一個讀線程執(zhí)行,調(diào)用take方法;

            7. ……

            8. 僅喚醒寫線程阻塞隊列中的一個。

            這就是多個Condition的強大之處,假設緩存隊列中已經(jīng)存滿,那么阻塞的肯定是寫線程,喚醒的肯定是讀線程,相反,阻塞的肯定是讀線程,喚醒的肯定是寫線程,那么假設只有一個Condition會有什么效果呢,緩存隊列中已經(jīng)存滿,這個Lock不知道喚醒的是讀線程還是寫線程了,如果喚醒的是讀線程,皆大歡喜,如果喚醒的是寫線程,那么線程剛被喚醒,又被阻塞了,這時又去喚醒,這樣就浪費了很多時間。

            本文來自:高爽|Coder,原文地址:http://blog.csdn.net/ghsau/article/details/7481142,轉(zhuǎn)載請注明。

      posted @ 2015-01-15 09:36 小馬歌 閱讀(220) | 評論 (0)編輯 收藏
       

      Gitlab是一個用Ruby on Rails開發(fā)的開源項目管理程序,可以通過WEB界面進行訪問公開的或者私人項目。它和Github有類似的功能,能夠瀏覽源代碼,管理缺陷和注釋。

      下面介紹如何在 Debian/Ubuntu 和 Centos 下搭建配置 GitLab。

      安裝依賴

      Debian/Ubuntu下:

      sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate 

      安裝python(注意需要2.5以上版本):

      sudo apt-get install -y python python-docutils 

      安裝git(注意需要1.7.10以上版本):

      sudo apt-get install -y git-core 

      Centos下官方倉庫的軟件比較老舊,推薦先添加epel源,然后再安裝依賴:

      sudo yum install git patch gcc-c++ readline-devel zlib-devel libffi-devel openssl-devel make autoconf automake libtool bison libxml2-devel libxslt-devel libyaml-devel git python python-docutils 

      安裝 Ruby 2.0

      需要安裝Ruby2.0,軟件倉庫中的Ruby 1.8不支持:

      mkdir /tmp/ruby && cd /tmp/ruby curl --progress ftp://ftp.ruby-lang.org/pub/ruby/2.0/ruby-2.0.0-p353.tar.gz | tar xz cd ruby-2.0.0-p353 ./configure --disable-install-rdoc make sudo make install 

      安裝Bundler Gem:

      sudo gem install bundler --no-ri --no-rdoc 

      配置gitlab-shell

      創(chuàng)建git用戶:

      sudo adduser --system --create-home --comment 'GitLab' git   

      配置gitlab-shell

      su - git -c "git clone https://github.com/gitlabhq/gitlab-shell.git"   su - git -c "cd gitlab-shell && git checkout v1.3.0"   su - git -c "cp gitlab-shell/config.yml.example gitlab-shell/config.yml"   sed -i "s/localhost/gitlab.51yip.com/g" /home/git/gitlab-shell/config.yml   su - git -c "gitlab-shell/bin/install"   chmod 600 /home/git/.ssh/authorized_keys   chmod 700 /home/git/.ssh 

      數(shù)據(jù)庫

      GitLab支持 MySQL 和 PostgreSQL 數(shù)據(jù)庫。下面以 MySQL為例,介紹安裝方法:

      Debian/Ubuntu下使用如下命令安裝:

      sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev 

      Centos下使用如下命令:

      sudo yum install mysql-server  sudo chkconfig mysqld on 

      配置MySQL:

      sudo echo "CREATE DATABASE IF NOT EXISTS gitlabhq_production DEFAULT CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci';" | mysql -u root  sudo echo "UPDATE mysql.user SET Password=PASSWORD('123456') WHERE User='root'; FLUSH PRIVILEGES;" | mysql -u root  

      注意,用你的密碼替換123456

      安裝配置 gitlab

      su - git -c "git clone https://github.com/gitlabhq/gitlabhq.git gitlab"   su - git -c "cd gitlab;git checkout 5-1-stable"   su git -c "cp config/gitlab.yml.example config/gitlab.yml"   su git -c "mkdir /home/git/gitlab-satellites"   su git -c "mkdir public/uploads"   su git -c "mkdir -p tmp/sockets/"   su git -c "mkdir -p tmp/pids/"   sed -i "s/ host: localhost/ host: gitlab.segmentfault.com/g" config/gitlab.yml   sed -i "s/from: gitlab@localhost/from: gitlab@gitlab.segmentfault.com/g" config/gitlab.yml   su git -c "cp config/puma.rb.example config/puma.rb"   su git -c 'git config --global user.name "GitLab"'   su git -c 'git config --global user.email "gitlab@gitlab.segmentfault.com"' 

      注意將gitlab.segmentfault.com替換為你自己的內(nèi)容。

      配置數(shù)據(jù)庫連接:

      sudo su git -c "cp config/database.yml.mysql config/database.yml" sudo sed -i "s/secure password/mysql的root密碼/g" config/database.yml 

      安裝MySQL需要的Gems

      sudo -u git -H bundle install --deployment --without development test postgres aws 

      初始化:

      sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab sudo chmod +x /etc/init.d/gitlab sudo update-rc.d gitlab defaults 21 

      查看是否配置妥當:

      sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production 

      重啟GitLab:

      sudo service gitlab start 

      配置Nginx

      Debian/Ubuntu下:

      sudo apt-get install -y nginx 

      CentOS下:

      sudo yum install nginx 

      下載配置文件樣例:

      sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab 

      修改 /etc/nginx/sites-available/gitlab,特別留意將 YOUR_SERVER_FQDN 改成自己的。

      重啟nginx:

      sudo service nginx restart 

      好了,你可以登錄GitLab了,默認安裝后的用戶名:admin@local.host,密碼5iveL!fe

      posted @ 2015-01-14 14:44 小馬歌 閱讀(667) | 評論 (0)編輯 收藏
      僅列出標題
      共95頁: First 上一頁 13 14 15 16 17 18 19 20 21 下一頁 Last 
       
      主站蜘蛛池模板: 亚洲网址在线观看你懂的| 综合久久久久久中文字幕亚洲国产国产综合一区首| 亚洲人成网亚洲欧洲无码久久| 精品免费AV一区二区三区| 国产福利免费观看| 污污视频网站免费观看| 亚洲午夜AV无码专区在线播放| 一级做α爱过程免费视频| 一本色道久久综合亚洲精品高清| 亚洲免费日韩无码系列| 亚洲精品无码久久久影院相关影片| 99在线免费视频| 亚洲精品视频免费在线观看| 免费做爰猛烈吃奶摸视频在线观看 | 亚洲av一综合av一区| 99久久国产精品免费一区二区| 亚洲av不卡一区二区三区 | 两个人www免费高清视频| 亚洲∧v久久久无码精品| 四虎成年永久免费网站 | 一个人免费观看视频在线中文| 国产亚洲AV夜间福利香蕉149| 久久国产精品免费专区| 亚洲香蕉久久一区二区| 亚洲av无码不卡私人影院| 少妇性饥渴无码A区免费| 亚洲中文字幕无码av在线| 国产成人无码免费视频97| 十八禁在线观看视频播放免费| 亚洲精品在线播放| 又粗又硬免费毛片| 午夜网站在线观看免费完整高清观看 | 13小箩利洗澡无码视频网站免费| 久久亚洲AV无码精品色午夜| 女人张开腿给人桶免费视频| 中文在线观看免费网站| 国产成人精品日本亚洲网址| 亚洲成A人片在线观看中文| 3344永久在线观看视频免费首页| 日本亚洲中午字幕乱码| 亚洲五月激情综合图片区|