前言
最近写了个复杂的数据处理程序,测试的时候在小数据量的场景下一点问题都没有,当部署到生产环境,发现执行不了,报了 java heap space 和 GC overhead limit exceeded 的错误,由于jvm的内存我们都是给最大的,而且明明内存都没满,怎么就报错了,只能从代码入手看是什么问题。
问题介绍
- java heap space:内存溢出
- GC overhead limit exceeded:JVM花费了98%的时间进行垃圾回收,而只得到2%可用的内存
问题重现
我使用的电脑配置不是很好,所以如果你的电脑比我的好,我下面的程序你就不会出现异常。
1、建立实体
package com.example.elasticsearch.entity;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Builder
public class Item {
private Long id;
private String title; //标题
private String category;// 分类
private String brand; // 品牌
private BigDecimal price; // 价格
private String images; // 图片地址
}
2、建立测试程序
package com.example.elasticsearch.test;
import com.example.elasticsearch.entity.Item;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
public class JvmTest {
@Test
public void testJvm() {
List<Item> items = new ArrayList<>();
for(int i = 0; i < 3000000; i++) {
items.add(Item.builder()
.id((long) i)
.title("手机" + i)
.category("手机")
.brand("大米")
.price(BigDecimal.valueOf(i))
.images("http://www.image/" + i + ".png")
.build());
}
log.info("执行1完成");
List<Item> items2 = new ArrayList<>();
for(int i = 0; i < 3000000; i++) {
items2.add(Item.builder()
.id((long) i)
.title("手机" + i)
.category("手机")
.brand("大米")
.price(BigDecimal.valueOf(i))
.images("http://www.image/" + i + ".png")
.build());
}
log.info("执行2完成");
List<Item> items3 = new ArrayList<>();
for(int i = 0; i < 3000000; i++) {
items3.add(Item.builder()
.id((long) i)
.title("手机" + i)
.category("手机")
.brand("大米")
.price(BigDecimal.valueOf(i))
.images("http://www.image/" + i + ".png")
.build());
}
log.info("执行3完成");
}
}
3、执行结果
可以看到控制台输出中报错了,然后我们打开JDK下的内存监听程序(jdk/bin/jvisualvm.exe),这是jdk自带的,再次执行程序,找到你程序的tomcat,如下图:
可以看到堆内存一直在增长,直到内存溢出。
分析解决办法
从问题中不难看出,由于内存一直在增长,最终导致内存溢出,那有没有办法把没用的内存释放掉呢?重新检查代码,这个例子中的items和items2集合都是没用的,却在占着内存,可以从这里入手。
于是,将上面的建立测试程序修改成如下:
package com.example.elasticsearch.test;
import com.example.elasticsearch.entity.Item;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
public class JvmTest {
@Test
public void testJvm() {
List<Item> items = new ArrayList<>();
for(int i = 0; i < 3000000; i++) {
items.add(Item.builder()
.id((long) i)
.title("手机" + i)
.category("手机")
.brand("大米")
.price(BigDecimal.valueOf(i))
.images("http://www.image/" + i + ".png")
.build());
}
log.info("执行1完成");
items.clear();
List<Item> items2 = new ArrayList<>();
for(int i = 0; i < 3000000; i++) {
items2.add(Item.builder()
.id((long) i)
.title("手机" + i)
.category("手机")
.brand("大米")
.price(BigDecimal.valueOf(i))
.images("http://www.image/" + i + ".png")
.build());
}
log.info("执行2完成");
items2.clear();
List<Item> items3 = new ArrayList<>();
for(int i = 0; i < 3000000; i++) {
items3.add(Item.builder()
.id((long) i)
.title("手机" + i)
.category("手机")
.brand("大米")
.price(BigDecimal.valueOf(i))
.images("http://www.image/" + i + ".png")
.build());
}
log.info("执行3完成");
}
}
从上面的代码中不难发现,我就是把items和items2集合不用的时候做了个clear清除操作,执行结果却大不相同:
可以看到,程序执行过程中,多了两次内存释放回收的操作,从而让程序不会内存溢出。
相关扩展
java运行环境包含了一个内置的Garbage Collection (GC)垃圾回收进程,用于对不在使用的内存区域进行回收,释放被占用的内存,jvm会根据程序的运行情况,执行GC垃圾回收操作。java语言,程序员只需关注内存的分配,无需关注内存的回收。
这种机制也会有一些问题,就是被占用的内存,经过多次长时间的GC操作都无法回收,导致可用内存越来越少,俗称内存泄露
如果没有这个异常,会出现什么情况呢?经过垃圾回收释放的2%可用内存空间会快速的被填满,迫使GC再次执行,出现频繁的执行GC操作, 服务器会因为频繁的执行GC垃圾回收操作而达到100%的时使用率,服务器运行变慢,应用系统会出现卡死现象,平常只需几毫秒就可以执行的操作,现在需要更长时间,甚至是好几分钟才可以完成。
我之前也遇到过”JVM花费了98%的时间进行垃圾回收,而只得到2%可用的内存“这种情况,如图示可以看出,CUP的活动与GC占用的CPU已经相等,导致内存溢出:
这个问题是我有一个查询数据库的操作,由于Mongodb的数据结构大,数据量多,一次查询出来导致内存飙升,GC一直寻找可以释放回收的内存,可是回收的却很少。这个问题只需要将大数据量的数据分批次返回即可。
/**
* 一次性返回代码
**/
@Test
public void testJvm() {
List<Item> items = itemService.findAll();
}
/**
* List集合截取
* @param list 集合
* @param toIndex 截取量
* @return List<List<String>>
*/
public static List<List<String>> groupStringList(List<String> list, int toIndex) {
List<List<String>> listGroup = new ArrayList<>();
int listSize = list.size();
for (int i = 0; i < list.size(); i += toIndex) {
if (i + toIndex > listSize) {
toIndex = listSize - i;
}
List<String> newList = list.subList(i, i + toIndex);
listGroup.add(newList);
}
return listGroup;
}
/**
* 分批次返回代码
**/
@Test
public void testJvm() {
// 获取所有的ID集合
List<String> ids = itemService.findAllIds();
// 将ID集合截取成最大1000的小集合
groupStringList(ids, 1000).forEach(smallIds -> {
List<Item> items = itemService.findByIds(smallIds);
System.out.println(items.size());
items.clear();
});
}
总结
遇到问题,多分析问题,就能找到解决问题的办法。