没错,老板让我写个 BUG!

前言

标题没有看错,真的是让我写个 bugjava

刚接到这个需求时我心里没有丝毫波澜,甚至还有点激动。这但是我特长啊;终于能够光明正大的写 bug 了🙄。算法

先来看看具体是要干啥吧,其实主要就是要让一些负载很低的服务器额外消耗一些内存、CPU 等资源(至于背景就很少说了),让它的负载能够提升一些。服务器

JVM 内存分配回顾

因而我刷刷一把梭的就把代码写好了,大概以下:布局

写完以后我就在想一个问题,代码中的 mem 对象在方法执行完以后会不会被当即回收呢?我想确定会有一部分人认为就是在方法执行完以后回收。spa

我也正儿八经的去调研了下,问了一些朋友;果不其然确实有一部分认为是在方法执行完毕以后回收。code

那事实状况如何呢?我作了一个试验。server

我用如下的启动参数将刚才这个应用启动起来。对象

java -Djava.rmi.server.hostname=10.xx.xx.xx 
-Djava.security.policy=jstatd.all.policy 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.port=8888  
-Xms4g -Xmx4g  -jar bug-0.0.1-SNAPSHOT.jar

这样我就能够经过 JMX 端口远程链接到这个应用观察内存、GC 状况了。内存


若是是方法执行完毕就回收 mem 对象,当我分配 250M 内存时;内存就会有一个明显的曲线,同时 GC 也会执行。ssl


这时观察内存曲线。

会发现确实有明显的涨幅,可是以后并无当即回收,而是一直保持在这个水位。同时左边的 GC 也没有任何的反应。

jstat 查看内存布局也是一样的状况。

不论是 YGC,FGC 都没有,只是 Eden 区的使用占比有所增长,毕竟分配了 250M 内存嘛。

那怎样才会回收呢?

我再次分配了两个 250M 以后观察内存曲线。

发现第三个 250M 的时候 Eden 区达到了 98.83% 因而再次分配时就须要回收 Eden 区产生了 YGC

同时内存曲线也获得了降低。

整个的换算过程如图:

因为初始化的堆内存为 4G,因此算出来的 Eden 区大概为 1092M 内存。

加上应用启动 Spring 之类消耗的大约 20% 内存,因此分配 3 次 250M 内存就会致使 YGC

再来回顾下刚才的问题:

mem 对象既然在方法执行完毕后不会回收,那何时回收呢。

其实只要记住一点便可:对象都须要垃圾回收器发生 GC 时才能回收;无论这个对象是局部变量仍是全局变量。

经过刚才的实验也发现了,当 Eden 区空间不足产生 YGC 时才会回收掉咱们建立的 mem 对象。

但这里其实还有一个隐藏条件:那就是这个对象是局部变量。若是该对象是全局变量那依然不能被回收。

也就是咱们常说的对象不可达,这样不可达的对象在 GC 发生时就会被认为是须要回收的对象从而进行回收。

在多考虑下,为何有些人会认为方法执行完毕后局部变量会被回收呢?

我想这应当是记混了,其实方法执行完毕后回收的是栈帧

它最直接的结果就是致使 mem 这个对象没有被引用了。但没有引用并不表明会被立刻回收,也就是上面说到的须要产生 GC 才会回收。

因此使用的是上面提到的对象不可达所采用的可达性分析算法来代表哪些对象须要被回收。

当对象没有被引用后也就认为不可达了。

这里有一张动图比较清晰:

当方法执行完以后其中的 mem 对象就至关于图中的 Object 5,因此在 GC 时候就会回收掉。

优先在 Eden 区分配对象

其实从上面的例子中能够看出对象是优先分配在新生代中 Eden 区的,但有个前提就是对象不能太大。

之前也写过相关的内容:

大对象直接进入老年代

而大对象则是直接分配到老年代中(至于多大算大,能够经过参数配置)。


当我直接分配 1000M 内存时,因为 Eden 区不能直接装下,因此改成分配在老年代中。

能够看到 Eden 区几乎没有变更,可是老年代却涨了 37% ,根据以前计算的老年代内存 2730M 算出来也差很少是 1000M 的内存。

Linux 内存查看

回到此次我须要完成的需求:增长服务器内存和 CPU 的消耗。

CPU 还好,自己就有必定的使用,同时每建立一个对象也会消耗一些 CPU。

主要是内存,先来看下没启动这个应用以前的内存状况。

大概只使用了 3G 的内存。

启动应用以后大概只消耗了 600M 左右的内存。

为了知足需求我须要分配一些内存,但这里有点须要讲究。

不能一直分配内存,这样会致使 CPU 负载过高了,同时内存也会因为 GC 回收致使占用也不是特别多。

因此我须要少许的分配,让大多数对象在新生代中,为了避免被回收须要保持在百分之八九十。

同时也须要分配一些大对象到老年代中,也要保持老年代的使用在百分之八九十。

这样才能最大限度的利用这 4G 的堆内存。

因而我作了如下操做:

  • 先分配一些小对象在新生代中(800M)保持新生代在90%
  • 接着又分配了老年代内 *(100%-已使用的28%);也就是 2730*60%=1638M 让老年代也在 90% 左右。

效果如上。

最主要的是一次 GC 都没有发生这样也就达到了个人目的。

最终内存消耗了 3.5G 左右。

总结

虽然说此次的需求是比较奇葩,但想要精确的控制 JVM 的内存分配仍是没那么容易。

须要对它的内存布局,回收都要有必定的了解,写这个 Bug 的过程确实也加深了印象,若是对你有所帮助请不要吝啬你的点赞与分享。

你的点赞与分享是对我最大的支持