记一次java内存溢出的解决过程
注:本文主要记录这次解决内存溢出问题的过程而不是具体问题。
最近在写一个搜索引擎,使用倒排索引结构进行文档检索,保存索引的基本思想是先将倒排列表保存到内存中一个有序Map里(TreeMap),然后当内存占用达到一定阈值的时候将内存中的倒排列表有序写入磁盘,当磁盘已经存在索引时,则将内存中的索引和磁盘中的索引进行合并,生成新的索引,合并过程类似于归并排序。合并内存索引和磁盘索引的代码如下:
public synchronized void merge(){
LogUtil.info("InvertIndex merge start...");
File f=new File(path); //这个文件是原磁盘上的索引文件
//磁盘存在索引,合并磁盘索引和内存索引
if (f.exists()) {
String outPath=path+".temp";
File outFile=new File(outPath);
TreeMap<String, TreeSet<Long>> ramSnapshot=null;
ramSnapshot=ram; //ram保存的是内存索引,这里因为ram可能被其他添加文档的线程修改,因此先存一份快照然后将ram清空,后面实际操作的是快照
ram=new TreeMap<>();
BufferedReader reader=null;
PrintWriter writer=null;
try {
//合并过程,有3个指针:分别指向内存索引的当前位置、原磁盘索引读到的位置、新的磁盘索引写入的位置
Iterator<Entry<String, TreeSet<Long>>> ramIterator= ramSnapshot.entrySet().iterator();
reader=new BufferedReader(new FileReader(f));
writer=new PrintWriter(new BufferedWriter(new FileWriter(outFile)));
Entry<String, TreeSet<Long>> entry=ramIterator.hasNext()?ramIterator.next():null;
String line=reader.readLine();
while (entry!=null&&line!=null) {
long freeRam=Runtime.getRuntime().freeMemory()/1000/1000;
System.out.println("freeRam: "+freeRam);
String ramWord=entry.getKey();
String diskWord=line.split(separator1)[0];
String out="";
int c=ramWord.compareTo(diskWord);
//合并过程,因为是两个有序列表,采用类似归并排序的合并方法,区别在于这里如果遇到倒排词相等的时候,需要合并到一个倒排词(合并两者文档列表)
if (c==0) {
TreeSet<Long> ramDocIds=entry.getValue();
TreeSet<Long> diskDocIds=this.convertLine2DocIds(line);
TreeSet<Long> union=ramDocIds;
union.addAll(diskDocIds);
out=this.convertIndex2Line(ramWord, union);
entry=ramIterator.hasNext()?ramIterator.next():null;
line=reader.readLine();
}else if (c<0) {
out=this.convertIndex2Line(ramWord, entry.getValue());
entry=ramIterator.hasNext()?ramIterator.next():null;
}else {
out=this.convertIndex2Line(diskWord, this.convertLine2DocIds(line));
line=reader.readLine();
}
writer.println(out);
}
LogUtil.info("InvertIndex complex merge complete.");
while (ramIterator.hasNext()) {
entry=ramIterator.next();
String out=this.convertIndex2Line(entry.getKey(), entry.getValue());
writer.println(out);
}
LogUtil.info("InvertIndex ram merge complete.");
while ((line=reader.readLine())!=null) {
writer.println(line);
}
LogUtil.info("InvertIndex disk merge complete.");
} catch (Exception e) {
LogUtil.err("merge ram index and disk index fail.", e);
}finally {
try {
if (reader!=null) {
reader.close();
}
if (writer!=null) {
writer.close();
}
} catch (Exception e2) {LogUtil.err("release resource fail.", e2);}
}
f.delete();
if (!outFile.renameTo(new File(path))) {
throw new RuntimeException("rename temp file fail.");
}
}else {
//磁盘上原本不存在索引,直接将内存索引写入磁盘
//代码略
}
代码的主要思想是维持3个指针:内存索引是一个有序TreeMap,iterator相当于一个虚拟指针;磁盘索引也是有序的,BufferdReader相当于一个虚拟指针;合并后生成的新索引使用BufferdWriter作为指针。然后使用归并排序的思想,比较内存索引词和磁盘索引词的大小,哪个小就将哪个写入新的索引然后将其指针前进一位,如果两个索引词相等,则合并两者的文档列表。
从上面的描述来看以上代码使用的内存应该是O(1)的,因为内存中除了内存索引之外,同一时刻只会从磁盘索引读出一行,但是实际运行的时候,总是在合并时报出GC overhead limit exceeded,这个异常就是说jvm用了大量时间(超过98%)执行GC但是只释放了很少的堆内存(小于2%),换句话说就是OOM的前兆。根据我对程序的内存占用的分析,这种情况是不正常的。
于是给程序添加 “-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:/dump” 参数,在程序出现内存溢出异常的时候dump出内存信息。
用eclipse MAT查看占用内存最多的对象:

发现是TreeMap的实例,在这个程序中我用了TreeMap来保存内存索引,而在合并索引的过程中,内存索引的大小应该是不变的,那么为什么会溢出呢?
通过看代码将问题定位在了红色代码处:

这里首先将ramDocIds指向entry.getValue(),然后又将union指向ramDocIds,此时union实际上是直接指向entry.getValue()的,然后union执行了addAll操作。。众所周知,addAll操作是将元素加到对象本身的,这里我的原意是声明一个局部变量保存列表合并结果然后存入新的索引文件,但是无意中却同时修改了内存索引导致内存索引越来越大。
OK,到这里问题就解决了,只需要将
TreeSet<Long> union=ramDocIds;
改为
TreeSet<Long> union=new TreeSet<>(ramDocIds);
即可。
回顾一下这次解决内存溢出问题的过程:
- 分析程序空间复杂度,看内存溢出的是否正常。
- 添加jvm参数,让程序在内存溢出的时候dump出内存快照。
- 使用eclipse MAT分析占用内存最多的对象。
- 在源码中找到相应的对象查找问题。
记一次java内存溢出的解决过程的更多相关文章
- java内存溢出的解决思路
原文地址:https://www.cnblogs.com/200911/p/3965108.html 内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能 ...
- java内存溢出怎么解决
java.lang.OutOfMemoryError这个错误我相信大部分开发人员都有遇到过,产生该错误的原因大都出于以下原因:JVM内存过小.程序不严密,产生了过多的垃圾. 导致OutOfMemory ...
- 添加IFrame导致内存溢出的解决过程(IE浏览器,目前发现了原因,还未解决)
1. 现象 每次动态添加iframe时,iexplore.exe进程占据的内存都会增加(大概10M左右),不会自动释放,最终导致内存溢出 2. 解决过程 经过网络的一番搜索,基本上给出的解决方案是 ...
- Java 内存溢出(java.lang.OutOfMemoryError)的常见情况和处理方式总结
最近老是遇见服务器内存溢出的问题,故在网上搜了搜,总结了一些java内存溢出的解决方式 java.lang.OutOfMemoryError这个错误我相信大部分开发人员都有遇到过,产生该错误的原因大都 ...
- Java常见的几种内存溢出及解决方法
Java常见的几种内存溢出及解决方法[情况一]:java.lang.OutOfMemoryError:Javaheapspace:这种是java堆内存不够,一个原因是真不够(如递归的层数太多等),另一 ...
- android通过BitmapFactory.decodeFile获取图片bitmap报内存溢出的解决办法
android通过BitmapFactory.decodeFile获取图片bitmap报内存溢出的解决办法 原方法: public static Bitmap getSmallBitmap(Strin ...
- Java内存溢出详解
转自:http://elf8848.iteye.com/blog/378805 一.常见的Java内存溢出有以下三种: 1. java.lang.OutOfMemoryError: Java heap ...
- Java内存溢出的详细解决方案
本文介绍了Java内存溢出的详细解决方案.本文总结内存溢出主要有两种情况,而JVM经常调用垃圾回收器解决内存堆不足的问题,但是有时仍会有内存不足的错误.作者分析了JVM内存区域组成及JVM设置虚拟内存 ...
- 老李案例分享:定位JAVA内存溢出
老李案例分享:定位JAVA内存溢出 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.在poptest的loadrunner的培 ...
随机推荐
- LUCAS定理简述
Lucas定理解决的是n,m比较大而p是小于100000质数 简而言之就是Lucas(n,m)=C(n%p,m%p)*Lucas(n/p,m/p)%p; 其中组合数C是用任意一种计算10五次方内取模的 ...
- Redis的键值命令、服务器命令
Redis提供了丰富的命令对数据库和各种数据类型进行操作,这些命令可以在Linux 终端使用.在编程时,比如各类语言包,这些命令都有对应的方法. 键值命令 服务器命令 获取数据库中所有键名 >k ...
- 怎样才能高效地使用JQuery
1. 使用最新版本的jQuery jQuery的版本更新很快,你应该总是使用最新的版本.因为新版本会改进性能,还有很多新功能.下面就来看看,不同版本的jQuery性能差异有多大.这里是三条最常见的jQ ...
- git使用笔记(十四)cat-file
By francis_hao Mar 18,2018 git cat-file :提供仓库中对象实体的类型.大小和内容的信息 概要 git cat-file (-t | -s | -e | ...
- MySQL数据库中tinyint字段值为1,读取出来为true的问题
原文:https://blog.csdn.net/shuyou612/article/details/46788475 MySQL数据库中tinyint字段值为1,读取出来为true的问题 今天在 ...
- poj1850 Code
Code Time Limit: 1000MS Memory Limit: 30000K Total Submissions: 10059 Accepted: 4816 Description ...
- golang设置代理
http://note.youdao.com/noteshare?id=a8df0ec2d623f282a782dbe937bdae9f
- 单例 ------ C++实现
基础知识掌握: 单例考虑三点:内存何时释放.运行速度如何.多线程下能否保证只有一个实例 如果获取对象的返回值类型是引用,返回值赋值给变量而不是引用会进行对象的拷贝,这样就会出现两个对象,可以把显示声明 ...
- 前端PHP入门-015-递归函数-飘过
提要: 如果感觉吃力,web前端的各位同学,可以弃之. 递归函数,递归只是一个名字,而递归函数的规定: 函数体内调用函数自己 . 我们来计算 10!=10*9*8*7*6*5*4*3*2*1 web前 ...
- C++对象与其第一个非静态成员地址相同
由于对象的内存空间里是按顺序依次存储了它的非静态成员变量,因此对象和它的第一个非静态成员变量的地址是相同的: class A { private: int i; } int main() { A a; ...