论如何编写JVM自适应的Java代码

和风微醺,人间四月天。 记于 2019.4.1 早10:31分

      一直都想给自己开一个博客写一点技术类的东西,但也一直疲于工作无暇他顾。恰近日突然的工作变动反而让自己得以略空闲,身心得以舒展并思考未来的路该怎么走。这个开博事宜又悄然上心头, 所谓想及千百遍不如实际走一遭。 交代好前言即将准备写下这篇技术博文,回顾10年多职业生涯在诸多技术主题里也思绪了片刻,突然也有点懵逼哈。写啥?怎么写?如何写的以词达意。。。 在片刻犹豫之后意上心头:<论如何编写出与JVM自适应优化所契合的代码>。 嗯,如大家所想这个标题范围有点广,似乎有点嗨,应该是一篇逼格略高但也许不知所云的文章。。。权且先这样定个基调吧。

      切入正题, 这是一篇源于实际生产事故一篇实操性质的技术博文。一切源于工作,如有雷同实属巧合。问题背景: 朋友所在公司开发一款新功能涉及大文本前端展示, 文本主体内容后台编辑留有在占位符, 服务端填充占位输出客户端展示,流程大致如此。在自测/内测过程中都挺OK,毕竟就是很简单的功能开发。上线后也相安无事了一段时间,但后面发生的事情就较为诡异了。由于市场部门推广,此功能受众面迅速扩大,访问量相比未推广前曾几何级递增且服务器稳定性出现波动,经常性OOM且需要重启才能解决。朋友公司所在技术团队,根据OOM的堆转存储文件分析发现:OOM发生时,堆内存活着大量的String对象 , char[]数组,占据好几GB的堆内存,大大超过其他存活对象所占据的内存。

      问题看起来貌似很明朗了,实际好像也是这样。朋友所在技术团队经过模块排查逐步定位,基本确定了最近上线的一系列功能模块里, 大文本前端展示为首要原因,这个模块贡献了大量的存活且无法有效释放的char[]数组。问题原因似乎找到了,但为何会是这样的呢? 于是乎针对这个功能的二次代码review如影随形的开展了。代码逻辑如下:网页客户端发起请求--->Controller层[接受客户端请求]--->Service层[根据业务逻辑填充占位符]--->DAO层[读取后台编辑后存储在数据库中的大文本]数据载体String对象就这样层层向上传递最终输出到客户端。因为代码着实简单review完也没看出具体问题。示例代码:

```

public String getBigText(final String tId) throws SQLException {

ResultSet result = null; //这里null为模拟获取结果集.

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

return bigText;

}

```

先说下解决方案吧,问题很简单却也挺恼人。我们可以采用intern方法字符串池化, 对于大量重复的字符串使用得当可以节约内存。当然大文本渲染的方式的也需要发生变化,填充数据需要异步读取显示。示例代码:

```

public String getBigText(final String tId) throws SQLException {

ResultSet result = null; //这里null为模拟获取结果集.

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

return bigText;

}

```

回到文章的开头貌似与<论如何编写出与JVM自适应优化所契合的代码>这个大标题一点关系都没有啊。在解决沟通的过程中提及了两个概念: 1.针对大量重复字符串,不采用其他技术方案的情况下使用intern可以最小化代码改动并达到低内存占用的目标  2. intern后GC Root引用链是如何被切断的,它是对象内存可被快速释放的关键要素 [备注:GC Root引用链,即JVM对象引用判断-根检索算法链式模型]。

        Linux之父有一段名言:talk is cheap, show me the code!  示例代码如下(这段代码摘自网络,单纯用来演示这项技术):

```

package util;

import java.util.Random;

import java.util.concurrent.TimeUnit;

/**

JVM参数:

-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("执行完毕,可以开启JVM堆分析了...");

    TimeUnit.SECONDS.sleep(600); //这里暂停线程,挂载JVM分析器

}

}

```

我们先来一段由性能剖析软件发起的针对上面代码执行中, 强制GC前后的内存占比.

GC前:

GC后:

如图所示,Full GC后大量的String对象与char[]数组依然存活,由此我们根据JVM内存清理算法可以推导出这些对象与数组存在强引用。不知是否有注意int[] 数组在GC后由138MB释放到了21423kb,原因即方法执行结束调用已出栈没有被强引用所持有,所以在可释放的范围内。我们再接着看提到多次的GC Root引用链到底是什么鬼,见下图:

我们仔细看标蓝的部分,剖析软件基于此字符串对象根据根检索算法正向与反向推导出完整的GC Root引用链。在此情况下回到文章开头部分,大文本客户端渲染在用户访问量大的情况下,对大数据字符串的引用直到完成输出到客户端前都会存在强引用,但凡出现OOM也只有重启一条路可以走了。。。

-------------------------------华丽分割线-----------------------------------------

问题已经分析的接近尾声了,我们再聊下阻断GC Root链后会发生什么。把代码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前的图一样,就不重复贴了,直接上GC后的图:

小伙伴们惊讶么? 一次GC直接释放完毕了。。。这个时候对方法的调用尚未结束,因为线程sleep了。再引申一个技术点吧,N多的技术书籍都在传授栈内存会随着线程的消亡而释放,其实这句话原则上是对的,但如果非要死扣细节其实我愿意加上:如果方法执行过程中,数据已经出栈且不存在强引用,GC即可释放分配在堆中的无引用对象而无需等到方法全部执行完线程退出后。

题外话: 虽然intern方法是个好东西但是使用要慎重,起码你要真的懂才行。否则引起永久代内存溢出或者青年代GC耗时增大等问题也是非常头疼的且更隐晦! JVM常量池内部是一个HashSet数据结构,有容量的限制有参数可以控制,高版本的JDK貌似具备自动resize的功能,个人没做更多版本差异性研究,大家有兴趣可以自行研究。

题外案例:

1. 国外著名的微博twitter也曾经遇到过字符串高内存占用的问题,部分技术博客亦有提及,也是采用intern解决的。

2. 阿里fastJson框架也曾在intern方法上跌过坑,有兴趣可搜索学习。

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; 此处调用即为引用链阻断。 使得对象引用不可达最终被GC回收,避免了内存泄露。最后,由于intern调用将引用指向了常量池,所以new String创建的对象被阻断了。。。无引用持有,保证了GC回收。

写在最后,这是我的第一篇技术博客如有写的不对的地方,麻烦留言指正喔,在此非常感谢。

2019.4.1 晚 19:16分 geweixinerr,  End。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342