JAVA的内存泄漏&内存溢出
JAVA的内存泄漏
内存泄漏(Memory Leak):说白了就是程序向系统申请了内存,但是用完了却不释放。假如一台服务器内存总共有1024M,分配的512M内存一直没有被回收,那么可用内存就只有512M,就好像有一部分内存被泄漏了一样。这样说来,内存泄漏好像只是『占着坑』,其他的什么都没有做。那么试想一下,内存泄漏的增多会最终导致什么呢?
内存泄漏的分类
- 经常发生:内存泄漏的代码会被执行多次,每次执行泄漏一块内存;
- 偶然发生:在某些特定情况下才会发生;
- 一次性:发生内存泄漏的方法只会执行一次,比如,在类的构造函数中分配内存,在函数中却没有释放该内存,所以内存泄漏只会发生一次;
- 隐式泄漏:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
内存泄漏的常见原因以及解决办法
既然内存泄漏指的是申请了内存,使用完了却不释放,理解到了这一点,那么我们可以来想一下什么情况会导致内存一直不会被释放呢?我能想到的有以下几种:
- 循环过多或死循环或者递归调用,产生大量对象。
- 连接资源没有被释放。 比如数据库连接、IO连接、socket连接等等,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset和Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去连接,在finally里面释放连接。
- 静态集合类,像HashMap、List等的使用最容易出现内存泄漏,因为这些静态变量的生命周期和JVM是一致的,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被list等引用着。在下面的例子中,list是静态的,只要JVM没有停止,obj就一直不会被释放。
public class MemoryLeak {
//定义静态集合类list
static List<Object> list = new ArrayList<Object>();
public void memoryLeak(){
for (int i = 0; i<100; i++) {
Object obj = new Object();
list.add(obj);
//在这里虽然给obj=null赋值了,告诉JVM这个obj对象是不
//可用的了,但是因为obj一直在被list引用着,所以obj对
//象也不能被回收,那么对应的内存也就无法被释放,从而造成内存泄漏
obj=null;
}
}
}
在这个例子中,循环申请Object 对象,并将所申请的对象放入list中,如果仅仅释放引用本身(obj=null),那么list 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到list 后,还必须从list 中删除,最简单的方法就是将list对象设置为null。
- 单例模式。 和静态集合类导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,从而造成内存泄漏。
public class SingletonReferDemo {
public SingletonReferDemo(){
SingletonDemo.getInstance().setSingletonReferDemo(this);
}
}
public class SingletonDemo {
private SingletonReferDemo singletonReferDemo;
private static SingletonDemo singletonDemo = new SingletonDemo();
public SingletonDemo(){
}
public static SingletonDemo getInstance(){
return singletonDemo;
}
public void setSingletonReferDemo(SingletonReferDemo singletonReferDemo){
this.singletonReferDemo=singletonReferDemo;
}
public SingletonReferDemo getSingletonReferDemo() {
return singletonReferDemo;
}
public static SingletonDemo getSingletonDemo() {
return singletonDemo;
}
}
- 内部类的对象被长期持有,那么内部类对象所属的外部类对象也不会被回收。
- 修改hashset中的值,因此改变了该对象的哈希值。
public class ChangeHashCode {
private int a;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ChangeHashCode that = (ChangeHashCode) o;
if (a != that.a){
return false;
}
return true;
}
@Override
public int hashCode() {
final int b = 31;
int result = 1;
result = b * result + a;
return result;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
public class HashSetTest {
public static void main(String[] args) {
HashSet<ChangeHashCode> hashCodes = new HashSet<ChangeHashCode>();
ChangeHashCode changeHashCode = new ChangeHashCode();
changeHashCode.setA(21);//52
hashCodes.add(changeHashCode);
changeHashCode.setA(31);//62
System.out.println("remove = "+hashCodes.remove(changeHashCode));//false
hashCodes.add(changeHashCode);
System.out.println("size="+hashCodes.size());//2
}
}
可以看出:在测试方法中,当元素的hashcode发生变化之后,就再也找不到改变之前的那个元素了。这也是String被设置为不可变类型的原因。我们可以将String 放心地存储到HashSet中,或者将String当作HashMap的key值。当我们想把自己定义的类保存到散列表的时候,需要保证对象的hashCode 不可变。
- 内存中加载数据量过大。 比如一次从数据库取出过多数据加载到内存中。
如何防止发生内存泄漏
- 特别注意一些像HashMap、ArrayList的集合对象,它们经常会引发内存泄漏。当它们被声明为static时,它们的生命周期就会和JVM一样长。
- 资源未关闭造成的内存泄漏,如没有调close()方法。
- 检查代码中是否有死循环或递归调用。
- 检查是否有大循环重复产生新对象实体。
- 检查List、MAP等集合对象是否有使用完后未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
JAVA的内存溢出
内存溢出(Out Of Memory)即OOM,这个我想大家应该并不陌生。说白了,内存溢出就是指程序在向系统申请内存时,系统没有足够的内存可以分配。举个例子,给了你一块存储int类型数据的内存空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM。
内存溢出的常见原因以及解决办法
- 堆内存溢出。当出现java.lang.OutOfMemoryError:Java heap space异常时,就是堆内存溢出了。有两种常见的情况会出现这个异常:
1.设置的jvm内存太小,对象所需内存太大,创建对象时分配空间,就会抛出这个异常。以下是代码示例(编译以下代码,执行时jvm参数设置为-Xms20m -Xmx20m):
public class HeapOom {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<byte[]>();
int i=0;
while (true){
list.add(new byte[5 * 1024 * 1024]);//一次分配5M内存空间
System.out.println("count="+(++i));
}
}
}
上面的代码,如果一次请求只分配一次5m的内存的话,请求量很少并且垃圾回收正常就不会出错,但是一旦并发上来就会超出最大内存值,然后就会抛出内存溢出。
解决方法:
首先,代码没有什么问题的情况下,可以适当调整-Xms和-Xmx两个jvm参数,然后使用压力测试来调整这两个参数达到最优值。
其次,尽量避免大的对象的申请,比如文件上传,大批量从数据库中获取数据,这种情况是需要避免的,尽量分块或者分批处理,有助于系统的正常稳定的执行。
最后,尽量提高一次请求的执行速度,垃圾回收越早越好,否则大量的并发来了的时候,再来新的请求就无法分配内存了,就容易造成系统的雪崩。
2.流量/数据峰值,应用程序自身的处理存在一定的限额,比如一定数量的用户或一定数量的数据。而当用户数量或数据量突然激增并超过预期的阈值时,那么就会峰值停止前正常运行的操作将停止并触发java . lang.OutOfMemoryError:Java堆空间错误。
- 方法区内存溢出。(反射,静态变量)
- Metaspace内存溢出。元空间内存溢出系统会抛出java.lang.OutOfMemoryError: Metaspace。出现这个异常的问题的原因是系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大。
以下是用循环动态生成class的方式来模拟元空间的内存溢出的。
public class MetaSpaceOom {
public static void main(String[] args) {
ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
//循环动态生成class
while(true){
ChangeHashCode code = new ChangeHashCode();
code.setA(1);
Class clazz = code.getClass();
System.out.println(clazz.getName());
//显示共加载过的类型数目
System.out.println("total="+classLoadingMXBean.getTotalLoadedClassCount());
//显示当前还有效的类型数目
System.out.println("active="+classLoadingMXBean.getLoadedClassCount());
//显示已经被卸载的类型数目
System.out.println("unloaded="+classLoadingMXBean.getUnloadedClassCount());
}
}
}
解决办法: 默认情况下,元空间的大小仅受本地内存限制。但是为了整机的性能,尽量还是要对该项进行设置,以免造成整机的服务停机。
1)优化参数配置,避免影响其他JVM进程-XX:MetaspaceSize初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 。
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。
2)慎重引用第三方包
对第三方包,一定要慎重选择,不需要的包就去掉。这样既有助于提高编译打包的速度,也有助于提高远程部署的速度。
3)关注动态生成类的框架
对于使用大量动态生成类的框架,要做好压力测试,验证动态生成的类是否超出内存的需求会抛出异常。
- **垃圾回收超时导致内存溢出。**当应用程序耗尽所有可用内存时,GC开销限制超过了错误,而GC多次未能清除它,这时便会引发java.lang.OutOfMemoryError。当JVM花费大量的时间执行GC,而收效甚微,而一旦整个GC的过程超过限制便会触发错误(默认的jvm配置GC的时间超过98%,回收堆内存低于2%)。
public class OverheadLimitOom {
public static void main(String[] args) {
Map map = System.getProperties();
Random random = new Random();
while (true){
map.put(random.nextInt(),"测试");
}
}
}
解决方法: 要减少对象生命周期,尽量能快速的进行垃圾回收。
- 线程栈溢出。(递归)
- 内存中加载的数据量过于庞大。,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体。
- 使用的第三方软件中的BUG。
- 启动参数内存值设定的过小。
解决办法:
- 修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
- 检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
- 使用内存查看工具动态查看内存使用情况。
- 对代码进行走查和分析,找出可能发生内存溢出的位置。重点排查以下几点:
- 检查对数据库查询中是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
- 检查代码中是否有死循环或递归调用。
- 检查是否有大循环重复产生新对象实体。
- 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
如何分析内存溢出
通过参数
-XX:+HeapDumpOnOutOfMemoryError
可以让虚拟机在出现溢出时Dump出当前的内存堆转储快照以便事后进行分析。
- eclispse开发工具自带一个内存影像分析工具对dump 出来的堆转储快照进行分析,重点是分析到底是出现了内存泄漏(Memory Leak)还是内存溢出(OutOfMemory)。
如果出现的内存泄漏问题,进一步通过工具查看泄漏对象到GC Roots 的引用链。找到泄漏对象是通过怎么样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收他们的,掌握了泄漏对象的类型信息以及GCRoots 引用链的信息就可以比较准确的定位出泄漏代码的位置。
如果不存在泄漏,换句话说就是内存中的对象确实都必须存活着,那就检查虚拟机的堆参数与机器物理内存的对比,看是否可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期间的内存消耗。
防止发生内存溢出
- 检查对数据库查询中是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
- 尽早释放无用内存。
- 处理字符串尽可能使用StringBuffer,因为每创建一个String占一个独立内存。
- 少用静态变量(JDK1.8不存在方法区,不用考虑)。
- 避免在循环中创建对象。
JAVA的内存泄漏&内存溢出的联系与区别
联系
- 内存泄漏的堆积最终会导致内存溢出。
- 内存溢出就是你需要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
- 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
- 内存溢出:比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错呗。
区别
其实他们的区别很简单,内存泄漏是申请到了内存使用完了没有释放,而内存溢出是向系统申请内存时,系统没有足够的内存可以提供,其实也就是内存泄漏的堆积就会导致内存溢出。