黄色网址大全免费-黄色网址你懂得-黄色网址你懂的-黄色网址有那些-免费超爽视频-免费大片黄国产在线观看

降低Java垃圾回收開銷的5條建議


  保持GC低開銷的竅門有哪些?

 

  隨著一再拖延而即將發(fā)布的Java9,G1(“GarbageFirst”)垃圾回收器將被成為HotSpot虛擬機(jī)默認(rèn)的垃圾回收器。從serial垃圾回收器到CMS收集器,JVM見證了許多GC實(shí)現(xiàn),而G1將成為其下一代垃圾回收器。

 

  隨著垃圾收集器的發(fā)展,每一代GC與其上一代相比,都帶來(lái)了巨大的進(jìn)步和改善。parallelGC與serialGC相比,它讓垃圾收集器以多線程的方式工作,充分利用了多核計(jì)算機(jī)的計(jì)算能力。CMS(“ConcurrentMark-Sweep”)收集器與parallelGC相比,它將回收過(guò)程分成了多個(gè)階段,使得應(yīng)用線程正在運(yùn)行的時(shí)候,收集工作可以并發(fā)地完成,大大改善了頻繁執(zhí)行“stop-the-world”的情況。G1對(duì)于擁有大量堆內(nèi)存的JVM表現(xiàn)出更好的性能,并且具有更好的可預(yù)測(cè)和統(tǒng)一的暫停過(guò)程。

 

  Tip#1:預(yù)測(cè)集合的容量

 

  所有標(biāo)準(zhǔn)的Java集合,包括定制和擴(kuò)展的實(shí)現(xiàn)(比如Trove和Google的Guava),底層都使用了數(shù)組(原生數(shù)據(jù)類型或者基于對(duì)象的類型)。因?yàn)閿?shù)組一旦被分配,其大小就不可變,因此添加元素到集合時(shí),大多數(shù)情況下都會(huì)導(dǎo)致需要重新申請(qǐng)一個(gè)新的大容量數(shù)組替換老的數(shù)組(指集合底層實(shí)現(xiàn)使用的數(shù)組)。

 

  即使沒(méi)有提供集合初始化的大小,大多數(shù)集合的實(shí)現(xiàn)都盡量?jī)?yōu)化重新分配數(shù)組的處理并且將其開銷平攤到zui低。不過(guò),在構(gòu)造集合的時(shí)候就提供大小可以得到很好的效果。

 

  讓我們將下面的代碼作為一個(gè)簡(jiǎn)單的例子分析一下:

 

  publicstaticListreverse(List<?extendsT>list){

 

  Listresult=newArrayList();

 

  for(inti=list.size()-1;i>=0;i--){

 

  result.add(list.get(i));

 

  }

 

  returnresult;

 

  }

 

  Thismethodallocatesanewarray,thenfillsitupwithitemsfromanotherlist,onlyinreverseorder.這個(gè)方法分配了一個(gè)新的數(shù)組,然后用另一個(gè)list中元素對(duì)該數(shù)組進(jìn)行填充,只是元素的數(shù)序發(fā)生了變化。

 

  這個(gè)處理方式可能會(huì)付出慘重的性能代價(jià),其優(yōu)化的點(diǎn)在添加元素到新的list中這行代碼。隨著每一次添加元素,list都需要確保其底層數(shù)組擁有足夠的位置來(lái)容納新的元素。如果有空閑的位置,那么只是簡(jiǎn)單地將新元素存儲(chǔ)到下一個(gè)空閑的槽位。如果沒(méi)有的話,將分配一個(gè)新的底層數(shù)組,拷貝舊的數(shù)組內(nèi)容到新的數(shù)組中,然后添加新的元素。這將導(dǎo)致多次分配數(shù)組,那些剩余的舊數(shù)組會(huì)被GC所回收。

 

  我們可以通過(guò)在構(gòu)造集合時(shí)讓其底層的數(shù)組知道它將存儲(chǔ)多少元素,從而避免這些多余的分配

 

  publicstaticListreverse(List<?extendsT>list){

 

  Listresult=newArrayList(list.size());

 

  for(inti=list.size()-1;i>=0;i--){

 

  result.add(list.get(i));

 

  }

 

  returnresult;

 

  }

 

  上面的代碼通過(guò)ArrayList的構(gòu)造器指定足夠大的空間來(lái)存儲(chǔ)list.size()個(gè)元素,在初始化時(shí)完成分配的執(zhí)行,這意味著List在迭代的過(guò)程中無(wú)需再次分配內(nèi)存。

 

  Guava的集合類則更進(jìn)一步,允許初始化集合時(shí)明確指定期望元素的個(gè)數(shù)或者指定一個(gè)預(yù)測(cè)值。

 

  Listresult=Lists.newArrayListWithCapacity(list.size());

 

  Listresult=Lists.newArrayListWithExpectedSize(list.size());

 

  上面的代碼中,前者用于我們已經(jīng)準(zhǔn)確地知道集合將要存儲(chǔ)多少元素,而后者的分配方式考慮了錯(cuò)誤預(yù)估的情況。

 

  Tip#2:直接處理數(shù)據(jù)流

 

  當(dāng)處理數(shù)據(jù)流時(shí),比如從一個(gè)文件讀取數(shù)據(jù)或者從網(wǎng)絡(luò)中下載數(shù)據(jù),下面的代碼是非常常見的:

 

  byte[]fileData=readFileToByteArray(newFile("myfile.txt"));

 

  所產(chǎn)生的字節(jié)數(shù)組可能被解析XML文檔、JSON對(duì)象或者協(xié)議緩沖消息,以及一些常見的可選項(xiàng)。

 

  當(dāng)處理大文件或者文件的大小無(wú)法預(yù)測(cè)時(shí),上面的做法很是不明智的,因?yàn)楫?dāng)JVM無(wú)法分配一個(gè)緩沖區(qū)來(lái)處理真正文件時(shí),就會(huì)導(dǎo)致OutOfMemeoryErrors。

 

  即使數(shù)據(jù)的大小是可管理的,當(dāng)?shù)嚼厥諘r(shí),使用上面的模式依然會(huì)造成巨大的開銷,因?yàn)樗诙阎蟹峙淞艘粔K非常大的區(qū)域來(lái)存儲(chǔ)文件數(shù)據(jù)。

 

  一種更加好的處理方式是使用合適的InputStream(比如在這個(gè)例子中使用FileInputStream)直接傳遞給解析器,不再一次性將整個(gè)文件讀取到一個(gè)字節(jié)數(shù)組中。所有主流的開源庫(kù)都提供相應(yīng)的API來(lái)直接接受一個(gè)輸入流進(jìn)行處理,比如:

 

  FileInputStreamfis=newFileInputStream(fileName);

 

  MyProtoBufMessagemsg=MyProtoBufMessage.parseFrom(fis);

 

  Tip#3:使用不可變的對(duì)象

 

  不變性有太多的好處。甚至不用我贅述什么。然而,有一個(gè)優(yōu)點(diǎn)會(huì)對(duì)垃圾回收產(chǎn)生影響,應(yīng)該關(guān)注一下。

 

  一個(gè)不可變對(duì)象的屬性在對(duì)象被創(chuàng)建后就不能被修改(在這里的例子使用的是引用數(shù)據(jù)類型的屬性),比如:

 

  publicclassObjectPair{

 

  privatefinalObjectfirst;

 

  privatefinalObjectsecond;

 

  publicObjectPair(Objectfirst,Objectsecond){

 

  this.first=first;

 

  this.second=second;

 

  }

 

  publicObjectgetFirst(){

 

  returnfirst;

 

  }

 

  publicObjectgetSecond(){

 

  returnsecond;

 

  }

 

  }

 

  將上面的類實(shí)例化后會(huì)產(chǎn)生一個(gè)不可變對(duì)象—它的所有屬性用final修飾,構(gòu)造完成后就不能改變了。

 

  不可變性意味著所有被一個(gè)不可變?nèi)萜魉玫膶?duì)象,在容器構(gòu)造完成前對(duì)象就已經(jīng)被創(chuàng)建。就GC而言:這個(gè)容器年輕程度至少和其所持有的年輕的引用一樣。這意味著當(dāng)在年輕代執(zhí)行垃圾回收的過(guò)程中,GC因?yàn)椴豢勺儗?duì)象處于老年代而跳過(guò)它們,直到確定這些不可變對(duì)象在老年代中不被任何對(duì)象所引用時(shí),才完成對(duì)它們的回收。

 

  更少的掃描對(duì)象意味著對(duì)內(nèi)存頁(yè)更少的掃描,越少的掃描內(nèi)存頁(yè)就意味著更短的GC生命周期,也意味著更短的GC暫停和更好的總吞吐量。

 

  Tip#4:小心字符串拼接

 

  字符串可能是在所有基于JVM應(yīng)用程序中常用的非原生數(shù)據(jù)結(jié)構(gòu)。然而,由于其隱式地開銷負(fù)擔(dān)和簡(jiǎn)便的使用,非常容易成為占用大量?jī)?nèi)存的罪歸禍?zhǔn)住?/p>

 

  這個(gè)問(wèn)題很明顯不在于字符串字面值,而是在運(yùn)行時(shí)分配內(nèi)存初始化產(chǎn)生的。讓我們快速看一下動(dòng)態(tài)構(gòu)建字符串的例子:

 

  publicstaticStringtoString(T[]array){

 

  Stringresult="[";

 

  for(inti=0;i<array.length;i++){

 

  result+=(array[i]==array?"this":array[i]);

 

  if(i<array.length-1){

 

  result+=",";

 

  }

 

  }

 

  result+="]";

 

  returnresult;

 

  }

 

  這是個(gè)看似不錯(cuò)的方法,接收一個(gè)字符數(shù)組然后返回一個(gè)字符串。但是這對(duì)于對(duì)象內(nèi)存分配卻是災(zāi)難性的。

 

  很難看清這語(yǔ)法糖的背后,但是幕后的實(shí)際情況是這樣的:

 

  publicstaticStringtoString(T[]array){

 

  Stringresult="[";

 

  for(inti=0;i<array.length;i++){

 

  StringBuildersb1=newStringBuilder(result);

 

  sb1.append(array[i]==array?"this":array[i]);

 

  result=sb1.toString();

 

  if(i<array.length-1){

 

  StringBuildersb2=newStringBuilder(result);

 

  sb2.append(",");

 

  result=sb2.toString();

 

  }

 

  }

 

  StringBuildersb3=newStringBuilder(result);

 

  sb3.append("]");

 

  result=sb3.toString();

 

  returnresult;

 

  }

 

  字符串是不可變的,這意味著每發(fā)生一次拼接時(shí),它們本身不會(huì)被修改,而是依次分配新的字符串。此外,編譯器使用了標(biāo)準(zhǔn)的StringBuilder類來(lái)執(zhí)行這些拼接操作。這就會(huì)有問(wèn)題了,因?yàn)槊恳淮蔚入[式地分配了一個(gè)臨時(shí)字符串,又隱式分配了一個(gè)臨時(shí)的StringBuilder對(duì)象來(lái)幫助構(gòu)建結(jié)果。

 

  更好的方式是避免上面的情況,使用StringBuilder和直接的追加,以取代本地拼接操作符(“+”)。下面是一個(gè)例子:

 

  publicstaticStringtoString(T[]array){

 

  StringBuildersb=newStringBuilder("[");

 

  for(inti=0;i<array.length;i++){

 

  sb.append(array[i]==array?"this":array[i]);

 

  if(i<array.length-1){

 

  sb.append(",");

 

  }

 

  }

 

  sb.append("]");

 

  returnsb.toString();

 

  }

 

  這里,我們只在方法開始的時(shí)候分配了僅有的一個(gè)StringBuilder。至此,所有的字符串和list中的元素都被追加到單獨(dú)的一個(gè)StringBuilder中。使用toString()方法一次性將其轉(zhuǎn)成成字符串返回。

 

  Tip#5:使用特定的原生類型的集合

 

  Java標(biāo)準(zhǔn)的集合庫(kù)簡(jiǎn)單且支持泛型,允許在使用集合時(shí)對(duì)類型進(jìn)行半靜態(tài)地綁定。比如想要?jiǎng)?chuàng)建一個(gè)只存放字符串的Set或者存儲(chǔ)Map<Pair,List>這樣的map,這種處理方式是非常棒的。

 

  真正的問(wèn)題源于當(dāng)我們想要使用一個(gè)list存儲(chǔ)int類型,或者一個(gè)map存儲(chǔ)double類型作為value。因?yàn)榉盒筒恢С衷鷶?shù)據(jù)類型,因此另外的一種選擇是使用包裝類型來(lái)進(jìn)行替換,這里我們使用List。

 

  這種處理方式是非常浪費(fèi)的,因?yàn)橐粋€(gè)Integer是一個(gè)完全的對(duì)象,一個(gè)對(duì)象的頭部占用12個(gè)字節(jié)以及其內(nèi)部的所維護(hù)的int屬性,每個(gè)Integer對(duì)象總共占用16個(gè)字節(jié)。這比起存儲(chǔ)相同個(gè)數(shù)的int類型的list而言,其消耗的空間是它的四倍!比這個(gè)更加嚴(yán)重的問(wèn)題在于,事實(shí)上因?yàn)镮nteger是真正的對(duì)象實(shí)例,因此它需要垃圾收集階段被垃圾收集器所考慮是否要回收。

 

  為了處理這個(gè)問(wèn)題,我們?cè)赥akipi中使用非常棒的Trove集合庫(kù)。Trove摒棄了部分泛型的特定來(lái)支持特定的使用內(nèi)存更快速的原生類型的集合。比如,我們使用非常消耗性能的Map<Integer,Double>,在Trove中有另一種特別的選擇方案,其形式為TIntDoubleMap

 

  TIntDoubleMapmap=newTIntDoubleHashMap();

 

  map.put(5,7.0);

 

  map.put(-1,9.999);

 

  ...

 

  Trove的底層實(shí)現(xiàn)使用了原生類型的數(shù)組,所以當(dāng)操作集合的時(shí)候不會(huì)發(fā)生元素的裝箱(int->Integer)或者拆箱(Integer->int),沒(méi)有存儲(chǔ)對(duì)象,因?yàn)榈讓邮褂迷鷶?shù)據(jù)類型存儲(chǔ)。

 

  最后

 

  隨著垃圾收集器持續(xù)的改進(jìn),以及運(yùn)行時(shí)的優(yōu)化和JIT編譯器也變得越來(lái)越智能。我們作為開發(fā)者將會(huì)發(fā)現(xiàn)越來(lái)越少地考慮如何編寫GC友好的代碼。然而,就目前階段,不論G1如何改進(jìn),我們?nèi)匀挥泻芏嗫梢宰龅氖聛?lái)幫JVM提升性能。

 

  

 

上一篇:程序員你為什么迷茫?
下一篇:關(guān)于社招和Java程序員聊聊

開班信息

主站蜘蛛池模板: 精品国产欧美一区二区三区成人 | 黄色大片视频 | 日本一道高清 | 欧美一区二区手机在线观看视频 | 最近中文字幕完整国语 | 国产成人午夜精品免费视频 | a级毛片黄片| 在线伊人网 | 欧美怀孕色xxxxx | 又黄又爽又色的性视频 | 免费在线日韩 | 成人在线h | 大胸美女被强吻胸动态图片 | 亚洲欧美一区二区三区久本道 | 波多野结衣在线观看一区二区三区 | 亚洲日本va午夜中文字幕 | 欧美日韩在线国产 | 成人欲涩漫h漫免费动漫 | 麻豆国产精品一二三在线观看 | 日老逼视频 | 激情视频亚洲 | 国产精品天天操 | 99精品欧美一区 | 亚洲图片国产日韩欧美 | 亚洲成a人片在线观看 欧美 | 青青青青青青草 | 亚洲第一免费网站 | 日韩高清片 | 亚洲va韩国va欧美va天堂 | 成人深夜视频 | 欧美在线播放成人免费 | 韩国欧洲一级毛片免费 | 中文字幕一区二区三区免费视频 | 国产精品v一区二区三区 | 日本午夜视频在线 | 福利视频欧美一区二区三区 | 色婷婷综合久久久中文字幕 | 涩涩资源站 | 亚洲图片综合网 | www.日本三级 | 久久国产精品成人免费 |