发现问题
今天上午( 周一早晨)吃完早餐伸个懒腰准备开始一天美好(苦逼)的代码生活,突然测试MM发来消息说应用不可用了,说周末加班就不能使用了(周末的班白加了)。接到消息立马打开error日志,一看原来是OOM了,莫名的有点兴奋,终于在实际应用逮到你了。二话不说看了下gcutil,发现FGC 70000次左右,YGC 2000次左右,我滴乖乖,一直在FGC啊,那肯定是某个对象内存溢出了,一直回收不了。看了下堆内存实例数的统计,果然有个对象实例数是400多万,找到了对象,是一个配置信息的对象,问题还是比较好定位的。
以上用到的命令如下,由于是公司的代码,就不上具体截图了,
jstat -gcutil pid
实例数最多的前20个
jmap -histo pid | head -20
排查问题
以下这段代码就是问题点的所在,之前该代码是在项目启动的时候加载一次,也就是将数据库的配置文件加载进来就不会再变了,在测试环境经常需要该配置,每次都要重启,很麻烦,测试就让加个定时刷新,然后下面这段代码就被丢到了定时任务里去了,每分钟跑一次,结果list没清理就OOM了。
public static Map<String, List<TestUser>> objMap = new HashMap<String, List<TestUser>>();
//读取数据库资源并初始化,这是为了模拟实际代码写的
public static void initObjMap() {
List<TestUser> objList = null;//原代码是读取数据库的配置信息
//将user按name归类,原代码就是要实现这样的逻辑,初始化配置信息
if (!CollectionUtils.isEmpty(objList)) {
for (TestUser tu : objList) {
if (null == objMap.get(tu.getName())) {
List<TestUser> testUserList = new ArrayList<TestUser>();
testUserList.add(tu);
objMap.put(tu.getName(), testUserList);
} else {
List<TestUser> testUserList = objMap.get(tu.getName());
//这里如果只加载一次没什么问题,但是为了动态刷新配置信息,每分钟加载一次就出问题了,
//不停的往List中加对象,FGC根本无法回收,最终OOM
testUserList.add(tu);
}
}
}
}
解决问题
以下是修改后的代码,每次加载时都创建新的对象,重指向后老对象就能释放,被垃圾回收器回收。但是objMap = collect这个操作是否是原子性的还有待研究,也就是会不会出现在用objMap 时,objMap 还未指向到新对象,但是又已经释放了老对象的时候?
//修改后的代码
//简洁,不存在多次加载内存溢出的问题
Map<String, List<TestUser>> collect = objList.stream().collect(Collectors.groupingBy(
TestUser::getName,
Collectors.toList()
));
//TODO 这一步是不是原子操作有待确认,也就是objMap除了指向新老对象外还有没有其他的状态?
//TODO 实验结果是不会有并发问题
objMap = collect;
不过从测试效果来看不存在这样的问题,测试代码为一个线程不停的改,100个线程不停的读,未出现异常。测试代码如下
public class TestReplaceMap {
public static Map<String, TestUser> objMap = new HashMap<String, TestUser>();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
for (; ; ) {
replaceMap();
}
}).start();
Thread.sleep(1000);//确保资源加载了
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (; ; ) {
printObj();
}
}).start();
}
}
public static void replaceMap() {
Map<String, TestUser> initMap = new HashMap<String, TestUser>();
initMap.put("aa", new TestUser("aa", "addw"));
initMap.put("bb", new TestUser("bb", "addw"));
initMap.put("cc", new TestUser("cc", "addw"));
initMap.put("dd", new TestUser("dd", "addw"));
objMap = initMap;
}
public static void printObj() {
if (null == objMap.get("aa")) System.out.println("error");
if (null == objMap.get("bb")) System.out.println("error");
if (null == objMap.get("aa")) System.out.println("error");
if (null == objMap.get("cc")) System.out.println("error");
if (null == objMap.get("dd")) System.out.println("error");
if (null == objMap.get("dd")) System.out.println("error");
if (null == objMap.get("dd")) System.out.println("error");
if (null == objMap.get("cc")) System.out.println("error");
if (null == objMap.get("bb")) System.out.println("error");
if (null == objMap.get("aa")) System.out.println("error");
}
}
class TestUser {
private String name;
private String address;
public TestUser(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}