事情是这样的。作为一个兢兢业业勤奋的小码农,虽然达不到对代码不能自拔的地步。但!我们已经养成了抓bug的技能,消灭程序bug已经成为一种人生目标,打算长期保持下去。本以为自己可以做一个安全快乐的码农,没想到老大的话如雷贯耳,击碎了我的初衷。老板让我写个bug。怎么了?标题是对的,真是让我写了个bug!刚接到这个请求的时候,我心里没有丝毫的波澜,甚至还有点小激动。这是我的特长,终于可以光明正大的写bug了。我们先来看看具体情况。其实主要是为了让一些负载很低的服务器多消耗一些内存、CPU等资源(后台我就不多说了),从而增加它的负载。JVM内存分配回顾于是就匆匆写了代码,大致如下:写完之后我在想一个问题,代码中的mem对象会不会在方法执行完后立即被回收?我想肯定有一些人认为方法执行完就被回收了。我也去认真调查了一下,问了一些朋友;正如预期的那样,他们中的一些人认为它在方法执行后被回收了。事实呢?我做了一个实验。我启动了刚才的应用程序,启动参数如下:这样我就可以通过JMX端口远程连接到这个应用程序,观察内存和GC情况。如果方法执行后mem对象被回收,当我分配250M内存时;内存会有明显的曲线,GC也会执行。这时候观察一下内存曲线,如下图:你会发现确实有明显的增加,但是之后并没有马上恢复,而是一直保持在这个水平上。同时左边的GC没有响应。用jstat查看内存布局时也是如此,如下图:无论是YGC还是FGC,Eden区的使用率都增加了。毕竟分配了250M内存。那怎么回收呢?我又分配了两个250M,然后观察内存曲线。当找到第三个250M时,Eden区达到了98.83%。因此,再次分配时,需要回收Eden区来生成YGC。同时内存曲线也有所降低。整个转换过程如下:由于初始化的堆内存为4G,所以计算出的Eden区约为1092M内存。加上应用程序启动Spring消耗的内存约20%,所以分配250M内存3次就会导致YGC。我们再回顾一下刚才的问题:既然mem对象执行完方法不会被回收,那么什么时候回收呢?其实只要记住一件事:对象只有在垃圾回收器发生GC时才能被回收;对象是否是局部变量也是全局变量。通过刚才的实验,我们也发现我们创建的mem对象在Eden区空间不足产生YGC时会被回收。但是这里其实有一个隐藏的条件:就是这个对象是一个局部变量。如果对象是全局变量,还是不能回收。也就是我们常说的对象不可达,这样说不可达的对象在GC发生的时候会被认为是需要回收的对象,会被回收。想多了,为什么有些人会认为局部变量在方法执行后会被回收呢?我想这应该是记错了。实际上,栈帧是在方法执行完之后被回收的。它最直接的结果就是没有引用mem对象。但是没有引用并不代表它马上就会被回收,也就是说它需要产生GC才能被回收。因此,上面提到的不可达对象中使用的可达性分析算法就是用来指出哪些对象需要被回收。当对象没有被引用时,它被认为是不可达的。这里有一个清晰的动图:方法执行的时候,里面的mem对象相当于图中的Object5,所以在GC的时候会被回收。对象首先分配在Eden区。从上面的例子可以看出,对象先分配在新生代的Eden区,但是有一个前提,就是对象不能太大。之前也写过相关内容:大对象直接进入老年代,大对象直接分配给老年代(至于多大算大,可以通过参数配置)。当我直接分配1000M内存时,由于Eden区不能直接加载,所以在老年代分配。可以看到Eden区几乎没有变化,但是oldgeneration增加了37%。按照之前计算的老年代内存2730M,差不多是1000M内存。Linux内存查看回到我这次需要完成的需求:增加服务器内存和CPU消耗。CPU还好,使用到一定程度,每创建一个对象,也会消耗一些CPU。主要是内存,启动这个应用前先看看内存情况:大概只用了3G内存。启动应用后,只消耗了600M左右的内存。为了满足需求我需要分配一些内存,但是这里有点棘手。内存不能一直分配,会导致CPU负载过高,同时内存不会因为GC回收而占用过多。所以我需要少量的分配,让大部分对象都在年轻代,需要保持在80%或90%,以免被回收。同时,也需要将一些大对象分配给老年代,让老年代的使用率保持在80%~90%。这样就可以最大程度的利用4G堆内存。所以我做了以下操作:首先在新生代(800M)中分配一些小对象,并将新生代保持在90%。然后老年代分配*(100%-28%使用);即2730*60%=1638M这样老年代也有90%左右。效果如上。最重要的是GC没有发生,所以我的目的达到了。最终内存消耗约为3.5G。总结虽然这次的需求比较奇怪,但是要精确控制JVM的内存分配并不是那么容易的。你需要对其内存布局和回收有一定的了解。写这个bug的过程,确实加深了大家的印象。如果对你有帮助,请不要吝啬你的点赞和分享。
