Java 内存溢出和Java泄露的几种情况

牧云@^-^@ 提交于 2020-03-04 19:30:19

内存泄漏定义(memory leak):

一个不再被程序使用的对象或变量还在内存中占有存储空间。
一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

内存溢出 (out of memory) :

指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。

二者的关系:

内存泄漏的堆积最终会导致内存溢出
内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。
内存溢出:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错,
由于java的JVM引入了垃圾回收机制,垃圾回收器会自动回收不再使用的对象,了解JVM回收机制的都知道JVM是使用引用计数法和可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。

1、静态集合类,

如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}

在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。

2、各种连接

如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。

3、变量不合理的作用域。

一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。

 public class UsingRandom {
private String msg;
public void receiveMsg(){
readFromNet();// 从网络中接受数据保存到msg中
saveDB();// 把msg保存到数据库中
}
}

如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。
实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。

4、内部类持有外部类

如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。

import java.util.ArrayList;

class EnclosingClass
{
   private int[] data;

   public EnclosingClass(int size)
   {
      data = new int[size];
   }

   class EnclosedClass
   {
   }

   EnclosedClass getEnclosedClassObject()
   {
      return new EnclosedClass();
   }
}

public class MemoryLeak
{
   public static void main(String[] args)
   {
      ArrayList al = new ArrayList<>();
      int counter = 0;
      while (true)
      {
         al.add(new EnclosingClass(100000).getEnclosedClassObject());
         System.out.println(counter++);
      }
   }
}

怎么内部类关联 持有外部类呢,通过反编译

4.1匿名类隐含了外部类的引用

package checks;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;

public class MyTest {
	public  static List<HashMap<String,Object>> l2 = new ArrayList<>();
	private java.util.List<Object> l = new ArrayList<>();
	{
		//增加MyTest对象的内存
		for (int i = 0; i < 1000; i++) {
			l.add(""+UUID.randomUUID());
			
		}
	}
	/**
	 * 匿名对象模式
	 */
	public void test(){
		l2.add(new HashMap<String,Object>(){
			{put("11111111111", "11111111111111");
			put("11111111111", "11111111111111");
			put("11111111111", "11111111111111");
			put("11111111111", "11111111111111");
			put("11111111111", "11111111111111");}
		});
	}
	/**
	 * 普通模式
	 */
	public void test2(){
		HashMap<String, Object> d = new HashMap<String,Object>();
		d.put("11111111111", "11111111111111");
		d.put("11111111111", "11111111111111");
		d.put("11111111111", "11111111111111");
		d.put("11111111111", "11111111111111");
		d.put("11111111111", "11111111111111");
		l2.add(d);
	}
	public static void main(String[] args) throws InterruptedException {
		Thread.sleep(10000);
		System.out.println("start");
		for (int i = 0; i < 10000; i++) {
			new MyTest().test2();
			//new MyTest().test();
		}
		System.out.println("end");
		Thread.sleep(200000);
	}
	
}
$ javap -p MyTest\$1
Warning: Binary file MyTest$1 contains checks.MyTest$1
Compiled from "MyTest.java"
class checks.MyTest$1 extends java.util.HashMap<java.lang.String, java.lang.Object> {
  final checks.MyTest this$0; //外部对象的引用
  checks.MyTest$1(checks.MyTest);
}

==================================================
匿名类/内部类隐含一个字段指向外部类的实例(这里为this$0,见上)。
这里的GCRoot为l2,包含了HashMap/HashMap匿名子类实例的引用。
而HashMap匿名子类实例包含一个field指向其外部类MyTest的实例,导致MyTest实例(应为临时对象)无法被GC。

参考:https://www.zhihu.com/question/49667128/answer/117174338

5、改变哈希值

当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露

public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孙悟空","pwd2",26);
Person p3 = new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
for (Person person : set)
{
System.out.println(person);
}
}

6.监听器

在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

7、单例模式

不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏,考虑下面的例子:

class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B类采用单例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
}

参考:https://www.jb51.net/article/92311.htm

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!