東方龍馬 | 慎用java.lang.ref.SoftReference實現緩存
發表時間 2017-09-18 16:21 來源 網絡

  在JVM內部實現緩存容器,東方龍馬認為最麻煩的事情是要對緩存大小進行控制。為何這樣說?當我們緩存的是一些值對象(ValueObject)時,一個難點是計算這一些對象(及對象引用的大小)。JVM的API并沒有賦予我們通過簡單的調用即可獲得對象(及其引用)大小的能力。當然,你可以通過ObjectOutputStream又或者自定義的方式將對象轉換成二進制數據[bytes],從而做到精確控制緩存占用的內存,但是帶來的一個問題是對象的序列化與反序列化帶來的開銷。

  JVM的Reference(java.lang.ref.Reference:Since JDK1.2)的出現似乎給開發者帶來了美好的前景。關于Java編程中的引用,粗略介紹如下:

  1.強引用

  這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當內存空 間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。

  強引用的例子:方法局部變量、JNI變量、類變量,概括起來,就是所有GC Root引用可達的都是強引用;

  2.軟引用(SoftReference)

  如果一個對象只具有軟引用,那就類似于可有可無的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。

  軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

  3.弱引用(WeakReference)

  如果一個對象只具有弱引用,那就類似于可有可無的生活用品。 弱引用與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它 所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。

  弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

  4.虛引用(PhantomReference)

  "虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

東方龍馬LOGO貼圖15

  虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在于:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃 圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是 否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采 取必要的行動。

  實際上,虛引用的get,總是返回null。

  java.lang.ref這個包(特別是java.lang.ref.SoftReference)似乎把開發者從繁瑣的以及容易出問題的內存管理中解放了出來:既不擔心在內存消耗過多時如何快速地釋放內存,也不擔心緩存管理不當帶來的內存泄漏,事實真是如此么?讓我們來看一個實際的案例。

  某用戶使用Gerrit2作為其代碼管理的工具。系統運維工程師反映,近期系統在運行過程中頻繁出現性能問題,最終用戶使用系統時經常出現掛起(無響應)。運行環境如下:

  OS:Linux

  中間件:Gerrit2

  JDK:Sun JDK1.8_0_x

  JVM Heap分配:16G/32G

接到這個問題,遵循既定的思路,讓用戶做一定的準備,調整JVM的參數捕獲故障時的現場信息進行問題分析。最后定位為JVM Heap頻繁的Full GC問題導致應用出現性能故障,參考如下:

  JVM GC日志顯示,每一次GC以后,JVM Heap空閑的空間仍然有1GB以上的空間可用;

  但是有Overhead為100%的GC情況;

  分析GC Completed以及Overhead情況,在接近故障點時,有明顯的GC頻繁及GC時間上升(峰值5923ms);

  原始的JVM GC日志顯示,在故障時間點附近,有非常頻繁的Full GC,觸發的原因為JVM Old區滿,并且每次Full GC后,Old區能釋放出來的空閑空間相當少;但是整個JVM總計的空閑Heap仍然有1GB以上的空間。

  性能問題原因:JVM Old區滿,頻繁的Full GC導致應用性能下降非常嚴重;

  附注:

  GC Completed or GC :Time(millisecond) spent during garbage collection.

  Overhead: Ratio(%) time spent in allocation failure vs. time between AF

  繼續深入分析問題,我們發現了內存中存在的大對象:

  Class Name | Shallow Heap | Retained Heap

  ---------------------------------------------------------------------------------------------------

  org.eclipse.jgit.internal.storage.file.WindowCache @ 0x7ff59077b508| 104 | 20,638,034,208

  ---------------------------------------------------------------------------------------------------

  Type |Name |Value

  -------------------------------------------------------------------------------------------------------

  ref |openBytes |20382985278

  ref |openFiles |1859

  int |windowSize |8192

  int |windowSizeShift|13

  boolean|mmap |false

  long |maxBytes |10485760

  int |maxFiles |16384

  int |evictBatch |64

  ref |evictLock |java.util.concurrent.locks.ReentrantLock @ 0x7ff590c04510

  ref |locks |org.eclipse.jgit.internal.storage.file.WindowCache$Lock[16384] @ 0x7ff590e9c7c0

  ref |table |java.util.concurrent.atomic.AtomicReferenceArray @ 0x7ff59077b5c0

  ref |clock |95846830

  int |tableSize |3200

  ref |queue |java.lang.ref.ReferenceQueue @ 0x7ff59077b570

  -------------------------------------------------------------------------------------------------------

  Class Name | Shallow Heap | Retained Heap

  ------------------------------------------------------------------------------------------------------

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf48e46a0| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf47ba558| 48 | 48

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf478bff0| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf478bf40| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf478be90| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf473ef90| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf473eee0| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf473ee30| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf473b980| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf4736210| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf47344e0| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf47343d0| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf4727498| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf46640d0| 48 | 8,264

  org.eclipse.jgit.internal.storage.file.ByteArrayWindow @ 0x7ffbf4664020| 48 | 8,264

  Total: 15 of 2,488,602 entries; 2,488,587 more | |

  ------------------------------------------------------------------------------------------------------

  評析:

  Class Name | Shallow Heap | Retained Heap

  -----------------------------------------------------------------------------------------------------

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf42d39e0| 112 | 6,312

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf3999e48| 112 | 5,752

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf385dd28| 112 | 264

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf27e1c20| 112 | 12,504

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf148de08| 112 | 10,048

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbf0b97010| 112 | 12,240

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbef2869e0| 112 | 9,352

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbeee8bc50| 112 | 41,408

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbeee26698| 112 | 10,000

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbec1c1318| 112 | 9,888

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbec1ba1a0| 112 | 9,920

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbeb619898| 112 | 47,144

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbe94a62a0| 112 | 11,696

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbe90dd688| 112 | 9,080

  org.eclipse.jgit.internal.storage.file.FileRepository @ 0x7ffbe56b3f88| 112 | 12,344

  Total: 15 of 3,379 entries; 3,364 more | |

  -----------------------------------------------------------------------------------------------------

  評析:

  。

  Class Name | Shallow Heap | Retained Heap

  -----------------------------------------------------------------------------------------------

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff593248670| 128 | 168,684,904

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5ca5e57e0| 128 | 163,743,112

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff65d2797c8| 128 | 130,335,888

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff67ed5a5a0| 128 | 116,092,248

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5d36b1350| 128 | 111,606,864

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff741d9c980| 128 | 92,786,784

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5c56577d0| 128 | 55,945,608

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5d4cb7ed0| 128 | 31,806,712

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5e3ec9c60| 128 | 26,108,840

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff593a07f80| 128 | 21,771,144

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5923c0150| 128 | 20,065,688

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5b7dd8768| 128 | 17,462,328

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff5d74ec5c0| 128 | 16,689,600

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff65327b220| 128 | 15,634,496

  org.eclipse.jgit.internal.storage.file.PackFile @ 0x7ff677da56e0| 128 | 13,699,608

  Total: 15 of 6,459 entries; 6,444 more | |

  -----------------------------------------------------------------------------------------------

  org.eclipse.jgit.internal.storage.file.WindowCache.openBytes接近20G,org.eclipse.jgit.internal.storage.file.ByteArrayWindow對象實例達2,488,602個,每個8K,總計19,908,816KB(20,386,627,584Byte)。org.eclipse.jgit.internal.storage.file.FileRepository對象實例3,379個,org.eclipse.jgit.internal.storage.file.PackFile對象實例6,459個。

  問題來到這里基本上就清晰了:JGit4.1 org.eclipse.jgit.lib.RepositoryCache以及org.eclipse.jgit.internal.storage.file.WindowCache緩存的PackFile以及ByteArrayWindow占用了大片的內存空間。緩存占用了大片Old區的內存,并且觸發了頻繁的Full GC導致性能問題的發生。開始的時侯,筆者也犯了一個同樣膚淺的錯誤,建議客戶通過增大JVM Heap對問題進行緩解,但最終的結果是:服務器發生問題的頻率比設置32G的時侯更頻繁;

  筆者嘗試分析一下緩存的機制,容器組件RepositoryCache以及WindowCache 其使用的是正是java.lang.ref.SoftReference對緩存對象進行引用。并且,RepositoryCache組件沒有緩存消耗機制(例如緩存的對象的數量或者緩存總計大小),而WindowCache組件雖然有控制緩存文件數量及總計內存大小,但是最終的結果與實際想要控制的差距太大,并未如設想那樣有效地控制內存消耗。

  既然程序是使用java.lang.ref.SoftReference保持對緩存對象的引用,參考原來Sun的說法,如果一個對象只有軟引用可達,在內存不足時,是可以被回收的,那關鍵的問題是JVM的GC如何判定這個SoftReference引用的對象何時被回收?

  通過Google大神,東方龍馬終于找到相關參考的文章,以下為原文參考:

  對于java.lang.ref.SoftReference對象,有一個全局的變量clock(實際上就是java.lang.ref.SoftReference的類變量clock,如下圖代碼所示):其保持了最后一次GC的時間點(以毫秒為單位),即每一次GC發生時,該值均會被重新設置。 同時,java.lang.ref.SoftReference對象實例均有一個timestamp的屬性,其被設置為最后一次成功通過SoftReference對象獲取其引用對象時的clock的值(最后一次GC)。所以,java.lang.ref.SoftReference對象實例的timestamp屬性,保持的是這個對象被訪問時的最后一次GC的時間戳;

  當GC發生時,以下兩個因素影響SoftReference引用的對象是否被回收:

  1、SoftReference 對象實例的timestamp有多舊;

  2、內存空閑空間的大小;

  是否保留SoftReference引用對象的判斷參考表達式,true為不回收,false 為回收:

  interval<=free_heap*ms_per_mb

  說明:

  interval:最后一次GC時間和SoftReference對象實例timestamp的屬性的差。簡單理解就是這個SoftReference引用對象的生存的時長;

  free_heap:JVM Heap中空閑空間大小,單位為MB

  ms_per_mb:每1M空閑空間可保持的SoftReference對象生存的時長(單位毫秒)。簡單地將這個參數理解為一個常量就好,默認值是1000;Sun JVM可以通過參數:-XX:SoftRefLRUPolicyMSPerMB進行設置;

  東方龍馬上述的判斷簡單地理解就是:如果SoftReference引用對象的生存時長<=空閑內存可保持軟引用的最大時間范圍,則不清除SoftReference所引用的對象;否則,則將其清除;

  舉例:有一個SoftReference,其屬性timestamp值為2000,最后一次GC clock值為5000,ms_per_mb值為1000,并且空閑空間為1MB,那么表達式:

  5000-2000<=1000*1

  上述表達式返回值為false(3000>1000),因此,這個SoftReference所引用的對象,會被GC所回收;

  如果此時我們有4MB的空閑內存,那么這個表達式:

  5000-2000<=1000*4

  上述表達式返回值為true(3000<4000),因此,這個SoftReference所引用的對象,不會被GC所回收;

  需要注意的是,JVM總是保留GC以后訪問過的SoftReference引用的對象。為何?因為GC以后訪問過的對象,clock-timestamp總是等于0,即使你通過參數-XX:SoftRefLRUPolicyMSPerMB設置ms_per_mb=0,表達式interval<=free_heap*ms_per_mb總是返回true,所以得出上述的結論;

  參考上述的理論,我們大概可以估算一下當一個對象僅有SoftReference引用可達時,其最大生命的周期情況:

  SoftRefLRUPolicyMSPerMB:1000ms(默認值)

  空閑空間 清理間隔(生存周期上限)

  1M: 1S

  10M: 10S

  100M: 100S

  1000M 1000S

  SoftRefLRUPolicyMSPerMB:100ms

  空閑空間 清理間隔(生存周期上限)

  1M 0.1S

  10M 1S

  100M 10S

  1000M 100S

  SoftRefLRUPolicyMSPerMB:10ms

  空閑空間 清理間隔(生存周期上限)

  1M 0.01S

  10M 0.1S

  100M 1S

  1000M 10S

  10000M 100S

  SoftRefLRUPolicyMSPerMB:5ms

  空閑空間 清理間隔(生存周期上限)

  2M 0.01S

  20M 0.1S

  200M 1S

  2000M 10S

  20000M 100S

  SoftRefLRUPolicyMSPerMB:1ms

  空閑空間 清理間隔(生存周期上限)

  1M 0.001S

  10M 0.01S

  100M 0.1S

  1000M 1S

  10000M 10S

  至此,對于上述案例的故障成因,東方龍馬有了一個更深層次的認識:

  設置較大的JVM Heap時,因為Sun的New Generation與Old Generation比例關系,每一次GC以后,New Generation釋放出來的空閑空間的數量,總是使SoftReference引用的對象的生存周期保持在一個較大的值,換言而之,其淘汰的速度較慢。而Old Generation滿頻繁觸發的Full GC以及內存碎片整理,使得整個JVM非常卡頓;

  而設置更大的JVM Heap后,使得每一次GC以后,New Generation釋放出來的空閑空間的數量更多,從而加劇了這種故障的情況;

  當然,故障的根本成因,是應用程序代碼并未對緩存進行控制;

  上述案例,在未改動代碼及結構的情況下,通過增大大JVM Heap,以及通過設置參數:-XX:SoftRefLRUPolicyMSPerMB=0解決;

  其它:IBM的JVM針對SoftReference的回收控制,同樣有類似參數:-Xsoftrefthreshold進行控制。以下是關于-Xsoftrefthreshold的描述:

  Sets the number of GCs after which a soft reference will be cleared if its referent has not been marked. The default is 32, meaning that on the 32nd GC where the referent is not marked the soft reference will be cleared.

  結束語:

  JVM的Reference(java.lang.ref.Reference:Since JDK1.2)并未像其描述的那樣美好,特別是java.lang.ref.SoftReference的使用。同樣地,即使是使用Reference實現In-Box的緩存,也需要充分考慮其對內存的消耗。這樣才使我們的應用運行得更穩定。

  東方龍馬憑借在數據庫,中間件領域耕耘20余年,希望我們的寶貴經驗和獨到見解可以幫助到你。

東方龍馬-商務素材2
下一篇:沒有了
篮球小说