Java的内存回收任务都是 JVM完成,一般不会出现内存泄漏。
如果程序出现内存增长的现象,一般原因是对象持续增加没有被gc销毁
为什么不会被销毁,内存的回收好比一张图,对象是一个节点,对象之间的引用关系是有向边,如果是循环引用,就会形成一个回路
节点只有在变成孤岛之后,才会标记为“垃圾”。
假设一个对象A被B持有,B被根对象C持有,A被销毁了,图中A的边变成0,因此它是一个孤岛对象,一段时间内必然被gc回收。而B还在,因为它的母对象C还持有它,那么gc不会销毁它。
又假如,对象B被销毁,这种情况,在C++中容易造成内存泄漏,因为B的子对象A的指针很容易变成悬挂指针,但是在Java 大可以放心,一旦B被销毁,它持有的对象 A如果引用计数(图的边)变成 0,那么它肯定是会被gc回收的。
总之,Java多数情况下你是可以放心地在 Heap中生成(new) 新对象的。
但是总是有些例外。
我遇到的问题就是很典型的容器持有对象超量——一个队列进出的节奏,顺序发生根本性的逆差——进的非常快,而出的非常慢,导致这个队列没有设置容量上限的情况下,可以把分配给jvm的堆最大上限完全吃完。造成不响应。
而 容器对象又是static 静态对象,它存在于整个程序的生命周期,除了重启,没有别的办法。
解决的方法就是,加快消费速度,设置队列上限——一定要设置上限,总有意料之外的事情发生,因为你无法预知消费速度一定大于生成速度。
public class MessageHandler extends AbstractMessageHandler {
private static LinkedBlockingQueue<MessagePacket> messageQueue = new LinkedBlockingQueue<>();
@PostConstruct
public void init(){
for (int i = 0; i < 3; ++i) {
new Thread(this::taskA, "task-A" + i).start();
}
for (int i = 0; i < 5; ++i) {
new Thread(this::taskB, "taskB-" + i).start();
}
}
public void taskB() {
while (true){
MessagePacket packet = messageQueue.poll();
if(packet == null){
try {
Thread.sleep(200);
continue;
}catch (InterruptedException e){
continue;
}
}
try {
... //省略
switch (msgCode){
case 1001: {
...// 省略
}
case 1002:{
... // 省略
ClientModel client = getSomeClient();
if(client == null){
Thread.sleep(2000); // 这有可能导致队列积压
continue;
}
SomeObj info = client.getSomeObjInfoById(someId);
if(info == null){
Thread.sleep(2000); // 这有可能导致队列积压
continue;
}
... // 省略代码
break;
}
default:
... //
}
}catch (Exception e){
e.printStackTrace();
}
}
}
以上代码中taskB是一个消费线程,中间有一种异常情况,前作者不知为何加上了 Thread.sleep(2000) 停顿两秒钟的逻辑,也许是觉得这时出现有这种错误的时候,系统需要一点时间恢复一下——但事实上不会——于是等待一下再试,但是程序运行中,居然大量出现这种错误, 然后这些包源源不断往队列中积压,而程序的2秒钟停顿,导致一分钟只能消费30个包,加上多线程 5个,一分钟最后消费 150个包,而调试中发现这个队列吃完JVM内存后后有大约8万多个包,程序的UDP管道停止收包之后,要处理完这 8万异常包也需要 80000 / 150 ≈ 530 分钟