一,内存溢出和内存泄露
1,内存溢出:你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,出现溢出。
2,内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。
二,内存溢出
java.lang.OutOfMemoryError,程序在申请内存时,没有足够的内存空间使用,出现OutOfMemoryError。
程序提现:
一般在程序提现为:
错误提示
此错误常见的错误提示:
tomcat:java.lang.OutOfMemoryError: PermGen space
tomcat:java.lang.OutOfMemoryError: Java heap space
weblogic:Root cause of ServletException java.lang.OutOfMemoryError
resin:java.lang.OutOfMemoryError
java:java.lang.OutOfMemoryError
解决办法
三,内存泄露
Memory Leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:
如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
关于内存泄露的处理页就是提高程序的健壮型,因为内存泄露是纯代码层面的问题。
Java heap space
这个错误信息不一定意味着就是内存泄漏,事实上,这个错误类似配置错误,是很简答的问题。
举个例子,有一个应用经常出现这种类型的OOM异常,要我来分析这个问题。经过检查,我发现出问题的原因在于应用中有段代码会实例化一个占用内存空间较大的数组;在这种情况下,这并不是应用程序的错误,而是在应用服务器配置中,使用了默认的heap memory大小,这太小了。我仅仅只是通过调整Xms就解决了。
另外一种情况,特别是针对运行了较长时间的应用,这个信息可能就意味着应用中有可能存在未释放引用的对象,导致垃圾回收器无法及时回收空间。这是Java语言中最标准的内存泄漏(在API中的定义描述为:unintentionally holding object references)。
另一个潜在的导致Java heap space OOM的原因是使用了finalizer。如果一个类有finalize方法,那么这个类型的对象不会在垃圾回收时间收回对应的空间,而会等到垃圾回收结束之后,对象进入finalize队列排队,等待finalize执行。在Sun公司的JVM实现中,finalizer是由一个守护线程执行的。如果这个finalizer线程被终止了,而等待finalize的对象仍然在finalize队列中,那么这些对象就无法被正常回收,这个时候OOM就有可能发生了。
PermGen space
这个错误信息意味着永久代空间被占满了[注:Java8中已经去掉永久代]。永久代空间是用来存储class对象和method对象的堆空间。如果一个应用需要加载大量的类,则需要通过调整-XX:MaxPermSize参数来扩大永久代空间大小。
驻留的字符串对象(Intered String Object)也是存储在永久代中的。由java.lang.String类持有一个string对象的字符串常量池。当String的intern方法被调用,这个方法会首先在字符串常量池中检查是否有相等的字符串存在,如果是,这个字符串常量池中的string对象会被intern方法返回,如果不存在,这个string会被添加到字符串常量池中。使用专业术语来说,java.lang.String.intern方法返回一个字符串的不可变形式。如果一个应用中有大量的驻留字符串,你也需要增加永久代空间的大小。
提示:你可以使用jmap -permgen命令输出针对永久代的统计信息,包含驻留字符串相关的信息。[注:Java8中没有这个选项,直接通过jmap -heap pid就能看到intern字符串信息]
内存溢出和内存泄露的联系
内存泄露会最终会导致内存溢出。
相同点:都会导致应用程序运行出现问题,性能下降或挂起。
不同点:1) 内存泄露是导致内存溢出的原因之一,内存泄露积累起来将导致内存溢出。2) 内存泄露可以通过完善代码来避免,内存溢出可以通过调整配置来减少发生频率,但无法彻底避免。
一个Java内存泄漏的排查案例
某个业务系统在一段时间突然变慢,我们怀疑是因为出现内存泄露问题导致的,于是踏上排查之路。
确定频繁Full GC现象
首先通过“虚拟机进程状况工具:jps”找出正在运行的虚拟机进程,最主要是找出这个进程在本地虚拟机的唯一ID(LVMID,Local Virtual Machine Identifier),因为在后面的排查过程中都是需要这个LVMID来确定要监控的是哪一个虚拟机进程。
同时,对于本地虚拟机进程来说,LVMID与操作系统的进程ID(PID,Process Identifier)是一致的,使用Windows的任务管理器或Unix的ps命令也可以查询到虚拟机进程的LVMID。
jps命令格式为:
jps [ options ] [ hostid ]
使用命令如下:
使用jps:
jps -l
使用ps:
ps aux | grep tomat
找到你需要监控的ID(假设为20954),再利用“虚拟机统计信息监视工具:jstat”监视虚拟机各种运行状态信息。
jstat命令格式为:
jstat [ option vmid [interval[s|ms] [count]] ]
使用命令如下:
jstat -gcutil 20954 1000
意思是每1000毫秒查询一次,一直查。gcutil的意思是已使用空间站总空间的百分比。
结果如下图:
jstat执行结果
查询结果表明:这台服务器的新生代Eden区(E,表示Eden)使用了28.30%(最后)的空间,两个Survivor区(S0、S1,表示Survivor0、Survivor1)分别是0和8.93%,老年代(O,表示Old)使用了87.33%。程序运行以来共发生Minor GC(YGC,表示Young GC)101次,总耗时1.961秒,发生Full GC(FGC,表示Full GC)7次,Full GC总耗时3.022秒,总的耗时(GCT,表示GC Time)为4.983秒。
找出导致频繁Full GC的原因
分析方法通常有两种:
jmap命令格式:
jmap [ option ] vmid
使用命令如下:
jmap -histo:live 20954
存活对象
按照一位IT友的说法,数据不正常,十有八九就是泄露的。在我这个图上对象还是挺正常的。
我在网上找了一位博友的不正常数据,如下:
image.png
可以看出HashTable中的元素有5000多万,占用内存大约1.5G的样子。这肯定不正常。
查看堆内存(histogram)中的对象数量,大小
num #instances #bytes class name
序号 实例个数 字节数 类名
----------------------------------------------
1: 3174877 107858256 [C
2: 3171499 76115976 java.lang.String
3: 1397884 38122240 [B
4: 214690 37785440 com.tongli.book.form.Book
5: 107345 18892720 com.tongli.book.form.Book
6: 65645 13953440 [Ljava.lang.Object;
7: 59627 7648416
8: 291852 7004448 java.util.HashMap$Entry
9: 107349 6871176 [[B
..........
total 9150732 353969416
查看java 堆(heap)使用情况
jmap -heap pid
查看java 堆(heap)使用情况
using thread-local object allocation.
Parallel GC with 4 thread(s) //GC 方式 Heap Configuration: //堆内存初始化配置
MinHeapFreeRatio=40 //对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40)
MaxHeapFreeRatio=70 //对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)
MaxHeapSize=512.0MB //对应jvm启动参数-XX:MaxHeapSize=设置JVM堆的最大大小
NewSize = 1.0MB //对应jvm启动参数-XX:NewSize=设置JVM堆的‘新生代’的默认大小
MaxNewSize =4095MB //对应jvm启动参数-XX:MaxNewSize=设置JVM堆的‘新生代’的最大大小
OldSize = 4.0MB //对应jvm启动参数-XX:OldSize=:设置JVM堆的‘老生代’的大小
NewRatio = 8 //对应jvm启动参数-XX:NewRatio=:‘新生代’和‘老生代’的大小比率
SurvivorRatio = 8 //对应jvm启动参数-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值
PermSize= 16.0MB //对应jvm启动参数-XX:PermSize=:设置JVM堆的‘永生代’的初始大小
MaxPermSize=64.0MB //对应jvm启动参数-XX:MaxPermSize=:设置JVM堆的‘永生代’的最大大小
Heap Usage: //堆内存分步
PS Young Generation
Eden Space: //Eden区内存分布
capacity = 20381696 (19.4375MB) //Eden区总容量
used = 20370032 (19.426376342773438MB) //Eden区已使用
free = 11664 (0.0111236572265625MB) //Eden区剩余容量
99.94277218147106% used //Eden区使用比率
From Space: //其中一个Survivor区的内存分布
capacity = 8519680 (8.125MB)
used = 32768 (0.03125MB)
free = 8486912 (8.09375MB)
0.38461538461538464% used
To Space: //另一个Survivor区的内存分布
capacity = 9306112 (8.875MB)
used = 0 (0.0MB)
free = 9306112 (8.875MB)
0.0% used
PS Old Generation //当前的Old区内存分布
capacity = 366280704 (349.3125MB)
used = 322179848 (307.25464630126953MB)
free = 44100856 (42.05785369873047MB)
87.95982001825573% used
PS Perm Generation //当前的 “永生代” 内存分布
capacity = 32243712 (30.75MB)
used = 28918584 (27.57891082763672MB)
free = 3325128 (3.1710891723632812MB)
89.68751488662348% used
定位代码
定位带代码,有很多种方法,比如前面提到的通过MAT查看Histogram即可找出是哪块代码。——我以前是使用这个方法。 也可以使用BTrace。
举例:
一台生产环境机器每次运行几天之后就会莫名其妙的宕机,分析日志之后发现在tomcat刚启动的时候内存占用比较少,但是运行个几天之后内存占用越来越大,通过jmap命令可以查询到一些大对象引用没有被及时GC,这里就要求解决内存泄露的问题。
Java的内存泄露多半是因为对象存在无效的引用,对象得不到释放,如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析:
以下一步步的按照项目实例来操作,去解决内存泄露的问题。
登录linux服务器,获取tomcat的pid,命令:
ps -ef|grep java
利用jmap初步分析内存映射,命令:
jmap -histo:live 3514 | head -7
如果上面一步还无法定位到关键信息,那么需要拿到heap dump,生成离线文件,做进一步分析,命令:
jmap -dump:live,format=b,file=heap.hprof 3514
拿到heap dump文件,利用eclipse插件MAT来分析heap profile。
安装MAT插件
在eclipse里切换到Memory Analysis视图
用MAT打开heap profile文件。
直接看到下面Action窗口,有4种Action来分析heap profile,介绍其中最常用的2种:
Shllow Heap排序后发现 Cms_Organization 这个类占用的内存比较多(没有得到及时GC),查看引用:
分析引用栈,找到无效引用,打开源码
有问题的源码如下:
public class RefreshCmsOrganizationStruts implements Runnable{
private final static Logger logger = Logger.getLogger(RefreshCmsOrganizationStruts.class);
private List<Cms_Organization> organizations;
private OrganizationDao organizationDao = (OrganizationDao) WebContentBean
.getInstance().getBean("organizationDao");
public RefreshCmsOrganizationStruts(List<Cms_Organization> organizations) {
this.organizations = organizations;
}
public void run() {
Iterator<Cms_Organization> iter = organizations.iterator();
Cms_Organization organization = null;
while (iter.hasNext()) {
organization = iter.next();
synchronized (organization) {
try {
organizationDao.refreshCmsOrganizationStrutsInfo(organization.getOrgaId());
organizationDao.refreshCmsOrganizationResourceInfo(organization.getOrgaId());
organizationDao.sleep();
} catch (Exception e) {
logger.debug("RefreshCmsOrganizationStruts organization = " + organization.getOrgaId(), e);
}
}
}
}
}
分析源码,定时任务定时调用,每次调用生成10个线程处理,而它又使用了非线程安全的List对象,导致List对象无法被GC收集,所以这里将List替换为CopyOnWriteArrayList 。
Dominator Tree:这个使用的也比较多,显示大对象的占用率。
public class CategoryCacheJob extends QuartzJobBean implements StatefulJob {
private static final Logger LOGGER = Logger.getLogger(CategoryCacheJob.class);
public static Map<String,List<Cms_Category>> cacheMap = new java.util.HashMap<String,List<Cms_Category>>();
@Override
protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
try {
//LOGGER.info("======= 缓存编目树开始 =======");
MongoBaseDao mongoBaseDao = (MongoBaseDao) BeanLocator.getInstance().getBean("mongoBaseDao");
MongoOperations mongoOperations = mongoBaseDao.getMongoOperations();
/*
LOGGER.info("1.缓存基础教育编目树");
Query query = Query.query(Criteria.where("isDel").is("0").and("categoryType").is("F"));
query.sort().on("orderNo", Order.ASCENDING);
List list = mongoOperations.find(query, Cms_Category.class);
String key = query.toString().replaceAll("\\{|\\}|\\p{Cntrl}|\\p{Space}", "");
key += "_CategoryCacheJob";
cacheMap.put(key, list);
*/
//LOGGER.info("2.缓存职业教育编目树");
Query query2 = Query.query(Criteria.where("isDel").is("0").and("categoryType").in("JMP","JHP"));
query2.sort().on("orderNo", Order.ASCENDING);
List<Cms_Category> list2 = mongoOperations.find(query2, Cms_Category.class);
String key2 = query2.toString().replaceAll("\\{|\\}|\\p{Cntrl}|\\p{Space}", "");
key2 += "_CategoryCacheJob";
cacheMap.put(key2, list2);
//LOGGER.info("3.缓存专题教育编目树");
Query query3 = Query.query(Criteria.where("isDel").is("0").and("categoryType").is("JS"));
query3.sort().on("orderNo", Order.ASCENDING);
List<Cms_Category> list3 = mongoOperations.find(query3, Cms_Category.class);
String key3 = query3.toString().replaceAll("\\{|\\}|\\p{Cntrl}|\\p{Space}", "");
key3 += "_CategoryCacheJob";
cacheMap.put(key3, list3);
//LOGGER.info("======= 缓存编目树结束 =======");
} catch(Exception ex) {
LOGGER.error(ex.getMessage(), ex);
LOGGER.info("======= 缓存编目树出错 =======");
}
}
}
这里的HashMap也有问题:居然使用定时任务,在容器启动之后定时将数据放到Map里面做缓存?这里修改这部分代码,替换为使用memcached缓存即可。
内存泄漏的原因分析,总结出来只有一条:存在无效的引用!良好的编码规范以及合理使用设计模式有助于解决此类问题。
CopyOnWriteArrayList如何做到线程安全的
CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。
当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。
当元素在新数组添加成功后,将array这个引用指向新数组。
CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。
这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。
CopyOnWriteArrayList的add操作的源代码如下:
public boolean add(E e) {
//1、先加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、将元素加入到新数组中
newElements[len] = e;
//4、将array引用指向到新数组
setArray(newElements);
return true;
} finally {
//5、解锁
lock.unlock();
}
}
由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
可见,CopyOnWriteArrayList的读操作是可以不用加锁的。
CopyOnWriteArrayList的使用场景
通过上面的分析,CopyOnWriteArrayList 有几个缺点:
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用
因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
CopyOnWriteArrayList透露的思想
如上面的分析CopyOnWriteArrayList表达的一些思想:
1、读写分离,读和写分开
2、最终一致性
3、使用另外开辟空间的思路,来解决并发冲突
Eclipse Memory Analyzer 进行堆转储文件分析
简单的说一下使用(控制台的)如果是tomcat或者是别的服务器需要你去查如何配置JVM参数:
以下是一个会导致java.lang.OutOfMemoryError: Java heap space的程序代码:(very easy)
package org.lx.test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class OutOfMemoryTest {
public static void main(String[] args) {
Map<Integer,Date> map=new HashMap<Integer, Date>();
for (int i = 0; i < 600000000; i++) {
map.put(i, new Date());
}
}
}
然后就到了参数设置的页面,按照A,B的顺序设置参数:(-XX:+HeapDumpOnOutOfMemoryError)避免写错误可以copy
那么这时候就生成了一个文件java_pid3708.hprof,这个文件 在你的项目的根目录下(myeclipse10)
那么接下来我们就打开这个文件进行分析如何打开见下图:(选中刚刚在项目根目录下生成的文件java_pid3708.hprof打开)
打开之后你会看见下图就OK了