写点什么

JVM——内存泄漏与内存溢出

作者:琦彦
  • 2022 年 10 月 08 日
    河南
  • 本文字数:7216 字

    阅读完需:约 24 分钟

JVM——内存泄漏与内存溢出

内存泄漏与内存溢出

1. 面试题

什么是内存泄漏和什么是内存溢出 (陌陌)

Java 存在内存泄漏吗,内存泄漏的场景有哪些,如何避免(百度)

Java 中会存在内存泄漏吗,简述一下?(猎聘)

内存泄漏是怎么造成的?(拼多多、字节跳动)

内存泄漏与内存溢出的区别 (字节跳动)

Java 存在内存溢出的现象吗 (字节跳动)

Java 中会存在内存泄漏吗,请简单描述。 (美团)

2. 内存溢出

内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。


由于 GC 一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现 OOM 的情况。


大多数情况下,GC 会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的 Full GC 操作,这时候会回收大量的内存,供应用程序继续使用。


javadoc 中对 OutOfMemoryError 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。

2.1. 内存不够的原因?

首先说没有空闲内存的情况:说明 Java 虚拟机的堆内存不够。原因有二:


(1)Java 虚拟机的堆内存设置不够。


比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小。我们可以通过参数-Xms、-Xmx 来调整。


(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)


对于老版本的 Oracle JDK,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 intern 字符串缓存占用太多空间,也会导致 OOM 问题。对应的异常信息, 会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”。


随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改观,出现 OOM,异常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。直接内存不足,也会导致 OOM。

2.2. OOM 前必有 GC?

这里面隐含着一层意思是,在抛出 OutOfMemoryError 之前,通常垃圾收集器会被触发,尽其所能去清理出空间。


例如:在引用机制分析中,涉及到 JVM 会去尝试回收软引用指向的对象等。


在 java.nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。


当然,也不是在任何情况下垃圾收集器都会被触发的


比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM 可以判断出垃圾收集并不能解决这个问题,所以直接抛出 OutOfMemoryError。

3.内存泄漏

内存泄漏(Memory Leak)


也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。


但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM,也可以叫做宽泛意义上的“内存泄漏”。


尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 OutOfMemory 异常,导致程序崩溃。


注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。



3.1. 内存泄漏的理解与分类

何为内存泄漏(memory leak)

可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏)。


是否还被使用? 是

是否还被需要? 否

内存泄漏(memory leak)的理解

严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。


但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM,也可以叫做宽泛意义上的“内存泄漏”。


对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长;


那么当 Y 生命周期结束的时候,X 依然引用着 Y,这时候,垃圾回收期是不会回收对象 Y 的;


如果对象 X 还引用着生命周期比较短的 A、B、C,对象 A 又引用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。

内存泄漏与内存溢出的关系:
1. 内存泄漏(memory leak )

申请了内存用完了不释放,比如一共有 1024M 的内存,分配了 512M 的内存一直不回收,那么可以用的内存只有 512M 了,仿佛泄露掉了一部分;


通俗一点讲的话,内存泄漏就是【占着茅坑不拉 shi】。

2. 内存溢出(out of memory)

申请内存时,没有足够的内存可以使用;


通俗一点儿讲,一个厕所就三个坑,有两个站着茅坑不走的(内存泄漏),剩下最后一个坑,厕所表示接待压力很大,这时候一下子来了两个人,坑位(内存)就不够了,内存泄漏变成内存溢出了。


可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。

泄漏的分类

经常发生:发生内存泄露的代码会被多次执行,每次执行,泄露一块内存;


偶然发生:在某些特定情况下才会发生;


一次性:发生内存泄露的方法只会执行一次;


隐式泄漏:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。

3.2. Java 中内存泄漏的 8 种情况

3.2.1. 1- 静态集合类

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


public class MemoryLeak {static List list = new ArrayList();
public void oomTests() { Object obj = new Object();//局部变量list.add(obj); }}
复制代码
3.2.2. 2- 单例模式

单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

3.2.3. 3- 内部类持有外部类

内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。


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


public class HandlerDemoActivity extends Activity implements OnClickListener {  private static final int MESSAGE_INCRESE = 0;  private static final int MESSAGE_DECRESE = 1;  private TextView tv_demo_number;  private Button btn_demo_increase;  private Button btn_demo_decrease;  private Button btn_demo_pause;  private Handler handler = new Handler(){     //回调方法     public void handleMessage(android.os.Message msg) {       String strNum = tv_demo_number.getText().toString();       //转换为整型数据,获取当前显示的数值       int num = Integer.parseInt(strNum);       switch(msg.what){       case MESSAGE_INCRESE:          num++;          tv_demo_number.setText(num + "");          if(num == 20){            Toast.makeText(HandlerDemoActivity.this, "已达到最大值", 0).show();            btn_demo_pause.setEnabled(false);            return;          }          //发送延迟的+1的消息          sendEmptyMessageDelayed(MESSAGE_INCRESE, 300);//指的是延迟处理,而不是延迟发送          break;       case MESSAGE_DECRESE:          num--;          tv_demo_number.setText(num + "");          if(num == 0){            Toast.makeText(HandlerDemoActivity.this, "已达到最小值", 0).show();            btn_demo_pause.setEnabled(false);            return;          }          //发送延迟的-1的消息          sendEmptyMessageDelayed(MESSAGE_DECRESE, 300);//指的是延迟处理,而不是延迟发送          break;       }     }  };  @Override  protected void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     setContentView(R.layout.activity_handler_demo);     init();  }  private void init() {     tv_demo_number = (TextView) findViewById(R.id.tv_demo_number);     btn_demo_increase = (Button) findViewById(R.id.btn_demo_increase);     btn_demo_decrease = (Button) findViewById(R.id.btn_demo_decrease);     btn_demo_pause = (Button) findViewById(R.id.btn_demo_pause);     btn_demo_increase.setOnClickListener(this);     btn_demo_decrease.setOnClickListener(this);     btn_demo_pause.setOnClickListener(this);  }  @Override  public void onClick(View v) {     ....  }}
复制代码
3.2.4. 4- 各种连接,如数据库连接、网络连接和 IO 连接等

各种连接,如数据库连接、网络连接和 IO 连接等。


在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。


否则,如果在访问数据库的过程中,对 Connection、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。


public static void main(String[] args) {try {        Connection conn = null;        Class.forName("com.mysql.jdbc.Driver");        conn = DriverManager.getConnection("url", "", "");        Statement stmt = conn.createStatement();        ResultSet rs = stmt.executeQuery("....");    } catch (Exception e) { //异常日志
} finally { //1.关闭结果集 Statement // 2.关闭声明的对象 ResultSet // 3.关闭连接 Connection }}
复制代码
3.2.5. 5- 变量不合理的作用域

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


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


如上面这个伪代码,通过 readFromNet 方法把接受的消息保存在变量 msg 中,然后调用 saveDB 方法把 msg 的内容保存到数据库中,此时 msg 已经就没用了,由于 msg 的生命周期与对象的生命周期相同,此时 msg 还不能回收,因此造成了内存泄漏。


实际上这个 msg 变量可以放在 receiveMsg 方法内部,当方法使用完,那么 msg 的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。

3.2.6. 6- 改变哈希值

改变哈希值,当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。


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


这也是 String 为什么被设置成了不可变类型,我们可以放心地把 String 存入 HashSet,或者把 String 当做 HashMap 的 key 值;


当我们想把自己定义的类保存到散列表的时候,需要保证对象的 hashCode 不可变。


举例1:/** * 演示内存泄漏* * @author FLY * @create 14:43 */public class ChangeHashCode {public static void main(String[] args) {        HashSet set = new HashSet();        Person p1 = new Person(1001, "AA");        Person p2 = new Person(1002, "BB");
set.add(p1); set.add(p2); p1.name = "CC"; set.remove(p1); System.out.println(set);//2个对象!
// set.add(new Person(1001, "CC"));// System.out.println(set);// set.add(new Person(1001, "AA"));// System.out.println(set);
}}
class Person {int id; String name;
public Person(int id, String name) {this.id = id;this.name = name; }
@Overridepublic boolean equals(Object o) {if (this == o) return true;if (!(o instanceof Person)) return false;
Person person = (Person) o;
if (id != person.id) return false;return name != null ? name.equals(person.name) : person.name == null; }
@Overridepublic int hashCode() {int result = id; result = 31 * result + (name != null ? name.hashCode() : 0);return result; }
@Overridepublic String toString() {return "Person{" +"id=" + id +", name='" + name + '\'' +'}'; }}举例2:/** * 演示内存泄漏 * @author FLY * @create 14:47 */public class ChangeHashCode1 {public static void main(String[] args) { HashSet hs = new HashSet(); Point cc = new Point(); cc.setX(10);//hashCode = 41hs.add(cc); cc.setX(20);//hashCode = 51System.out.println("hs.remove = " + hs.remove(cc));//falsehs.add(cc); System.out.println("hs.size = " + hs.size());//size = 2}
}
class Point {int x;
public int getX() {return x; }
public void setX(int x) {this.x = x; }
@Overridepublic int hashCode() {final int prime = 31;int result = 1; result = prime * result + x;return result; }
@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null) return false;if (getClass() != obj.getClass()) return false; Point other = (Point) obj;if (x != other.x) return false;return true; }}
复制代码
3.2.7. 7- 缓存泄漏

内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。


对于这个问题,可以使用 WeakHashMap 代表缓存,此种 Map 的特点是,当除了自身有对 key 的引用外,此 key 没有其他引用那么此 map 会自动丢弃此值。


/** * 演示内存泄漏* * @author FLY * @create 14:53 */public class MapTest {static Map wMap = new WeakHashMap();static Map map = new HashMap();
public static void main(String[] args) {init();testWeakHashMap();testHashMap(); }
public static void init() { String ref1 = new String("obejct1"); String ref2 = new String("obejct2"); String ref3 = new String("obejct3"); String ref4 = new String("obejct4");wMap.put(ref1, "cacheObject1");wMap.put(ref2, "cacheObject2");map.put(ref3, "cacheObject3");map.put(ref4, "cacheObject4"); System.out.println("String引用ref1,ref2,ref3,ref4 消失");
}
public static void testWeakHashMap() {
System.out.println("WeakHashMap GC之前");for (Object o : wMap.entrySet()) { System.out.println(o); }try { System.gc(); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("WeakHashMap GC之后");for (Object o : wMap.entrySet()) { System.out.println(o); } }
public static void testHashMap() { System.out.println("HashMap GC之前");for (Object o : map.entrySet()) { System.out.println(o); }try { System.gc(); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("HashMap GC之后");for (Object o : map.entrySet()) { System.out.println(o); } }
}/** * 结果* String引用ref1,ref2,ref3,ref4 消失* WeakHashMap GC之前* obejct2=cacheObject2 * obejct1=cacheObject1 * WeakHashMap GC之后* HashMap GC之前* obejct4=cacheObject4 * obejct3=cacheObject3 * Disconnected from the target VM, address: '127.0.0.1:51628', transport: 'socket' * HashMap GC之后* obejct4=cacheObject4 * obejct3=cacheObject3 **/
复制代码


上面代码和图示主演演示 WeakHashMap 如何自动释放缓存对象,当 init 函数执行完成后,局部变量字符串引用 weakd1,weakd2,d1,d2 都会消失,此时只有静态 map 中保存中对字符串对象的引用,可以看到,调用 gc 之后,HashMap 的没有被回收,而 WeakHashMap 里面的缓存被回收了。

3.2.8. 8- 监听器和回调

内存泄漏另一个常见来源是监听器和其他回调,如果客户端在你实现的 API 中注册回调,却没有显式的取消,那么就会积聚。


需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为 WeakHashMap 中的键。


发布于: 刚刚阅读数: 3
用户头像

琦彦

关注

孤独的技术没有价值 2019.08.24 加入

还未添加个人简介

评论

发布
暂无评论
JVM——内存泄漏与内存溢出_JVM_琦彦_InfoQ写作社区