java如果把字符串转成对象_Java中的重复对象:不仅仅是字符串
當(dāng)Java應(yīng)用程序消耗大量內(nèi)存時(shí),它本身就會(huì)出現(xiàn)問題,并可能導(dǎo)致GC壓力增加和GC暫停時(shí)間過長。在我之前的一篇文章中,我討論了Java中常見的內(nèi)存浪費(fèi)源:重復(fù)字符串。兩個(gè) java.lang.String 對(duì)象, a 并 b 在重復(fù)時(shí) a != b && a.equals(b)。換句話說,在JVM存儲(chǔ)器中有兩個(gè)(或更多)單獨(dú)的字符串具有相同的內(nèi)容。此問題經(jīng)常發(fā)生,尤其是在業(yè)務(wù)應(yīng)用程序中。在這樣的應(yīng)用程序中,字符串代表了許多真實(shí)世界的數(shù)據(jù),然而,相應(yīng)的數(shù)據(jù)域(例如客戶名稱,國家名稱,產(chǎn)品名稱)是有限的并且通常很小。根據(jù)我的經(jīng)驗(yàn),在未經(jīng)優(yōu)化的Java應(yīng)用程序中,重復(fù)的字符串通常會(huì)浪費(fèi)5到30%的堆。但是,你有沒有想過其他類的實(shí)例,包括數(shù)組,有時(shí)也會(huì)重復(fù),浪費(fèi)了相當(dāng)多的內(nèi)存?如果沒有,請(qǐng)繼續(xù)閱讀。
對(duì)象復(fù)制方案
只要某種類型的不同對(duì)象的數(shù)量有限,但應(yīng)用程序不斷創(chuàng)建此類對(duì)象而不嘗試緩存/重用現(xiàn)有對(duì)象,就會(huì)發(fā)生內(nèi)存中的對(duì)象復(fù)制。以下是我看到的對(duì)象復(fù)制的幾個(gè)具體示例:
- 在Hadoop文件系統(tǒng)(HDFS)NameServer中, byte[] 數(shù)組而不是 Strings用于存儲(chǔ)文件名。當(dāng)在不同目錄中存在具有相同名稱的文件時(shí),相應(yīng)的 byte[] 陣列是重復(fù)的。有關(guān) 詳細(xì)信息,請(qǐng)參閱此票證。
- 在一些監(jiān)視系統(tǒng)中,從被監(jiān)視實(shí)體(機(jī)器,應(yīng)用程序,組件等)接收的周期性“事件”或“更新”被表示為具有兩個(gè)主要字段的小對(duì)象:時(shí)間戳和值。當(dāng)許多更新同時(shí)到達(dá)并且所有更新都具有相同的值(例如,0或1表示被監(jiān)視實(shí)體的健康狀況良好)時(shí),會(huì)創(chuàng)建許多重復(fù)對(duì)象。
- 該 蜂房數(shù)據(jù)倉庫曾經(jīng)有過以下問題。當(dāng)針對(duì)具有大量分區(qū)的同一DB表執(zhí)行多個(gè)并發(fā)查詢時(shí),每個(gè)分區(qū)的元數(shù)據(jù)的單獨(dú)的每個(gè)查詢副本被加載到內(nèi)存中。分區(qū)元數(shù)據(jù)表示為 java.util.Properties 實(shí)例。因此,針對(duì)2000個(gè)分區(qū)運(yùn)行50個(gè)并發(fā)查詢,Properties 用于為這些分區(qū)中的每個(gè)分區(qū)創(chuàng)建50個(gè)相同的副本 ,或者總共100,000個(gè)這樣的集合,這消耗了大量內(nèi)存。有關(guān)詳細(xì)信息,請(qǐng)參閱此票證。
這只是幾個(gè)例子。其他不太明顯的包括存儲(chǔ)相同消息的多個(gè)相同字節(jié)緩沖區(qū),具有表示某些頻繁出現(xiàn)的數(shù)據(jù)組合的相同內(nèi)容的多個(gè)(通常是小的)對(duì)象集,等等。
擺脫重復(fù)的對(duì)象
如上所述,字符串是一類特別容易重復(fù)的對(duì)象。很久以前JDK開發(fā)人員已經(jīng)實(shí)現(xiàn)了這個(gè)問題,并使用String.intern()方法解決了這個(gè)問題。上面提到的文章詳細(xì)討論了它。簡而言之,此方法使用具有有效弱引用的全局字符串緩存(池)。如果它還沒有在緩存中,它會(huì)保存并返回給定的字符串實(shí)例,或者返回具有相同值的緩存字符串實(shí)例。String.intern() 曾經(jīng)不是很好的 性能和可擴(kuò)展性 從JDK 7開始大幅改進(jìn)。因此,當(dāng)沒有過度使用時(shí),它可能是許多應(yīng)用程序的良好解決方案。然而,在討論這篇文章在高度并發(fā)或性能關(guān)鍵的應(yīng)用程序中,它可能成為瓶頸,可能需要一種不同的“手動(dòng)”實(shí)習(xí)方法。
讓我們考慮其他對(duì)象實(shí)習(xí)。請(qǐng)記住,以下討論僅適用于不可變對(duì)象,即在創(chuàng)建后不會(huì)更改的對(duì)象。如果對(duì)象內(nèi)容可以改變,消除重復(fù)變得更加困難,并且需要定制的逐案解決方案。
最廣泛使用的現(xiàn)成實(shí)習(xí)功能由Guava庫通過com.google.commmon.collect.Interners類提供。此類有兩個(gè)返回內(nèi)部實(shí)例的關(guān)鍵方法: newStrongInterner() 和 newWeakInterner()。弱內(nèi)部函數(shù)最終會(huì)釋放不再需要的對(duì)象(未在任何地方強(qiáng)烈引用),通常使用較少的內(nèi)存,因此更頻繁地使用。它被實(shí)現(xiàn)為具有類似于標(biāo)準(zhǔn)JDK的弱鍵的并發(fā)哈希集 ConcurrentHashMap。在許多情況下,這是一個(gè)很好的選擇,有助于通過較小的CPU性能開銷大幅減少內(nèi)存占用,這通常會(huì)減少GC時(shí)間。但是,請(qǐng)考慮以下情況:
- 一些C類有2000萬個(gè)實(shí)例,每個(gè)實(shí)例占32個(gè)字節(jié)
- 其中1000萬個(gè)實(shí)例彼此完全相同,另外1000萬個(gè)實(shí)例都是截然不同的。在實(shí)踐中,這種尖銳的劃分幾乎從未發(fā)生過,但是,大約一半的物體僅表示少數(shù)獨(dú)特的值,而在另一半中,大多數(shù)物體很少或沒有重復(fù),這種情況非常常見。簡化的劃分使我們的計(jì)算更容易。
在這種情況下,當(dāng)我們不實(shí)習(xí)任何C實(shí)例時(shí),它們使用32 * 20M = 640M字節(jié)。但是如果我們intern() 為每個(gè)人調(diào)用番石榴會(huì)發(fā)生什么 呢?
前1000萬個(gè)對(duì)象將成功減少到只有一個(gè)C實(shí)例,占用的內(nèi)存可以忽略不計(jì)。但是,對(duì)于剩下的1000萬個(gè)對(duì)象中的每一個(gè),都沒有節(jié)省,因?yàn)樗鼈冎械拿恳粋€(gè)都是唯一的。盡管如此,Guava弱內(nèi)部人員將維持一個(gè)內(nèi)部表格,其中包含1000萬個(gè)條目以容納這些對(duì)象中的每一個(gè)。該表將使用com.google.common.collect.MapMakerInternalMap$WeakKeyDummyValueEntry 每個(gè)實(shí)習(xí)對(duì)象的一個(gè)類實(shí)例 ,以及引用每個(gè)條目的內(nèi)部數(shù)組中的一個(gè)插槽。a的大小 WeakKeyDummyValueEntry是40個(gè)字節(jié),因此每個(gè)實(shí)習(xí)對(duì)象需要40 + 4 = 44個(gè)字節(jié)。44 * 10M = 440M。添加到32 * 10M = 320M,C的唯一實(shí)例仍然占用,現(xiàn)在總內(nèi)存占用量為760M字節(jié)!換句話說,我們使用更多的內(nèi)存比以前,而不是更少。
這種情況可能有點(diǎn)極端,但在實(shí)踐中,一般規(guī)則仍然存在:如果在給定的一組對(duì)象中,唯一對(duì)象的百分比很高,那么傳統(tǒng)的內(nèi)部存儲(chǔ)器可以節(jié)省內(nèi)存,存儲(chǔ)對(duì)每個(gè)對(duì)象的引用給予它,可能很小,如果不是負(fù)面的話。我們可以做得更好嗎?
固定大小的陣列,無鎖(FALF)內(nèi)部
事實(shí)證明,如果我們不需要每個(gè)唯一對(duì)象的單個(gè)副本,而只是想節(jié)省內(nèi)存,并且可能仍然有一些重復(fù)的對(duì)象 - 換句話說,如果我們同意“機(jī)會(huì)性地”重復(fù)刪除對(duì)象 - 有一個(gè)簡單而有效的解決方案。我沒有在文獻(xiàn)中看到它,所以我把它命名為“固定大小數(shù)組,無鎖(FALF)Interner”。
此interner實(shí)現(xiàn)為一個(gè)小的,固定大小,基于開放哈希映射的對(duì)象緩存。當(dāng)存在高速緩存未命中時(shí),給定插槽中的高速緩存對(duì)象始終用新對(duì)象替換。沒有鎖定和沒有同步,因此沒有相關(guān)的開銷。基本上,這個(gè)緩存是基于這樣的想法:具有值X的具有許多副本的對(duì)象具有更高的機(jī)會(huì)保持在緩存中足夠長的時(shí)間以保證在錯(cuò)過驅(qū)逐X之前自己的幾個(gè)緩存命中并且用具有對(duì)象的對(duì)象替換它。一個(gè)不同的值Y.這是這個(gè)interner的一個(gè)可能的實(shí)現(xiàn):
/** Fixed size array, lock free object interner */staticclassFALFInterner<T>{staticfinalintMAXIMUM_CAPACITY=1<<30;privateObject[]cache;FALFInterner(intexpectedCapacity) {cache=newObject[tableSizeFor(expectedCapacity)];}Tintern(Tobj) {intslot=hash(obj)&(cache.length-1);TcachedObj=(T)cache[slot];if(cachedObj!=null&&cachedObj.equals(obj))returncachedObj;else{cache[slot]=obj;returnobj;}}/** Copied from java.util.HashMap */staticinthash(Objectkey) {inth;return(key==null)?0: (h=key.hashCode())^(h>>>16);}/*** Returns a power of two size for the given target capacity.* Copied from java.util.HashMap.*/staticinttableSizeFor(intcap) {intn=cap-1;n|=n>>>1;n|=n>>>2;n|=n>>>4;n|=n>>>8;n|=n>>>16;return(n<0)?1: (n>=MAXIMUM_CAPACITY)?MAXIMUM_CAPACITY:n+1;}}為了比較FALF interner和Guava weak interner的性能,我寫了一個(gè)簡單的多線程基準(zhǔn)測試。代碼生成并實(shí)現(xiàn)從遵循高斯分布的隨機(jī)數(shù)派生的字符串。也就是說,具有某些值的字符串比其他字符串更頻繁地生成,因此將導(dǎo)致更多重復(fù)。在這個(gè)基準(zhǔn)測試中,FALF interner運(yùn)行速度比Guava interner快約17%。但是,并非全部。當(dāng)我測量內(nèi)存占用時(shí) jmap -histo:live 基準(zhǔn)測試完成后運(yùn)行,但在退出之前,事實(shí)證明,使用FALF interner,使用的堆大小比Guava interner小近30倍!這是固定大小的小緩存與弱散列映射的內(nèi)存占用量的差異,其中每個(gè)獨(dú)特對(duì)象都有一個(gè)條目。
公平地說,FALF interner通常需要比傳統(tǒng)的,一刀切的內(nèi)部調(diào)整器更多的調(diào)整。首先,由于緩存大小是固定的,您需要仔細(xì)選擇它。一方面,為了最大限度地減少未命中,這個(gè)大小應(yīng)該足夠大 - 理想情況下等于你實(shí)習(xí)生類型的唯一對(duì)象的數(shù)量。另一方面,我們的目標(biāo)是最小化已用內(nèi)存,因此在實(shí)踐中,您可能會(huì)選擇(更大)更小的大小,該大小大致等于具有大量重復(fù)項(xiàng)的對(duì)象的數(shù)量。
另一個(gè)重要的考慮因素是為被攔截對(duì)象選擇散列函數(shù)。在一個(gè)小的,固定大小的緩存中,盡可能均勻地在插槽中分布對(duì)象非常重要,以避免不經(jīng)常使用許多插槽時(shí)的情況,而有一個(gè)小組,其中每個(gè)插槽由幾個(gè)對(duì)象值爭用很多副本。這種爭用將導(dǎo)致緩存遺漏許多重復(fù),因此,更大的內(nèi)存占用。當(dāng)這種情況發(fā)生在具有非常簡單hashCode() 方法的類的實(shí)例時(shí) (例如,代碼類似于java.lang.String 類中的代碼 ),它可能表明該散列函數(shù)實(shí)現(xiàn)是不合適的。更高級(jí)的哈希函數(shù),就像com.google.common.hash.Hashing提供的哈希函數(shù)之一 類,可以大大提高FALF interner的效率。
檢測重復(fù)對(duì)象
到目前為止,我們還沒有討論過開發(fā)人員如何確定應(yīng)用程序中的哪些對(duì)象有很多重復(fù)項(xiàng),因此需要進(jìn)行實(shí)習(xí)。對(duì)于大型應(yīng)用程序,這可能是非平凡的。即使您可以猜測哪些對(duì)象可能重復(fù),也很難估計(jì)其確切的內(nèi)存影響。根據(jù)經(jīng)驗(yàn),解決此問題的最佳方法是生成應(yīng)用程序的堆轉(zhuǎn)儲(chǔ),然后使用工具對(duì)其進(jìn)行分析。
堆轉(zhuǎn)儲(chǔ)本質(zhì)上是正在運(yùn)行的JVM堆的完整快照。它可以通過調(diào)用jmap 實(shí)用程序在任意時(shí)刻進(jìn)行 ,也可以將JVM配置為在失敗時(shí)自動(dòng)生成它 OutOfMemoryError。如果你谷歌“JVM堆轉(zhuǎn)儲(chǔ)”,你會(huì)立即看到一堆關(guān)于這個(gè)主題的相關(guān)文章。
堆轉(zhuǎn)儲(chǔ)是一個(gè)大小與JVM堆大小相同的二進(jìn)制文件,因此只能使用特殊工具讀取和分析它。有許多這樣的工具,包括開源和商業(yè)。最流行的開源工具是Eclipse MAT; 還有VisualVM和一些不那么強(qiáng)大,鮮為人知的工具。商業(yè)工具包括通用Java分析器:JProfiler和YourKit,以及JXRay - 專門為堆轉(zhuǎn)儲(chǔ)分析構(gòu)建的工具。
與大多數(shù)其他工具不同,JXRay會(huì)立即分析堆轉(zhuǎn)儲(chǔ)以解決大量常見問題,包括重復(fù)字符串和其他對(duì)象。目前,對(duì)象比較淺薄。也就是說,只有兩個(gè)對(duì)象(例如 ArrayLists)x0, x1, x2, ... 以相同的順序引用完全相同的對(duì)象組時(shí)才被認(rèn)為是重復(fù)的 。換一種說法,兩個(gè)對(duì)象 a 和 b 被認(rèn)為是平等的,都指向其他對(duì)象 a 和 b 使用位是相等的,有點(diǎn)。
JXRay運(yùn)行一次給定的堆轉(zhuǎn)儲(chǔ),并生成一個(gè)包含HTML格式的所有收集信息的報(bào)告。這種方法的優(yōu)點(diǎn)是,您可以隨時(shí)隨地查看分析結(jié)果,并輕松與他人分享。這也意味著您可以在任何機(jī)器上運(yùn)行該工具,包括數(shù)據(jù)中心中功能強(qiáng)大但功能強(qiáng)大的“無頭”機(jī)器。
從JXRay獲得報(bào)告后,在您喜歡的瀏覽器中打開它并展開相關(guān)部分。你可能會(huì)看到這樣的事情:
因此,在此轉(zhuǎn)儲(chǔ)中,24.7%的已使用堆被重復(fù)的非數(shù)組非集合對(duì)象浪費(fèi)!上表列出了其實(shí)例對(duì)此開銷貢獻(xiàn)最大的所有類。要查看這些對(duì)象的來源(哪些對(duì)象引用它們,一直到GC根),向下滾動(dòng)到報(bào)告的“昂貴數(shù)據(jù)字段”或“完整參考鏈”子部分,展開它,然后單擊相關(guān)表中的一行。以下是上述類之一的示例 TopicPartition:
從這里,我們可以很好地了解哪些數(shù)據(jù)結(jié)構(gòu)可以管理有問題的對(duì)象。
總而言之,重復(fù)對(duì)象,即具有相同內(nèi)容的同一類的多個(gè)實(shí)例,可能成為Java應(yīng)用程序的負(fù)擔(dān)。它們可能會(huì)浪費(fèi)大量內(nèi)存和/或增加GC壓力。衡量此類對(duì)象影響的最佳方法是獲取堆轉(zhuǎn)儲(chǔ)并使用JXRay之類的工具對(duì)其進(jìn)行分析。當(dāng)重復(fù)對(duì)象是不可變的時(shí),您可以使用現(xiàn)成的或自定義的內(nèi)部實(shí)現(xiàn)來減少此類對(duì)象的數(shù)量,從而減少其內(nèi)存影響。可變復(fù)制對(duì)象更難以擺脫,并且可能只能通過逐個(gè)定制的解決方案來消除。
總結(jié)
以上是生活随笔為你收集整理的java如果把字符串转成对象_Java中的重复对象:不仅仅是字符串的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: google chrome如何保存密码(
- 下一篇: 苹果联系人导入安卓手机(苹果联系人导入安