OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

jopen 10年前發布 | 61K 次閱讀 OpenJDK Java開發

從Java 6開始,要求標準化非堆存儲(off-heap)作為Java內部API的提議就已經在JDK強化提案(JEP)中被提出。這種方式的處理能力和堆存儲(on-heap)一樣高效,并且沒有堆存儲使用中的一些局限問題。堆存儲在百萬數量級瞬時使用的對象/值下工作的相當好,但是一旦你試圖存儲十億數量級的對象/值時,你就要想辦法去避免垃圾回收帶來的持續增加的延遲。并且有時系統會要求同時保證大量數據處理和低延遲。非堆存儲就是有這樣一種能力:獨立管理內存空間而不產生垃圾回收壓力。Java中管理集合的兩個類”Queue“和"HashMap"使用起來相當方便,如果使用這兩個已有接口再加上我們自己的垃圾回收機制實現起來應該不是很難。這樣既能實現大量數據存儲并且能大大減少延遲,相比而言,原有的堆存儲方式很容易產生內存不足錯誤,隨之就要重啟服務了。

這篇文章將會研究 JEP所帶來的影響,將使得我們獲悉類似于Java HashMap和新的off-heap的性能。簡言之,JEP可能就有“指導”HashMap這個可愛的老家伙的一些新特性的魔法。 JEP所述的特性,在OpenJDK的發布來看,相對于傳統的Java平臺優先級做了許多重大的改變。

1、關于安全性的重構,這一sun.misc.Unsafe上的有用的部分,被放入了 新的API包
2、提倡使用新的API包,直接影響高性能的本地內存操作(在off-heap上的本地內存操作對象上)。
3、(通過新的API)提供一個 外部函數接口(FFI)橋 針對Java直接操作系統資源和系統調用。
4、許可了Java運行時能輔助 硬件事務性內存(Hardware Transactional Memory)的提供者能把焦點集中在重寫低并發字節碼到高并發的 speculatively branched機器碼。
5、移除了FUD(坦率的講這是一種技術偏見),這與使用off-heap編程策略來提升Java的執行性能有關。總的來講,JEP有幾點是很清楚的,在OpenJDK平臺上,相對于曾經的   dark craft, secret society of off-heap practitioners,現在的主流對開放是擁抱的。

本文力求(用普遍而溫和的方式)讓所有對此感興趣的 Java 開發者都能有所收獲。作者希望即使新手也能跟上本文節奏,而不會有看不懂的“磕磕絆絆”;因此不要氣餒,耐心坐下來讀完吧。本文努力介紹一些歷史背景,為以下問題提供思路:

  • 堆存儲 HashMap 的問題是怎么產生的?

    </li>

  • 在解決這個問題上面,有過哪些經驗/教訓?

    </li>

  • 在堆存儲 HashMap 的應用情景中,有哪些仍未解決的問題?

    </li>

  • 新的 JEP 提供的功能(將 HashMap 非堆存儲)能帶來哪些好處?

    </li>

  • 未來的 JEP 在解決現在尚未解決的問題上面,有哪些值得期待之處?

    </li> </ul>

    那就讓我們一起開始這段旅程吧。值得記住的是,在 Java 出現之前,hash 表是實現在原生內存堆中的,如C 和 C++ 都是如此。某種意義上來說,重新引入非堆存儲是重新介紹一些古老的技巧,這些技巧當代的開發者往往不曾了解。各種意義上來說,這都是一次“回到未來”的旅程。旅途愉快!

    OpenJDK的非堆存儲(Off-Heap)的強化提案(JEP)

    已經有一些非堆存儲(Off-Heap)的強化提案(JEP)被提出來。下面描繪了一個提供非堆存儲(Off-Heap)內存的最低要求。方案試圖替代現在sun.misc.Unsafe所提供的內容,不僅如此,這些方案還提供了另外一些有用的功能。
    提案總結:總的來說就是為sun.misc.Unsafe創建了一個替代的部分,這樣就可以不用直接使用那個庫。
    直接目標:移除需要直接訪問的內部類。
    間接目標:不提供那些不推薦的方法,也不實現那些不安全(Unsafe)的方法。
    成功標準:提供一種方式去實現那些重要的功能,并且達到與那些不安全(Unsafe)和 FileDispatcherImpl的方式一樣的性能。
    提案動機:當前不安全(Unsafe)的方式就意味著就需要構建更大的,線程上更安全的非堆存儲(Off-Heap)結構。這對于最小化垃圾處理器(GC)的開銷有益。這對于在進程和內嵌數據庫之間的內存共享可以不用C語言和JNI,這也就有可能提供更快更多的移動計算性能。當前的FileDispatcherImpl方式用于實現任意大小內存的映射。(標準API被限制在2GB以內。)

    描述:為非堆存儲(off-heap)提供一個包裝類(類似于 ByteBuffer) ,還需要下面的增強。

    • 64位的大小和偏移量

      </li>

    • 對于易失(volatile)的和有序的訪問以及比較和交換的操作上有線程安全的結構。

      </li>

    • JVM優化邊界檢查,開發者控制邊界檢查。(允許提供安全性設置)

      </li>

    • 有能力在同一緩沖區的不同記錄復用一份緩沖。

      </li>

    • 有能力去映射一個非堆存儲(off-heap)數據結構,讓緩沖區在優化過的方式下進行邊界檢查。

      </li> </ul>

      保留關鍵功能

      • 支持內存映射文件 

        </li>

      • 支持NIO 

        </li>

      • 支持把寫操作提交到磁盤

        </li> </ul>

        候選方案:直接使用sun.misc.Unsafe
        測試:sun.misc.Unsafe和內存映射文件有同樣的測試需求。附加的測試應該工作在同樣的方式下,要求展示的線程安全的操作為AtomicXxxx類。AtomicXxxx類應該被重寫并且單獨使用公共的API。

        風險: 當一群開發者使用了Unsafe之后,他們可能一致認為沒有更適合的替代品。這意味著JEP的范圍很廣,或者創建了新的JEP覆蓋了Unsafe中的其他功能。

        其他JDK : NIO

        兼容性: 提供了向后兼容的庫。它兼容java7,如果你有足夠的興趣去研究的話,也有可能兼容java6。(截止到這篇文章,Java 7是當前的版本)。

        安全性: 在理想情況下,安全的風險性不能超過ByteBuffer太多。

        性能和可擴展性: 優化邊界檢查是困難的。為了添加更多的普通操作,則需要把功能添加到新的緩沖區,以減少開銷,例如讀寫UTF。

        HashMap簡史

        “Hash Code”這個概念第一次出現是在1953年1月的《Computing literature》中,H. P. Luhn  (1896-1964) 在一篇 IBM 的內部備忘錄中提出了這個術語。當時 Luhn 是要解決這個問題:“給出組成一本教科書的一系列單詞,要得出 100% 完整的(單詞,出現頁碼集)對應關系,最好的算法和數據結構是什么?”

        OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

        H.P. Luhn (1896-1964)

        </td>

        Luhn 寫道, “hashcode” 是基本的運算符。

        Luhn 寫道, “Associative Array” 是基本的運算數。

        由此, ‘HashMap’ (也稱為 HashTable) 就這樣產生了。

        注: HashMap 是由 1896 年出生的計算機科學家提出來的。HashMap 可是個老 家伙啦!

        </td> </tr> </tbody> </table>

        從 HashMap 的誕生講到它的早期應用場景,我們從1950年代跳到1970年代

      • Niklaus Wirth 在他1976年編寫的經典著作《算法 + 數據結構 = 程序》中,談到對于所有的程序,都可以將“算法”視為基本的運算符,將“數據結構”視為基本的運算“數”。

        從那時起,數據結構(HashMap,Heap等)發展緩慢。1987年有一個重大突破, Tarjan 提出了非常著名的 F-Heap ;但除此之外,乏善可陳。要知道,HashMap 是1953年第一次提出的,已經過去60余年啦!

        與此同時,算法方面 (Karmakar 1984, NegaMax 1989, AKS Primality 2002, Map-Reduce 2006, Grover’s Quantum search - 2011) 則進展迅速,為計算的基礎建設帶來了嶄新的、強大的運算符。

        然而,現在到了2014,也許又輪到數據結構來取得重大進展了。從 OpenJDK 平臺來看, 非堆 HashMap 就是一個正在發展的數據結構。

        HashMap 的歷史就介紹到這。下面我們來探索今天的 HashMap 吧。具體來說,我們先來看一看這個老家伙在 Java 中現存的 3 種實現。

        </td>

        OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

        N. Wirth 1934-

        </td> </tr> </tbody> </table>

        java.util.HashMap (非線程安全)

        對于任何真正的多線程并發用例,它會立即失敗,而且是每次都會失敗。所有用到它的代碼必須使用 Java 內存模型(JMM)的內存屏障(memory barrier)策略(如 synchronized 或 volatile) 來保證順序執行。

        OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

        </td>

        一個簡單的失敗樣例如下:

        - synchronized 的寫入

        - 沒加 synchronized 的讀取

        - 真正并發 (2 個 CPU/L1)

         

        我們來看看為什么會失敗...

        </td> </tr> </tbody> </table>

        假設線程1寫入 HashMap,那么它做出的改動只會保存在 CPU 1的1級緩存中。然后線程2,在幾秒鐘后開始在 CPU 2上運行;它讀取 HashMap,是從 CPU 2的1級緩存中讀出來的——它看不到線程1做出的改動,因為在讀和寫的線程中沒有讀、寫間的內存屏障,雖然 Java 內存模型要求線程共享 HashMap 的情形下必須要有。即使線程1的寫操作加了 synchronize 也會失敗,這樣雖然能把它做出的改動寫入到主內存中,但線程2仍然看不到這些改動,因為線程2只會從 CPU 2的1級緩存中讀取。所以在寫操作上加 synchronized 只能避免寫操作的沖突。要對于所有的線程都添加必要的內存屏障,你必須也要 synchronize 讀操作。

        thrSafeHM = Collections.synchronizedMap(hm) ; (粗粒度鎖定)

        使用“同步”時實現高性能要求低競爭率。這是很常見的,而且在很多情況下這并不像聽起來那么壞。然而,一旦你引入任何競爭(多個線程試圖同時操作同一集合),性能就會受到影響。在最壞的情況下,如具有很高的鎖爭用,你可能會得到多個線程比單個線程(操作沒有鎖定或任何種類的爭奪)的性能表現更差的結論。

        OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)
        Collections.synchronizedMap() 返回一個 MT-Safe HashMap.

        這是一個通過粗粒度的鎖來實現所有關鍵部分的mutate()和access()操作,這樣可以讓多個線程操作整個Map。這個結果在Zero  MT-concurrency中,意味著一個時刻僅有一個線程可以訪問。另一個后果就是作為高鎖爭用(High Lock Contention)的粗粒度鎖,鎖住的途徑是一種非常不受歡迎的已知條件。關于高鎖爭用(High Lock Contention)(請看在左邊的圖片,N個線程爭用一個鎖,但是迫于阻塞只好等待著,鎖已經給了正在運行的線程)。

        幸好這是完全同步的,不會真正的同步,隔離(isolation)=序列化(SERIALIZABLE)(總體上這是令人失望的)HashMap陷阱,我們期待的OpenJDK非堆存儲(off-heap)JEP已經有一個 值得推薦的期待:硬件事務性內存(Hardware Transactional Memory (HTM))。關于HTM,粗粒度的同步寫操作在Java中將會再一次變得很酷!就讓HTM通過代碼上的零并發和在硬件的零并發來幫助我們,實現真正的并發并且100%的多線程安全。這很酷,對吧?

        java.util.concurrent.ConcurrentHashMap (線程安全、智能鎖,但并非完美)

        在jdk1.5的核心API中,終于發布了java程序員夢寐以求的java.util.concurrent.ConcurrentHashMap。雖然ConcurrentHashMap不能廣泛替代HashMap(ConcurrentHashMap消耗更多的資源,在低競爭條件下可能不太適合。),但是它解決了其它類型的HashMap解決不了的問題:提供既有真正的多線程安全,又有真正的多線程并發的能力。讓我們畫一幅畫來準確地描述ConcurrentHashMap為什么(原文是how)這么有用的(有效,有作用,不知道怎么翻譯好了。原文:helpful)。

        1.分離鎖
        OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

        2.每個獨立的HashMap子集對應一個鎖:N個hash桶(子集)對應N段(Segments)鎖。(在圖片右邊,段(Segments) = 3

        3.在設計出將一個高競爭的鎖分解成多個不影響數據完整性的鎖時,分離鎖是非常有用的。

        4.更好的并發,在處理"先檢查判斷狀態,再操作"("check-then-act")的競態條件問題時,concurrentHashMap是一個不需要同步的解決方案。

        5.問題:你如何同時保護整個集合(collections)? 獲取所有的鎖(遞歸地)?

        現在你可能要問了:隨著ConcurrentHashMap和java.util.concurrent包的發布,java是一個高性能計算社區(High Performance Computing community)能夠在上面創建解決方案來解決他們問題的終極編程平臺嗎?

        不幸的是,很現實的一個回答還是“還沒呢”。真的,那么還存在著什么問題呢?

        ConcurrentHashMap存在著規模問題和保存中間態對象(medium-lived objects)問題。如果你有一小部分使用concurrentHashMap的關鍵的集合對象,很可能有些會很大。在某些情況下,在這些集合中存在著大量的中間態對象(medium-lived objects)。這些中間態對象(medium-lived objects)貢獻了大部分的GC次數(時間,GC pause times),他們的消耗有可能是短暫對象(short-lived objects)的20倍。長時間存活對象(Long-lived objects)往往停留在終身區(tenured space),短暫對象(short-lived objects)在young區死亡,但是中間態對象(medium-lived objects)會復制到所有的存活空間,并在終身區(trenured space)死亡,中間態對象(medium-lived objects)到處拷貝并在最后被清理產生的消耗十分巨大。最理想的是你能有一個沒有GC影響的儲存數據的集合。


        /******注****/

        翻譯中的medium-lived objects, short-lived objects,Long-lived objects,tenured space,young space

        這類詞,對應的是java GC中的詞語,對應的中文翻譯是啥我記不住了,請編輯或其他朋友修正。

        /**********/

        ConcurrentHashMap元素在運行時存在Java VM堆里。因為CHM是堆存儲,它對于 Stop-the-World (STW) 有著顯著的貢獻,即使不是最顯著的。當STW的GC事件發生,所有應用程序的處理都要忍受著臭名昭著的“緊急暫停”的延時。這種延時,是由CHM(以及它的所有元素)放在堆存儲中引起的,是一個慘痛的經歷。這是一個經驗也是一個高性能計算所不能忍受的問題。

        OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)在高性能計算組織完全接受Java之前,必須要有個解決方案來馴服這個堆存儲的GC怪獸

        解決方案從精神層面上講非常簡單:就是把CHM放到非堆存儲中。

        而這個解決方案,OpenJDK的非堆存儲JEP當然是支持的。

        在我們深入展示非堆存儲如何跟HashMap相似之前,我們先完整地了解堆存儲的不友好的細節。

        堆簡史

        Java的堆存儲是由操作系統分配給JVM的。所有的Java對象都通過堆存儲JVM位置/標識來引用。你在堆存儲上運行一個對象必定會引用兩個堆區域其中之一。這些區域更確切來說為一代。明確來分為:(1)年輕代 (由EDEN和兩個SURVIVOR子空間組成) 和 (2) 年老代。 (注:Oracle日前宣布,持久代在JDK7中開始逐步淘汰,而在JDK8將會完全被淘汰)。所有的代都遭受了可怕的“全部停止(Stop-the-World)”完全垃圾回收事件,除非你使用“少量暫停”回收機制例如Azul的Zing.

        在垃圾回收的世界里,操作是由“回收機制”執行的,這些回收器的操作對象是堆的“代”(以及子空間)的目標。回收器在堆棧/空間目標中進行操作。完整的垃圾回收是如何工作的內部細節是它自己本身一個(非常大的)主題,有專門的文章會提到。

        現在知道一點:如果任何回收器(任何類型的)操作任何一代的堆空間都會造成“停止一切(Stop The World)”的事件——這是一個非常嚴重的問題。

        這是一個問題必須得有個解決方案。

        這是一個問題只有非堆存儲JEP可以解決。

        讓我們仔細看看。

        Java堆布局: 查看它的歷代

        OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

        垃圾回收使得編程變得更加容易,但是在SLA目標的世界里,無論是書面的還是暗示的(我的Java Applet暫停30秒不是一種選擇),停止一切(Stop-The-World)時間暫停對于許多Java開發人員來說是一個很頭疼的問題,擺在他們面前的只有性能問題。順便提一下,還有許多其他性能問題需要處理,只有在STW不再是問題的時候。

        使用off-heap存儲的好處,就是中等壽命對象的數量可以大幅度下降。它甚至也可以降低短壽命對象的數量。對于高頻交易系統,它一天可以創建的垃圾比你的Eden空間大小還要小,這意味著你可以運行一整天而不需要一個簡單的回收。一旦你有非常低的內存壓力,以及部分對象已經到達年老代(tenured)空間,調整你的GC就會變得很瑣碎。通常你甚至不需要設置GC的參數(除非希望增加eden區的大小)。

        通過移動對象到非堆存儲,Java應用程序往往能夠收回監管控制自己的命運,滿足SLA性能的期望和義務。

        等一下,剛剛最后一句說啥來著?

        注意:所有乘客,請收起你的托盤并坐直來。OpenJDK非堆存儲JEP的中央租戶是一個非常值得重復的事情。

        移動回收(如HashMap)到非堆存儲,Java應用程序經常能夠請求他們的回收(不再依賴于STW的GC機制中的“緊急暫停”事件)去控制他們自身的命運,滿足SLA性能的期望和義務。

        這是一個很實用的選擇,在Java的高頻率交易系統中已經在使用。

        這個選擇也徹底需要Java保持著對高性能計算越來越多的吸引力。

        堆存儲的優勢

        1. 常見的,寫普通的Java代碼。所有有經驗的Java開發人員都可以做到。

          </li>

        2. 訪問內存的安全性問題。

          </li>

        3. 自動的GC服務——無需自身管理的malloc()/free()操作。

          </li>

        4. 完整的 Java Lock API和JMM相結合。

          </li>

        5. 添加無序列化/復制數據到一個結構中去。

          </li> </ol>

          非堆存儲的優勢

          1. 控制"停止一切(Stop the World)"的GC事件到你比較滿意的層次。

            </li>

          2. 可以超越在規模上的堆存儲結構(當使用堆存儲的時候會變得很高)

            </li>

          3. 可以作為一個本地的IPC傳輸(無需java.net.Socket的IP回送)

            </li>

          4. 分配器的注意事項:

            </li> </ol>

              • NIO DirectByteBuffer到/dev/shm (tmpfs)的map?

                </li>

              • 或者直接sun.misc.Unsafe.malloc()?

                </li> </ul> </ul>

                HashMap的介紹 … 有什么新的問題使得這個“老家伙”得以解決呢(通過使用非堆存儲)?

                介紹 OpenHFT HugeCollections (SHM)

                到底哪里才是“非堆存儲”?

                下圖介紹了兩個使用ShardHashMap(SHM)作為進程間通信(IPC)的Java VM過程(PID1和PID2)。圖表底部的水平軸代表的是完全SHM操作系統的所在域。當OpenHFT對象被操作時,它就會在操作系統中物理內存的用戶地址空間或者內核地址空間的某處。往深一層思考,我們知道他們以“關于進程”的局部開始著手。從Linux操作系統來看,JVM是一個a.out (通過調用 gcc呈現的)。當a.out在運行的時候會有一個PID。一個 PID的 a.out (在運行時)包含以下三個方面:

                1. 文檔(低地址……執行代碼的地方)

                  </li>

                2. 數據(通過sbrk(2)從低地址升級到高地址來掌管)

                  </li>

                3. 棧(從高地址到低地址來掌管)

                  </li> </ol>

                  這是PID在操作系統中的表現形式。也就是說,PID是一個執行的JVM,JVM有它自己操作對象潛在的局部性。

                  從JVM來看,操作對象作為On-PID-on-heap(一般的Java)或者On-PID-off-heap(通過Unsafe或者NIO到Linux mmap(2)的橋梁)。無論在On-PID-on-heap還是在On-PID-off-heap,所有的操作對象仍然存活在用戶的地址空間。在C/C++中,API(操作系統調用的)提供了允許C++操作對象有 Off-PID-off-heap的地方,這些操作對象都寄存在內核地址空間內。

                  OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

                  (點擊圖片可以放大)

                  根據上面這個圖,分為以下六個方面:

                  #1. 為了更好地落實圖中的流程,我們可以先遵從JavaBean的規范,把PID 1定義成BondVOInterface。我們想要證明(以下的編號是上圖中流程的編號)如何操作 Map<String,BondVOInterface> ,并把非堆存儲的優勢標注起來。

                  來自GitHub:

                  public interface BondVOInterface {
                      /* add support for entry based locking */
                      void busyLockEntry() throws InterruptedException;
                      void unlockEntry();
                      long getIssueDate();
                      void setIssueDate(long issueDate); /* time in millis */
                      long getMaturityDate();
                      void setMaturityDate(long maturityDate); /* time in millis */
                      double getCoupon();
                      void setCoupon(double coupon);
                      // OpenHFT Off-Heap array[ ] processing notice ‘At’ suffix
                      void setMarketPxIntraDayHistoryAt(@MaxSize(7) int tradingDayHour, MarketPx mPx);
                      /* 7 Hours in the Trading Day:
                      * index_0 = 9.30am,
                      * index_1 = 10.30am,
                      …,
                      * index_6 = 4.30pm
                      */
                      MarketPx getMarketPxIntraDayHistoryAt(int tradingDayHour);
                      /* nested interface - empowering an Off-Heap hierarchical “TIER of prices”
                      as array[ ] value */
                      interface MarketPx {
                             double getCallPx();
                             void setCallPx(double px);
                             double getParPx();
                             void setParPx(double px);
                             double getMaturityPx();
                             void setMaturityPx(double px);
                             double getBidPx();
                             void setBidPx(double px); 
                             double getAskPx();
                             void setAskPx(double px); 
                             String getSymbol();
                             void setSymbol(String symbol); 
                      }
                  }

                  PID 1(在上圖所示的 step#1,使用了Interface)調用OpenHFT的 SharedHashMap工廠類,如下所示:

                  SharedHashMap<String, BondVOInterface> shm = new SharedHashMapBuilder()
                      .generatedValueType(true)
                      .entrySize(512)
                      .create(
                              new File("/dev/shm/myBondPortfolioSHM"),
                              String.class,
                              BondVOInterface.class
                      );
                  BondVOInterface bondVO = DataValueClasses.newDirectReference(BondVOInterface.class);
                  shm.acquireUsing("369604103", bondVO);
                  bondVO.setIssueDate(parseYYYYMMDD("20130915"));
                  bondVO.setMaturityDate(parseYYYYMMDD( "20140915"));
                  bondVO.setCoupon(5.0 / 100); // 5.0%
                  BondVOInterface.MarketPx mpx930 = bondVO.getMarketPxIntraDayHistoryAt(0);
                  mpx930.setAskPx(109.2);
                  mpx930.setBidPx(106.9);
                  BondVOInterface.MarketPx mpx1030 = bondVO.getMarketPxIntraDayHistoryAt(1);
                  mpx1030.setAskPx(109.7);
                  mpx1030.setBidPx(107.6);

                  現在,OpenHFT從堆存儲到非堆存儲的魔法要開始了!看仔細了……瀏覽完整篇文章后你就會發現,每次提到的“魔法”意味著這里是最值得“觀光”的時刻:

                  #2. 在每個運行的進程中調用上面所提到的OpenHFT,編譯BondVOInterface&pound;native 內部的實現,使得更好地控制字節尋址運算,并完成非堆存儲中的abstractAccess()/abstractMutate()操作設置(使用接口getXX()/setXX()并遵循JavaBean的命名規范)。OpenHFT運行時采用你的接口并在一個實現類中編譯,這將為你提供了更為明確的非堆存儲性能的橋梁。數組被封裝成帶有索引的getter和setter,數組的接口也以同樣的方式生成一個外部接口。數組的setter和getter的命名方式為 setXxxxAt(int index, Type t); and getXxxxAt(int index); (注:數組的gettr/settr的命名均以“At”后綴結尾)。

                  所有細節已經通過OpenHFT JIT編譯器中的一個正在運行的進程來向你描述清楚了。你現在只要做的就是提供接口。是不是很酷啊?

                  #3. PID 1調用OpenHFT API中的shm.put(K, V); 通過鍵值 (V = BondVOInterface) 把數據塞進非堆存儲的SHM中。這樣我們可以跨越在[2]中建立的OpenHFT的橋梁。

                  我們現在就是非堆存儲啦!真是太神奇了,不是嗎? :-)

                  OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

                  我們接下來看看如何從PID 2到達這一步。

                  #4. 一旦PID 1已經完成了把它的數據塞進非堆存儲SHM中,PID 2才開始調用完全相同的OpenHFT工廠類,如下所示:

                  SharedHashMap<String, BondVOInterface> shmB = new SharedHashMapBuilder()
                      .generatedValueType(true)
                      .entrySize(512)
                      .create(
                             new File("/dev/shm/myBondPortfolioSHM"),
                             String.class,
                             BondVOInterface.class
                       );

                  開始它橫跨OpenHFT所構建的橋梁的旅程。當然,假設PID 1和PID 2是存在于同一臺本地機子的操作系統中,共享文件夾 /dev/shm (同樣具有優先訪問 /dev/shm/myBondPortfolioSHM 文件夾權限).

                  #5. PID 2 調用V = shm.get(K); (每次都會創建一個新的非堆存儲的引用) 或者 PID 2 調用 V2 = shm.getUsing(K, V); 來根據你的選擇重新引用一個非堆存儲 (或者當 K 不是一個Entry 的時候返回NULL)。在OpenHFT的API中,還有第三種 獲取 簽名的方式提供給 PID 2: V2 = acquireUsing(K,V); 其中的差別是,K 必須不能是一個 Entry, 這樣你就將不會返回NULL - 但是 - 相應的你 將會返回一個新提供的非NULL V2的占位符引用 。這個引用允許 PID 2 適當地去操作位于SHM的非堆存儲 V2 Entry

                  注: 每當 PID 2 調用 V = shm.get(K);  它就會返回一個新的非堆存儲的引用。這就使得你每次為這些數據創建引用的時候產生了很多垃圾,直到你銷毀它。 然而, 當 PID2 調用 V2 = shm.getUsing(K, V); 或者 V2 = shm.acquireUsing(K, V);, 非堆存儲的引用就被移到新鍵值的位置上,而這個操作會減少GC,因為你已經自己在回收每一個東西了。

                  注:在這個地方是沒有拷貝發生的,只有在數據位于非堆存儲空間才會被設置或者改變。

                   BondVOInterface bondVOB = shmB.get("369604103");
                   assertEquals(5.0 / 100, bondVOB.getCoupon(), 0.0); 
                   BondVOInterface.MarketPx mpx930B = bondVOB.getMarketPxIntraDayHistoryAt(0);
                   assertEquals(109.2, mpx930B.getAskPx(), 0.0);
                   assertEquals(106.9, mpx930B.getBidPx(), 0.0);
                   BondVOInterface.MarketPx mpx1030B = bondVOB.getMarketPxIntraDayHistoryAt(1);
                   assertEquals(109.7, mpx1030B.getAskPx(), 0.0);
                   assertEquals(107.6, mpx1030B.getBidPx(), 0.0);

                  #6. 一個非堆存儲的記錄是一個為了非堆存儲操縱和偏移而包裝成字節的引用。通過改變這兩個,只要是你選擇的接口,內存的任何角落都能被訪問。當PID 2操作'shm'的引用,設置了正確的字節和偏移,讀取存儲在/dev/shm文件視圖的哈希映射來進行運算。在getUsing()返回后,偏移量的計算是微乎其微的,也是內聯的。即一旦代碼經過JITed編譯的,get()和set()方法就會變成簡單的機器代碼指令來訪問這些字段,只有在你訪問的字段是可讀或者可寫的,這才是真正意義上的零拷貝!非常棒!

                    //ZERO-COPY
                    // our reusable, mutable off heap reference, generated from the interface.
                    BondVOInterface bondZC = DataValueClasses.newDirectReference(BondVOInterface.class);
                    // lookup the key and give me my reference to the data if it exists.
                    if (shm.getUsing("369604103", bondZC) != null) {
                        // found a key and bondZC has been set
                        // get directly without touching the rest of the record.
                        long _matDate = bondZC.getMaturityDate();
                        // write just this field, again we need to assume we are the only writer.
                        bondZC.setMaturityDate(parseYYYYMMDD("20440315"));
                        //demo of how to do OpenHFT off-heap array[ ] processing
                        int tradingHour = 2; //current trading hour intra-day
                        BondVOInterface.MarketPx mktPx = bondZC.getMarketPxIntraDayHistoryAt(tradingHour);
                        if (mktPx.getCallPx() < 103.50) {
                            mktPx.setParPx(100.50);
                            mktPx.setAskPx(102.00);
                            mktPx.setBidPx(99.00);
                            // setMarketPxIntraDayHistoryAt is not needed as we are using zero copy,
                            // the original has been changed.
                        }
                    }
                    // bondZC will be full of default values and zero length string the first time. 
                    // from this point, all operations are completely record/entry local,
                    // no other resource is involved.
                    // now perform thread safe operations on my reference
                    bondZC.addAtomicMaturityDate(16 * 24 * 3600 * 1000L); //20440331
                    bondZC.addAtomicCoupon(-1 * bondZC.getCoupon()); //MT-safe! now a Zero Coupon Bond.
                    // say I need to do something more complicated
                    // set the Threads getId() to match the process id of the thread.
                    AffinitySupport.setThreadId();
                    bondZC.busyLockEntry();
                    try {
                        String str = bondZC.getSymbol();
                        if (str.equals("IBM_HY_2044"))
                            bondZC.setSymbol("OPENHFT_IG_2044");
                    } finally {
                        bondZC.unlockEntry();
                  }

                  意識到全局的OpenHFT的堆存儲←→非堆存儲的魔法會在以上的圖表中發生是非常重要的。

                  事實上,在第#6步,OpenHFT SHM的實現是在運行時攔截arg-2,重載 V2 = shm.getUsing(K, V);本質上,SHM實現的是查詢:

                  (
                    ( arg2 instanceof Byteable ) ?
                         ZERO_COPY :
                         COPY
                  )

                  以及作為零拷貝來執行(通過更新引用),來取代完整拷貝(通過Externalizable)。

                  如何實現非堆存儲引用功能的關鍵接口是Byteable。這是允許引用(重新)分配的。

                  public interface Byteable {
                       void bytes(Bytes bytes, long offset);
                  }

                  如果你實現了你自己的類去支持這個方法,你可以實現或者生成屬于你自己的Byteable的類。

                  直到現在,如之前我們所提到的,你可能已經禁不住覺得“這一切發生得太奇妙了”。這所有的魔法就在應用程序執行的進程中發生了!使用運行編譯器作為 BondVOInterface 接口的輸入,OpenHFT內部確定了接口的源碼并編譯該源碼(同樣的,是在進程中)到OpenHFT的實現類。如果你不想在運行過程中才生成該類,你可以提前生成并在構建的時候編譯它。OpenHFT內部會重新加載新的實現類到可運行的上下文中。接下來,運行時再物理執行 BondVOInterface&pound;native 內部類生成的方法去影響映射到非堆存儲的 Bytes[]記錄上的零拷貝操作器的容量。這個容量是零拷貝的,所以你將在一個線程上執行線程安全操作,這對另一個線程是可見的,盡管另一個線程是在別的進程中。

                  你可以看到OpenHFT SHM奇跡之處:Java現在擁有真正意義上的零拷貝IPC。

                  嘛哩嘛哩哄!

                  性能結果: CHM vs.SHM

                  On Linux 13.10, i7-3970X CPU @ 3.50GHz, hex core, 32 GB of memory.

                  SharedHashMap -verbose:gc -Xmx64m

                  OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

                  ConcurrentHashMap -verbose:gc -Xmx30g

                  OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

                  當然,主要導致CHM低于SHM438%的原因是,CHM要忍受21.8秒的STW GC事件。但是從SLA的角度來看,導致這個的原因(并沒有針對此進行補救)是無關緊要的。對于SLA來說,CHM只是慢了438%。而從SLA的角度來看,CHM的性能是難以忍受的慢。

                  JSR-107的自適性:SHM為(100%可互操作)非堆存儲的JCACHE運算對象

                  Java Community Process將在2014年第二季度宣布JCACHE所發布的JSR-107 EG標準規范——Java緩存標準API/SPI。 JCACHE 將會為Java緩存社區做那些過去JDBC為Java RDBMS社區做的事情。在JCACHE的核心和基礎是它原始的緩存操作接口 javax.cache.Cache<K,V>。如果仔細看Cache API就會發現Cache非常接近Map的超集(只有一點點學術上的差別)。JCACHE其中一個主要目的是幫助實現可擴展的(向上和向外擴展)解決 Java數據局部性、延遲和緩存的問題。 所以,如果JCACHE的中央運算對象是一個Map,而JCACHE其中一個主要的任務是解決數據局部性/延遲性的問題,那使用OpenHFT的非堆存儲SHM作為實現JCACHE的主要運算對象的接口究竟適不適合呢?對于某些Java高速緩存的使用情況, OpenHFT的非堆存儲SHM是非常完美的。

                  在這里稍等片刻(請留個位)這篇文章將分享如何把OpenHFT SHM完全作為一個JSR-107可互操作的非堆存儲JCACHE運算對象。在這之前,我們想陳述這個javax.cache.Cache接口是java.util.Map接口的超集事實。我們需要確切地知道“一個超集有多大?”……因為這會影響到我們究竟需要做多少工作才完全達到100%河100%完全適應SHM作為實現接口。

                  - 什么是Cache必須提供,而基礎的HashMap不提供?

                  • Eviction, Expiration

                    </li>

                  • WeakRef, StrongRef (順便提一下,和非堆存儲Cache的實現類無關)

                    </li>

                  • 本地角色 (例如 Hibernate L2)

                    </li>

                  • EntryProcessors

                    </li>

                  • ACID事務處理

                    </li>

                  • 事件監聽

                    </li>

                  • “Read Through” 操作器 (同步/異步)

                    </li>

                  • “Write Behind” 操作器 (同步/異步)

                    </li>

                  • JGRID 參與(JSR-347)

                    </li>

                  • JPA 參與

                    </li> </ul>

                    - OpenHFT+Infinispan “聯姻日” 計劃 (JCACHE慶典)

                    下圖描述的是開發工作范圍內的一小部分,它將采取社區主導OpenHFT程序員去適應/貢獻OpenHFT非堆存儲SHM作為完整的JSR-107可互操作JCACHE運算對象(社區主導開源JCACHE 提供方=RedHat Infinispan)。

                    OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

                    (點擊圖片放大)

                    總結:非堆存儲Hashmap...今天,明天,“直到永遠”

                    在這接近“最后一站”,我們希望在離別前給大家講個寓言故事作參考。

                    社區主導開源非堆存儲HashMap供應者和JCACHE提供的供應商(包括私有和開源)之間的關系可以協同作用的。每一塊都為使得最終用戶的非堆存儲經驗更豐富發揮著重要作用。非堆存儲Hash-Map提供者可以實現非堆存儲HashMap的核心(如JCACHE)運算對象。JCACHE供應商(包括私有和開源)可以使運算對象適應到他們產品中,然后提供核心JCACHE運算符(和基礎結構)。

                    這種關系就像奶牛(奶農,如果你愿意,核心運算對象制造方=牛奶)與乳業公司(牛奶經營者操作符 set={巴氏滅菌,脫脂,1%,2%,一半一半,等等})之間的關系。總之,(奶牛,乳業公司)是共同生產一個產品,使得最終用戶能夠享受到高于(奶牛,乳業公司)不合作而得到的。最終用戶是需要他們兩個的。

                    但是,“買家要擔心”這句話要送給最終用戶:

                    有的人應該會遭遇私有廠商傳播閉源非堆存儲HashMap/Cache的解決方案,聲稱他們閉源非堆存儲的運算對象在某方面“勝于”開源和社區主導的方法。好吧,只要記住這一點:

                    乳業公司并不生產牛奶。奶牛可以產奶。

                    奶牛使得牛奶變得公開化,24/7,以及完全不感到厭煩的焦點。乳業公司可以使得牛奶變得更多元化(一半一半,2%,1%,脫脂)……所以他們只是有機會發揮重要作用……但他們并不產奶。眼下開源“奶牛”是制造非堆存儲HashMap的“牛奶”。如果私有解決方案提供商可以使得那個牛奶更多元化,那就去做吧,這種努力的嘗試也是大家所希望看到的。但是并不鼓勵這些提供商試圖去宣稱他們私有的牛奶是在各方面都更勝一籌的“牛奶”。因為奶牛才是制造最好的牛奶。

                    最后,Java開始融入高性能計算社區是一件多么值得興奮的事啊。一切都會有很大的變化,而所有的變化都是好的。

                    并發包, 從越來越多優秀的 現代 GC 解決方案, 從 非阻塞 I/O 性能, 從 套接字直接協議的 native RDMA, JVM 內部方法, …. , 所有的方法都是通向本地 Caching, OpenHFT的SHM作為 原生 IPC 傳輸, 以及 機器級別HTM-助手功能 都被要求在這個OpenJDK非堆存儲JEP,有一點是清楚的:OpenJDK的平臺社區確實是把提高性能的優先級放在最高。 have a high-priority to improve performance.

                    再來看看這個可愛的家伙HashMap現在能做的!OpenJDK,OpenHFT,Linux和非堆存儲HashMap現在在“低處”是朋友了(即本地操作系統)。

                    OpenJDK 和 HashMap,大量數據處理時,避免垃圾回收延遲的技巧(off-heap)

                    保護好現在不受STW GC干擾,HashMap現在作為一個重要的HPC數據結構運算對象而重生。永葆年輕,HashMap...永遠年輕!

                    感謝和我們一起共同履行,我們希望你會欣賞這次的旅程。下次再見。

                     本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
                     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
                     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
sesese色