OpenJDK與HashMap……放心地教這個老傢伙一些新(非堆!)技巧
OpenJDK的非堆JDK增強提議(JDK Enhancement-Proposal,JEP)試圖標準化一項基礎設施,它從Java6開始,只能在HotSpot和OpenJDK內部使用。這種設施能夠像管理堆記憶體那樣管理非堆記憶體,同時避免了使用堆記憶體所帶來的一些限制。對於上百萬短期存在的物件/值來說,堆記憶體工作起來是很好的,但是如果你想要增加一些其他的需求,如幾十億的物件/值的話,假若你想避免持續增加的GC暫停,那麼你需要做一些更加有創造性的工作。在有些場景下,你還需要完全避免暫停。非堆提供了構建“arenas”記憶體儲存的功能,它遵循自己的規則,並不會影響到GC的暫停時間。兩個很容易使用arenas的集合是Queue和HashMap,因為它們具有很簡單的物件生命週期,所以編寫自己的垃圾收集並不太繁瑣。這種集合所帶來的好處就是它的大小能夠比傳統的堆集合大得多,甚至超過主儲存器(main memory)的規模,而對暫停時間的影響卻微乎其微。相比之下,如果你的堆大小超過了主儲存器,那麼你的機器就會變得不可用,可能會需要關電源重啟。
本文將會調查這個JEP的影響,它會讓大家熟悉的Java HashMap具備新的非堆功能。簡而言之,這個JEP所具有的魔法能夠“教會”HashMap(這是一個可愛的老傢伙old dog)一些新的技巧。這個JEP會要求將來的OpenJDK釋出版本與傳統Java平臺的優先順序產生很大的差異:
- 將sun.misc.Unsafe中有用的部分重構為一個新的API包
- 提倡使用新的API包在非堆的原生記憶體操作物件上直接進行高效能的原生記憶體操作。
- (通過新的API)提供外部功能介面(Foreign Function Interface,FFI)來橋接Java與作業系統資源(Operating System resource)和系統呼叫(system call)。
- 移除FUD(坦率的說,這是一種技術上的偏執),它與使用非堆程式設計策略來實現Java效能的提升有關。最終,基本明確的是這個JEP要求OpenJDK平臺要開放性地將其納為主流,它曾經被視為黑暗的工藝、非堆參與者的祕密組織。
本文力圖(以一種通俗和溫和的方式)讓所有感興趣的Java開發人員都能有所收穫。作者希望即使是新手也能完整地享受本文所帶來的這段旅程,儘管在路途上可能會有一些不熟悉的“坑坑窪窪”,但是不要氣餒——希望您在位置上安坐直到文章結束。本文會提供一個有關歷史問題的上下文,這樣你會對下面的問題具備足夠的背景知識:
-
那麼,讓我們開始這段旅程吧。需要記住的一點是在Java之前,雜湊表(hash table)是在原生記憶體堆中實現的,比如說在C和C++中。在一定程度上可以說,重新介紹非堆儲存是“老調重彈”,這是大多數當前的開發人員所不知道的。在許多方面可以說,這是一趟“回到未來”的旅行,因此享受這個過程吧!
- 為了應對這些問題,歷史上所給出方案的成功/失敗之處是什麼?
- 在堆HashMap的使用場景中,依然存在的未解決問題是什麼?
- 新JEP所提供的功能能夠帶來什麼助益(也就是將HashMap變為非堆的)?
- 對於非堆JEP所沒有解決的問題,將來的JEP能夠給我們什麼期待呢?
OpenJDK非堆JEP
針對非堆JEP,已經有了幾個提議(submission)。下面的樣例展現了支援非堆記憶體的最小需求。其他的提議嘗試提供sun.misc.Unsafe的替代品,這個類是目前的非堆功能所需要的。它們還包含了很多其他有用和有趣的功能。
JEP概述:建立sun.misc.Unsafe部分功能的替代品,這樣就沒有必要再去直接使用這個庫了。
目標:移除對內部類的訪問。
非目標:不支援廢棄(deprecated)的方法,也不支援Unsafe尚未實現的方法。
成功指標:實現與Unsafe和FileDispatcherImpl相同的核心功能,並且效能方面要與之保持一致。
驅動力:目前來講,Unsafe是構建大規模、執行緒安全的非堆資料結構的唯一方法。在如下的領域,這種方式會很有用,如最小化GC的影響、跨程序共享記憶體以及在不使用C和JNI的情況下實現嵌入式資料庫,因為使用C和JNI的話,可能會更慢並且更加困難。FileDispatcherImpl目前需要將記憶體對映為任意的大小。(標準API限制為小於2GB。)
描述:為非堆記憶體提供一個包裝類(類似於ByteBuffer),但是具有如下的功能增強。
- 64位的大小和偏移。
- 執行緒安全結構,如volatile和順序訪問、比較和交換(compare and swap,CAS)操作。
- JVM優化的邊界檢查,或開發人員控制邊界檢查。(提供的安全設定允許這樣做)
- 在一個緩衝區中,能夠為不同的記錄重用部分緩衝區。
- 能夠將非堆的資料結構對映到這樣一個緩衝區之中,在這個過程中,邊界檢查已經被優化掉了。
要保留的核心功能:
- 支援記憶體對映檔案
- 支援NIO
- 支援將寫操作提交到磁碟上。
替代方案:直接使用sun.misc.Unsafe。
測試:測試需求應該與目前的sun.misc.Unsafe和記憶體對映檔案相同。還需要額外的測試來證明它與AtomicXxxx類一致的執行緒安全操作。AtomicXxxx類可以使用這個公開API進行重寫。
風險:有很多的開發人員在使用Unsafe,他們可能並不認同合適的替代方案是什麼。這意味著這個JEP的範圍可能會擴大,或者會建立新的JEP來涵蓋Unsafe中的其他功能。
其他JDK: NIO
相容性:需要保持向後相容的庫。這可以針對Java 7實現,如果有足夠興趣的話,也可以支援Java 6。(當撰寫本文的時候,當前的版本是Java 7)
安全性:理想情況下,安全性的風險不應該超過當前的ByteBuffer。
效能和可擴充套件性:優化邊界檢查會比較困難。可能需要為這個新的緩衝區新增更多的功能,通過通用的操作來減少損耗,如writeUTF、readUTF。
HashMap簡史
“雜湊碼(Hash Code)”這個術語最早於1953年1月出現在Computing文獻之中,H. P. Luhn(1896-1964)在編寫IBM內部備忘錄時,使用到了這個術語。Luhn試圖解決的問題是“給定一個文字格式的單詞流,要實現100%完整的(單詞、頁集)索引,最優的演算法和資料結構是什麼樣的?”
H.P. Luhn (1896-1964) |
Luhn寫到“hashcode”是基本的運算子(operator)。 Luhn寫到“關聯陣列(Associative Array)”是基本的運算物件(operand)。 術語“HashMap”(亦稱為HashTable)逐漸形成了。 注意:HashMap這個詞源自出生於1896年的電腦科學家。HashMap真的是個老傢伙了! |
讓我們將HashMap的故事從它的起始階段轉移到早期的實際使用階段,也就是從1950年代中期跳到1970年代中期。
在其1976年寫成的經典著作《演算法+資料結構=程式》之中, Niklaus Wirth討論了“演算法”,將其視為基本的“運算子”,並將“資料結構”視為基本的 “運算物件”,對於所有的計算機程式來講這都是適用的。 從那時開始,資料結構領域(HashMap、堆等)的進步是很緩慢的。在1987年,我們確實也看到了Tarjan非常重要的F-Heap突破,但是除此之外,在運算物件方面確實乏善可陳。當然需要記住的是,HashMap最早出現於1953年,已經有超過六十年的歷史了! 但是在2014年,資料結構領域可能再次會有一些重大的進展。在OpenJDK平臺方面,非堆的 HashMap是一個正在不斷髮展的資料結構。 關於HashMap的歷史,我們已經介紹了很多的內容。現在,我們開始探索一下如今的HashMap,尤其是看一下在Java中,HashMap當前的三個變種。 |
N. Wirth 1934- |
java.util.HashMap(非執行緒安全)
在真正的多執行緒(Multi-Threaded,MT)併發使用者場景下,它會快速失敗,並且每次都是如此。所有地方的程式碼必須使用Java記憶體模型(Java Memory Model,JMM)的記憶體屏障策略(如synchronized或volatile)以保證執行的順序。
會發生失敗的簡單假設場景:
- 同步寫入
- 非同步讀取
- 真正併發(2 x CPU/L1)
讓我們看一下為什麼會發生失敗……
假設Thread 1往HashMap中進行寫入,而寫入的效果只儲存在CPU 1的一級快取之中。然後,Thread 2幾秒後得以在CPU 2上繼續執行,它會讀取來自於CPU 2一級快取中的HashMap——這並不會看到Thread 1的寫入,這是因為寫入和讀取執行緒中的寫讀操作之間都沒有記憶體屏障操作,而這是共享狀態的Java記憶體模型所需要的。即便Thread 1同步寫操作,寫操作的效果重新整理到了主記憶體中,Thread 2依然看不到變化的效果,因為讀取操作來自於CPU 2的一級快取。所以,在寫入操作上的同步只能避免寫入操作的衝突。要滿足所有執行緒的記憶體屏障操作,你必須還要同步讀取。
thrSafeHM = Collections.synchronizedMap(hm) ;(粗粒度的鎖)
要使用“synchronized”達到高效能的話,競爭出現的機率要比較低。這種場景是非常常見的,因此在很多場景中,這並不會像聽上去那麼糟糕。但是,如果你要引入競爭的話(多個執行緒同時嘗試操作同一個集合),就會影響到效能了。在最壞的場景下,如果有高頻率的競爭,最終的結果可能是多個執行緒的效能甚至比不上單個執行緒的效能(沒有任何鎖定和競爭的操作)。
這是通過在所有的key上粗粒度地阻塞所有mutate()和access()操作實現的,實際上就是在所有的執行緒操作符上阻塞整個Map操作物件,只有一個執行緒可以對其進行訪問。這導致的了零多執行緒併發(Zero MT-concurrency),也就是同時只有一個執行緒在進行訪問。這種粗粒度鎖的另外一個結果是我們非常不喜歡的一個場景,被稱之為高度的鎖競爭(High Lock Contention)(參見左圖,N個執行緒在競爭一個鎖,但是必須要阻塞等待,因為這個鎖被正在執行的一個執行緒所持有)。
對於這種完全同步、非併發、isolation=SERIALIZABLE(並且總體上來說令人失望)的HashMap,幸好在我們即將到來的OpenJDK非堆JEP中有了推薦的補救措施:硬體事務性記憶體(Hardware Transactional Memory,HTM)。藉助HTM,在Java中編寫粗粒度同步阻塞將會再次變得很酷。HTM會幫助將零併發的程式碼在硬體層面轉換為真正併發且100%執行緒安全的。這會再次變得很酷,對吧?
java.util.concurrent.ConcurrentHashMap(執行緒安全、更巧妙的鎖,但是依然不“完美”)
在JDK 1.5釋出的時候,Java程式設計師發現在核心API中包含了期待已久的java.util.concurrent.ConcurrentHashMap。儘管CHM並不能成為HashMap統一的替代方案(CHM使用更多的資源,在低競爭的場景下可能並不合適),但是它確實解決了其他HashMap所不能解決的問題:實現真正的多執行緒安全和真正的多執行緒併發。讓我們畫圖來展現一下CHM能夠帶來什麼好處。
- 鎖分片
- 對於java.util.HashMap中獨立的子集有一個鎖的集合:N個hash桶/N個分段(Segment)鎖。(右側的圖中,Segments=3)
- 如果在設計時,想要將高度競爭的鎖重構為多個鎖,而又不損害資料完整性時,鎖分段是非常有用的。
- 問題:該如何同時保護整個集合?(遞迴)獲取所有的鎖?
那麼,現在你可能會問:有了ConcurrentHashMap和java.uti.concurrent包,高效能運算社群(High Performance Computing community)是否可以將Java作為程式設計平臺來構建方案以解決他們的問題呢?
非常遺憾的是,最為現實的答案依然是“時機尚未成熟”。那麼,還存在的問題到底是什麼?
CHM有一個問題是有關擴充套件性和持有中等生命週期(medium-lived)物件的。如果有少量的重要集合使用CHM的話,那麼其中有一些可能會非常大。在有些場景下,你會有大量中等存活時間的物件儲存在這樣的集合中。中等生命週期物件的問題在於它們佔用了大部分的GC暫停時間,比起短期存活(short-lived)的物件,它們的成本可能會高上20倍。長期存活的物件會位於老年代,而短期存活的物件在新生代就會死亡,但是中等生命週期的物件會經歷所有的survivor空間複製,然後在老年代死亡,這使得它們的複製和最終清理成本很高。理想情況下,你所需要的儲存資料的集合對GC的影響是零。
ConcurrentHashMap中的元素在執行時位於Java VM的堆中。CHM位於堆上,因此它是造成Stop-the-World(STW)暫停的重要因素,我們不將其稱之為最重要的因素其實也差不多。當STW GC事件發生時,所有的應用程式執行緒都會經歷“難堪的暫停”延遲。這種延遲,是由位於堆上的CHM(及其所有的元素)造成的,這是一種痛苦的體驗。這種體驗和問題是高效能運算社群所無法忍受的。
在高效能運算社群完全擁抱Java之前,必須要有一種方案馴服堆GC這個怪獸。
這個方案在理論上非常簡單:將CHM放在堆外。
當然,該方案也正是這個OpenJDK非堆JEP所要設計支援的。
在深入介紹HashMap非堆生命週期之前,讓我們看一下有關堆的細節,這些細節描述了它的不便之處。
Heap的簡史
Java堆記憶體是由作業系統分配給JVM的。所有的Java物件都是通過其堆上的JVM地址/標識來進行引用的。堆上的執行時物件引用肯定會位於兩個不同的堆區域中的某一個上。這些區域更為正式的叫法是代(generation)。具體來講:(1)新生(Young)代(包括EDEN區和兩個SURVIVOR子空間)以及(2)老年(Tenured)代。(注意:Oracle宣佈永久代將會從JDK 7開始逐漸淘汰,並會在JDK 8中完全消除掉)。所有的分代都會導致恐怖的“Stop-the-World”完整垃圾回收事件,除非你使用“無暫停(pause less)”的收集器,如Azul的Zing。
在垃圾收集的領域,操作是由“收集器”執行的,這些收集器的操作物件就是堆中的目標分代(及其子空間)。收集器會操作在堆的目標分代/空間上。垃圾收集的完整內部細節是另外一個(很大的)主題,在一篇專門的文章中進行了闡述。
就現在來說,記住這一點就夠了:如果(任意型別的)某個收集器在任何分代的堆空間上導致“Stop the World”事件,那麼這就是一個嚴重的問題。
這是一個必須要有解決方案的問題。
這是非堆JEP能夠解決的一個問題。
讓我們近距離地看一下。
Java堆的佈局:按照分代的視角
垃圾收集使得編寫程式容易了許多,但是當面臨SLA目標時,不管是寫在書面上的還是隱含的(比如Java Applet停止30秒是不能允許的),Stop-The-World暫停時間都是一個很令人頭疼的問題。這個問題非常嚴重,以至於對於很多Java開發人員來說,這是他們所面對的唯一的問題。值得一提的是,當STW不再是問題的時候,還有很多其他要解決的效能問題。
使用非堆儲存的收益在於中等生命週期物件的數量會急劇下降。它甚至還能降低短期存活物件的數量。對於高頻率的交易系統,一天之內所建立的物件可能會比Eden區還小,這意味著一天之內甚至不會觸發一次minor收集。一旦記憶體方面的壓力降低了,並且有很少的物件能夠到達老年代,那麼優化GC將會變得非常容易。通常你甚至不需要設定任何的GC引數(除了可能會增加eden的大小)。
藉助轉移到非堆上,Java應用通常可以宣告完全主宰自己的命運,也就是能夠滿足效能的SLA期待和條款。
稍等。剛才最後一句話是什麼意思?
注意:所有的乘客,請收起您的摺疊板並將座椅調至直立狀態。這是很值得重複的一句話,也是這個OpenJDK非堆JEP所解決的核心問題所在。
通過將集合(如HashMap)實現非堆,Java應用通常可以宣告完全主宰自己的命運(不再受STW GC“難堪的暫停”事件的擺佈),也就是能夠滿足效能的SLA期待和條款。
這是一個具備實用性的可選方案,在基於Java的高頻率交易系統上已經得到了應用。
對於Java來說,如果想對高效能運算社群保持持續的吸引力,這也是一個完全必要的方案。
堆的優勢
- 以熟悉的方式,很自然地編寫Java程式碼。所有有經驗的Java開發人員都能編寫這樣的程式碼。
- 安全,不必擔心記憶體訪問問題。
- 自動化的GC服務——沒有必要自己去管理malloc()/free()操作。
- 對Java鎖API和JMM的整合都完全不必再擔心。
- 沒有序列化/複製的資料要新增到結構體之中。
非堆的優勢
- 能夠將“Stop The World” GC事件控制到你認為合適的級別。
- 在擴充套件性方面(當使用堆所造成的影響足夠高的時候)要強於堆上的結構。
- 可以用做原生的IPC傳輸手段(不會有java.net.Socket的IP迴路)。
-
在分配方法上的考慮因素:
- 使用NIO DirectByteBuffer,實現到/dev/shm (tmpfs)的對映?
- 或者直接使用sun.misc.Unsafe.malloc()?
HashMap的現狀……(通過使用非堆)這個“老傢伙”能夠解決什麼新問題?
OpenHFT HugeCollections (SHM)簡介
“非堆”到底是什麼?
在下面的圖中,闡述了兩個JavaVM程序(PID1和PID2),它們試圖使用SharedHashMap(SHM)作為程序間通訊(inter-process communication,IPC)的設施。圖中底部的水平軸展現了完整的SHM OS位置分佈域。當進行操作的時候,OpenHFT物件必須要位於OS實體記憶體的使用者地址空間或者核心地址空間。繼續深入研究一下,我們知道開始的時候,它們必須是“On-Process”的位置。按照Linux OS的視角來看,JVM是一個a.out(通過呼叫gcc來生成)。當這個a.out執行時,從Linux程序內部來看,這個執行的a.out有一個PID。 PID的a.out(在執行時)有一個大家所熟知的內部構造, 包含了三個段(segment):
- 文字段(Text,低地址……程式碼執行的地方)
- 資料(Data,通過sbrk(2)實現從低地址到高地址的增長)
- 棧(從高地址向低地址增長)
這是在OS的角度來看PID。PID是一個正在執行的JVM,這個JVM對其操作物件的可能位置分佈有一個自己的視角。
按照JVM的檢視,操作物件可能位於On-PID-on-heap(正常的Java)或者On-PID-off-heap(通過Unsafe或NIO的bridge橋接到Linux mmap(2))之中。不管是On-PID-on-heap還是On-PID-off-heap,所有的操作物件依然都還是在使用者地址空間中執行。在C/C++中,有API(OS系統呼叫)能夠允許C++操作物件位於Off-PID-off-heap上。這些操作物件存在於核心地址空間上。
下面6個編號的段落對上圖進行了描述。
#1. 為了更好地闡述上圖中的流程,假設PID 1定義了一個BondVOInterface,它是符合JavaBean約定的。我們想要闡述(按照上圖中的數字順序)如何操作Map<String,BondVOInterface>,這種方式會著重強調非堆的優勢。
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(在上圖的步驟1中,使用介面)呼叫了一個OpenHFT SharedHashMap工廠,程式碼可能會像如下所示:
SharedHashMap 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);
現在,會發生一些堆 →非堆的魔法。請仔細觀察……在本文所帶給您的整個旅程中,將要分享給您的“魔法”是旅程中“最美的風景”:
#2.在執行時,每個程序呼叫上面的OpenHFT工廠方法時,會生成並編譯一個BondVOInterface£native 內部實現,它會完全負責必要的位元組位置演算法(byte addressing arithmetic),從而實現充分完整的非堆abstractAccess() / abstractMutate()操作符集合(通過該介面的getXX()/setXX()方法,這些方法符合Java Bean的方法簽名約定)。它們所造成的效果就是OpenHFT在執行時會使用你的介面並將其編譯為實現類,這個實現類會作為具體非堆功能的橋樑。陣列(array)也是類似的,會使用基於索引的getter和setter。陣列的介面也會像外層介面一樣。陣列的setter和getter方法簽名格式為setXxxxAt(int index, Type t); 和getXxxxAt(int index); (注意,‘At’字尾同時適用於陣列的getter/setter簽名)。
這是都是在執行時為你生成的,藉助於程序中的OpenHFT JIT編譯器。你所要做的就是提供介面。非常酷,對吧?
#3. PID 1然後呼叫OpenHFT的API shm.put(K, V);,從而按照Key (V = BondVOInterface),將資料寫入到非堆的SHM中。我們已經跨過了在[2]中所構建的OpenHFT橋。
我們已經實現了非堆!非常有意思吧?:-)
讓我們再從PID 2的視角看一下是怎麼做到的。
#4. 只要PID 1完成將資料放到非堆SHM之中,PID 2現在就可以呼叫完全相同的OpenHFT工廠了,如下所示:
SharedHashMap shmB = new SharedHashMapBuilder() .generatedValueType(true) .entrySize(512) .create( new File("/dev/shm/myBondPortfolioSHM"), String.class, BondVOInterface.class );
以這樣的方式,跨越了OpenHFT構造的連線橋,獲得了完全相同的非堆OpenHFT SHM引用。當然,這假設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使用的get 方法簽名:V2 = acquireUsing(K,V);,它的區別在於,如果K 不是一個Entry的話,你所得到的並不是NULL,而是會返回一個引用,這個引用指向了一個新建立的非NULL 的V2佔位符。這個引用能夠讓PID 2在合適的時候操作SHM的非堆V2 Entry。
注意:當PID 2呼叫V = shm.get(K);時,它會返回一個新的非堆引用。這會產生一些垃圾,但是在丟棄它之前,你能夠一直持有對這個資料的引用。然而,當PID 2呼叫V2 = shm.getUsing(K, V);或者V2 = shm.acquireUsing(K, V);的時候, 非堆引用轉移到了新key的位置上,這個操作跟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. 非堆記錄是一個引用,它包裝了Bytes以用來進行非堆的操作,同時還包裝了一個偏移量(offset)。通過對這兩者進行變更,記憶體中的任何區域都能夠訪問到,就如同它是你所選擇的介面那樣。當PID 2操作‘shm’引用時,它要設定正確的Bytes和偏移量,這會通過讀取儲存在/dev/shm檔案中的hash map來進行計算。在getUsing()返回後,對於偏移量的計算就會非常簡單並且是內聯執行的,也就是說,一旦程式碼被JIT之後,get()和set()方法就會變為簡單的機器碼指令,以實現對這些域的訪問。只有你所訪問的域會被讀取或寫入,真正的零複製(ZERO-COPY)!太漂亮了!
//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