論如何編寫JVM自適應(yīng)的Java代碼

和風(fēng)微醺,人間四月天藕赞。 記于 2019.4.1 早10:31分

? ? ? 一直都想給自己開一個(gè)博客寫一點(diǎn)技術(shù)類的東西孤紧,但也一直疲于工作無暇他顧。恰近日突然的工作變動反而讓自己得以略空閑吃既,身心得以舒展并思考未來的路該怎么走考榨。這個(gè)開博事宜又悄然上心頭, 所謂想及千百遍不如實(shí)際走一遭鹦倚。 交代好前言即將準(zhǔn)備寫下這篇技術(shù)博文河质,回顧10年多職業(yè)生涯在諸多技術(shù)主題里也思緒了片刻,突然也有點(diǎn)懵逼哈震叙。寫啥掀鹅?怎么寫?如何寫的以詞達(dá)意媒楼。乐尊。。 在片刻猶豫之后意上心頭:<論如何編寫出與JVM自適應(yīng)優(yōu)化所契合的代碼>划址。 嗯扔嵌,如大家所想這個(gè)標(biāo)題范圍有點(diǎn)廣,似乎有點(diǎn)嗨夺颤,應(yīng)該是一篇逼格略高但也許不知所云的文章对人。。拂共。權(quán)且先這樣定個(gè)基調(diào)吧牺弄。

? ? ? 切入正題, 這是一篇源于實(shí)際生產(chǎn)事故一篇實(shí)操性質(zhì)的技術(shù)博文。一切源于工作宜狐,如有雷同實(shí)屬巧合势告。問題背景: 朋友所在公司開發(fā)一款新功能涉及大文本前端展示, 文本主體內(nèi)容后臺編輯留有在占位符, 服務(wù)端填充占位輸出客戶端展示,流程大致如此抚恒。在自測/內(nèi)測過程中都挺OK咱台,畢竟就是很簡單的功能開發(fā)。上線后也相安無事了一段時(shí)間俭驮,但后面發(fā)生的事情就較為詭異了回溺。由于市場部門推廣春贸,此功能受眾面迅速擴(kuò)大,訪問量相比未推廣前曾幾何級遞增且服務(wù)器穩(wěn)定性出現(xiàn)波動遗遵,經(jīng)常性O(shè)OM且需要重啟才能解決萍恕。朋友公司所在技術(shù)團(tuán)隊(duì),根據(jù)OOM的堆轉(zhuǎn)存儲文件分析發(fā)現(xiàn):OOM發(fā)生時(shí),堆內(nèi)存活著大量的String對象 , char[]數(shù)組车要,占據(jù)好幾GB的堆內(nèi)存允粤,大大超過其他存活對象所占據(jù)的內(nèi)存。

? ? ? 問題看起來貌似很明朗了,實(shí)際好像也是這樣翼岁。朋友所在技術(shù)團(tuán)隊(duì)經(jīng)過模塊排查逐步定位类垫,基本確定了最近上線的一系列功能模塊里, 大文本前端展示為首要原因,這個(gè)模塊貢獻(xiàn)了大量的存活且無法有效釋放的char[]數(shù)組琅坡。問題原因似乎找到了悉患,但為何會是這樣的呢? 于是乎針對這個(gè)功能的二次代碼review如影隨形的開展了榆俺。代碼邏輯如下:網(wǎng)頁客戶端發(fā)起請求--->Controller層[接受客戶端請求]--->Service層[根據(jù)業(yè)務(wù)邏輯填充占位符]--->DAO層[讀取后臺編輯后存儲在數(shù)據(jù)庫中的大文本]數(shù)據(jù)載體String對象就這樣層層向上傳遞最終輸出到客戶端购撼。因?yàn)榇a著實(shí)簡單review完也沒看出具體問題。示例代碼:

```

public String getBigText(final String tId) throws SQLException {

ResultSet result = null; //這里null為模擬獲取結(jié)果集.

String bigText = result.getString("content");

return bigText;

}

```

先說下解決方案吧谴仙,問題很簡單卻也挺惱人迂求。我們可以采用intern方法字符串池化, 對于大量重復(fù)的字符串使用得當(dāng)可以節(jié)約內(nèi)存。當(dāng)然大文本渲染的方式的也需要發(fā)生變化晃跺,填充數(shù)據(jù)需要異步讀取顯示揩局。示例代碼:

```

public String getBigText(final String tId) throws SQLException {

ResultSet result = null; //這里null為模擬獲取結(jié)果集.

String bigText = result.getString("content").intern(); //字符串池化

return bigText;

}

```

回到文章的開頭貌似與<論如何編寫出與JVM自適應(yīng)優(yōu)化所契合的代碼>這個(gè)大標(biāo)題一點(diǎn)關(guān)系都沒有啊。在解決溝通的過程中提及了兩個(gè)概念: 1.針對大量重復(fù)字符串掀虎,不采用其他技術(shù)方案的情況下使用intern可以最小化代碼改動并達(dá)到低內(nèi)存占用的目標(biāo)? 2. intern后GC Root引用鏈?zhǔn)侨绾伪磺袛嗟牧瓒ⅲ菍ο髢?nèi)存可被快速釋放的關(guān)鍵要素 [備注:GC Root引用鏈,即JVM對象引用判斷-根檢索算法鏈?zhǔn)侥P蚞烹玉。

? ? ? ? Linux之父有一段名言:talk is cheap, show me the code驰怎!? 示例代碼如下(這段代碼摘自網(wǎng)絡(luò),單純用來演示這項(xiàng)技術(shù)):

```

package util;

import java.util.Random;

import java.util.concurrent.TimeUnit;

/**

JVM參數(shù):

-Xmx4096m

-Xms4096m

-XX:+PrintGCDetails

**/

public final class StringIntern {

private static final int MAX = 1000 * 10000;

private static final String[] arr = new String[MAX];

public static void main(String[] args) throws Exception {

TimeUnit.SECONDS.sleep(20); //這里暫停線程,掛載JVM分析器

? ? Integer[] DB_DATA = new Integer[10];

? ? Random random = new Random(10 * 10000);

? ? for (int i = 0; i < DB_DATA.length; i++) {

? ? ? ? DB_DATA[i] = random.nextInt();

? ? }


? ? for (int i = 0; i < MAX; i++) {

? ? ? ? arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));

// ? ? ? ? arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern(); //intern,切斷GC 引用鏈

? ? }

? ? System.out.println("執(zhí)行完畢,可以開啟JVM堆分析了...");

? ? TimeUnit.SECONDS.sleep(600); //這里暫停線程,掛載JVM分析器

}

}

```

我們先來一段由性能剖析軟件發(fā)起的針對上面代碼執(zhí)行中, 強(qiáng)制GC前后的內(nèi)存占比.

GC前:

GC后:

如圖所示,F(xiàn)ull GC后大量的String對象與char[]數(shù)組依然存活二打,由此我們根據(jù)JVM內(nèi)存清理算法可以推導(dǎo)出這些對象與數(shù)組存在強(qiáng)引用县忌。不知是否有注意int[] 數(shù)組在GC后由138MB釋放到了21423kb,原因即方法執(zhí)行結(jié)束調(diào)用已出棧沒有被強(qiáng)引用所持有继效,所以在可釋放的范圍內(nèi)症杏。我們再接著看提到多次的GC Root引用鏈到底是什么鬼,見下圖:

我們仔細(xì)看標(biāo)藍(lán)的部分瑞信,剖析軟件基于此字符串對象根據(jù)根檢索算法正向與反向推導(dǎo)出完整的GC Root引用鏈厉颤。在此情況下回到文章開頭部分,大文本客戶端渲染在用戶訪問量大的情況下凡简,對大數(shù)據(jù)字符串的引用直到完成輸出到客戶端前都會存在強(qiáng)引用逼友,但凡出現(xiàn)OOM也只有重啟一條路可以走了精肃。。帜乞。

-------------------------------華麗分割線-----------------------------------------

問題已經(jīng)分析的接近尾聲了司抱,我們再聊下阻斷GC Root鏈后會發(fā)生什么。把代碼intern 注釋去掉挖函,把上一行代碼加上注釋, 如下:

```

// arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));

arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();

```

GC前的圖一樣,就不重復(fù)貼了状植,直接上GC后的圖:

小伙伴們驚訝么浊竟? 一次GC直接釋放完畢了怨喘。。振定。這個(gè)時(shí)候?qū)Ψ椒ǖ恼{(diào)用尚未結(jié)束必怜,因?yàn)榫€程sleep了。再引申一個(gè)技術(shù)點(diǎn)吧后频,N多的技術(shù)書籍都在傳授棧內(nèi)存會隨著線程的消亡而釋放梳庆,其實(shí)這句話原則上是對的,但如果非要死扣細(xì)節(jié)其實(shí)我愿意加上:如果方法執(zhí)行過程中卑惜,數(shù)據(jù)已經(jīng)出棧且不存在強(qiáng)引用膏执,GC即可釋放分配在堆中的無引用對象而無需等到方法全部執(zhí)行完線程退出后。

題外話: 雖然intern方法是個(gè)好東西但是使用要慎重露久,起碼你要真的懂才行更米。否則引起永久代內(nèi)存溢出或者青年代GC耗時(shí)增大等問題也是非常頭疼的且更隱晦! JVM常量池內(nèi)部是一個(gè)HashSet數(shù)據(jù)結(jié)構(gòu)毫痕,有容量的限制有參數(shù)可以控制征峦,高版本的JDK貌似具備自動resize的功能,個(gè)人沒做更多版本差異性研究消请,大家有興趣可以自行研究。

題外案例:

1. 國外著名的微博twitter也曾經(jīng)遇到過字符串高內(nèi)存占用的問題臊泰,部分技術(shù)博客亦有提及,也是采用intern解決的缸逃。

2. 阿里fastJson框架也曾在intern方法上跌過坑,有興趣可搜索學(xué)習(xí)察滑。

JVM案例:

```

? ? /*

? ? * Private remove method that skips bounds checking and does not

? ? * return the value removed.

? ? */

? ? private void fastRemove(int index) {

? ? ? ? modCount++;

? ? ? ? int numMoved = size - index - 1;

? ? ? ? if (numMoved > 0)

? ? ? ? ? ? System.arraycopy(elementData, index+1, elementData, index,

? ? ? ? ? ? ? ? ? ? ? ? ? ? numMoved);

? ? ? ? elementData[--size] = null; // clear to let GC do its work

? ? }

```

以上代碼源自JDK1.8 ArrayList remove方法打厘, elementData[--size] = null; 此處調(diào)用即為引用鏈阻斷。 使得對象引用不可達(dá)最終被GC回收贺辰,避免了內(nèi)存泄露嵌施。最后,由于intern調(diào)用將引用指向了常量池,所以new String創(chuàng)建的對象被阻斷了吗伤。。硫眨。無引用持有足淆,保證了GC回收。

寫在最后礁阁,這是我的第一篇技術(shù)博客如有寫的不對的地方巧号,麻煩留言指正喔姥闭,在此非常感謝。

2019.4.1 晚 19:16分 geweixinerr,? End棚品。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末铜跑,一起剝皮案震驚了整個(gè)濱河市门怪,隨后出現(xiàn)的幾起案子锅纺,更是在濱河造成了極大的恐慌,老刑警劉巖伞广,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異减拭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)拧粪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門沧侥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宴杀,你說我怎么就攤上這事⊥眨” “怎么了绢记?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵正卧,是天一觀的道長。 經(jīng)常有香客問我签孔,道長,這世上最難降的妖魔是什么窘行? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任饥追,我火速辦了婚禮抽高,結(jié)果婚禮上透绩,老公的妹妹穿的比我還像新娘。我一直安慰自己帚豪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布莹桅。 她就那樣靜靜地躺著,像睡著了一般诈泼。 火紅的嫁衣襯著肌膚如雪煤禽。 梳的紋絲不亂的頭發(fā)上铐达,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天瓮孙,我揣著相機(jī)與錄音,去河邊找鬼杭抠。 笑死恳啥,一個(gè)胖子當(dāng)著我的面吹牛偏灿,可吹牛的內(nèi)容都是我干的钝的。 我是一名探鬼主播忿墅,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼沮峡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了邢疙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤呼畸,失蹤者是張志新(化名)和其女友劉穎颁虐,沒想到半個(gè)月后蛮原,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體另绩,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年蹦漠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了车海。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡研铆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出棵红,到底是詐尸還是另有隱情留量,我是刑警寧澤窄赋,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布楼熄,位于F島的核電站,受9級特大地震影響错敢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜稚茅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亚享。 院中可真熱鬧,春花似錦欺税、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽应役。三九已至燥筷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荆责,已是汗流浹背亚脆。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留濒持,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓屈雄,卻偏偏與公主長得像官套,于是被迫代替她去往敵國和親酒奶。 傳聞我的和親對象是個(gè)殘疾皇子奶赔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345