Java内存溢出和内存泄漏,是个什么东西?

本文深入探讨了Java中的内存溢出(OutofMemoryError)和内存泄漏,包括它们的原因、示例代码、解决策略以及常用诊断工具。强调了内存管理和优化的重要性,如合理设置JVM参数、避免大对象分配、控制递归深度等。同时提到了栈内存溢出、垃圾回收超时、本地线程创建失败等场景及其解决办法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

大家好,我是猿人(猿码天地创始人),今天给码农们或即将成为码农或想成为码农的朋友讲讲Java内存溢出和内存泄漏,是个什么东西,现在是深夜23:00分,猿人最擅长熬夜,就是不怕掉头发!一切都是为了亲爱的粉丝朋友能学到知识,猿人熬夜也是值得的!

我是猿人,一个热爱技术、热爱编程的IT猿。技术是开源的,知识是共享的!

写作是对自己学习的总结和记录,如果您对 Java、分布式、微服务、中间件、Spring Boot、Spring Cloud等技术感兴趣,可以关注我的动态,我们一起学习,一起成长!

用知识改变命运,让家人过上更好的生活,互联网人一家亲!

---公众号猿码天地

Java知识学堂https://gitee.com/zhangbw666/it-knowledge

你多学一样本事,就少说一句求人的话,现在的努力,是为了以后的不求别人,实力是最强的底气。记住,活着不是靠泪水博得同情,而是靠汗水赢得掌声。——《写给程序员朋友》 

好,废话不多说,直接干,进入正题:Good Good Study,Day Day Up!

1. 内存溢出(out of memory)

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。比如对象从堆的新生代进入老年代,老年代的内存空间不够,在发生了fullgc后空间还是不足以存放新生代存活的对象,则会发生OOM。是不是很多同学对JVM内存模型以及垃圾回收机制还不懂,哈哈,别着急,后面猿人会写几篇关于JVM的文章,供大家学习。

2. 内存泄漏(memory leak)

内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

memory leak会最终会导致out of memory!

再给大家讲一种情况,一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM。这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。

3. 为什么会出现?

1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
3.代码中存在死循环或循环产生过多重复的对象实体;
4.使用的第三方软件中的BUG;
5.启动参数内存值设定的过小

4. 解决方案

  • 第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加)

  • 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

  • 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

重点排查以下几点:

1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。

2.检查代码中是否有死循环或递归调用。

3.检查是否有大循环重复产生新对象实体。

4.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

第四步,使用内存查看工具动态查看内存使用情况。

5. 推荐工具

  • jps

显示当前所有java进程

  • jmap

查看内存信息,实例个数以及占用内存大小

  • jstack

用jstack加进程id查找死锁

找出占用cpu最高的线程堆栈信息

  • jvisualvm

监控内存泄露,跟踪垃圾回收,执行时内存、cpu分析,线程分析...

  • jinfo

查看正在运行的Java应用程序的扩展参数

查看jvm的参数

  • jstat

查看堆内存各部分的使用量,以及加载类的数量

垃圾回收统计

JVM运行情况预估

  • Arthas

Arthas 是 Alibaba 在 2018 年 9 月开源的 Java 诊断工具。支持 JDK6+, 采用命令行交互模式,可以方便的定位和诊断线上程序运行问题。Arthas 官方文档十分详细,详见:https://alibaba.github.io/arthas

6. 内存溢出泄漏的5个场景

JVM运行时首先需要类加载器(classLoader)加载所需类的字节码文件。加载完毕交由执行引擎执行,在执行过程中需要一段空间来存储数据。这段内存空间的分配和释放过程正是我们需要关心的运行时数据区。内存溢出的情况就是从类加载器加载的时候开始出现的,内存溢出分为两大类:OutOfMemoryError和StackOverflowError。以下举出10个内存溢出的情况,并通过实例代码的方式讲解了是如何出现内存溢出的。

6.1 堆内存溢出

当出现java.lang.OutOfMemoryError:Java heap space异常时,就是堆内存溢出了。

原因

设置的jvm内存太小,对象所需内存太大,创建对象时分配空间,就会抛出这个异常。

示例代码

编译以下代码,执行时jvm参数设置为-Xms20m -Xmx20m

}//堆溢出
public class HeapOomError {
 public static void main(String[] args){
  List<byte[]> list = new ArrayList<>();
  int i=0;
  while(true){
   list.add(new  byte[5 * 1024 *1024]);
   System.out.println("count is:"+(++i));
  }
 }
}

执行后,会报堆内存溢出:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.bowen.demo.oom.HeapOomError.main(HeapOomError.java:17)
Disconnected from the target VM, address: '127.0.0.1:57051', transport: 'socket'

解决方式`

首先,如果代码没有什么问题的情况下,可以适当调整-Xms和-Xmx两个jvm参数,使用压力测试来调整这两个参数达到最优值。

其次,尽量避免大的对象的申请,像文件上传,大批量从数据库中获取,这是需要避免的,尽量分块或者分批处理,有助于系统的正常稳定的执行。

最后,尽量提高一次请求的执行速度,垃圾回收越早越好,否则,大量的并发来了的时候,再来新的请求就无法分配内存了,就容易造成系统的雪崩。

以上这个示例,如果一次请求只分配一次5m的内存的话,请求量很少垃圾回收正常就不会出错,但是一旦并发上来就会超出最大内存值,就会抛出内存溢出。

6.2 堆内存泄漏

问题描述

Java中的内存泄漏是一些对象不再被应用程序使用但垃圾收集无法识别的情况。因此,这些未使用的对象仍然在Java堆空间中无限期地存在。不停的堆积最终会触发java . lang.OutOfMemoryError。

示例代码

//内存泄漏导致的内存溢出
public class MemoryLeakOomError{

 static class Key {
  Integer id;

  Key(Integer id) {
   this.id = id;
  }

  @Override
  public int hashCode() {
   return id.hashCode();
  }

  @Override
  public boolean equals(Object o) {
   boolean response = false;
   if (o instanceof Key) {
    response = (((Key) o).id).equals(this.id);
   }
   return response;
  }
 }

 public static void main(String[] args) {
  Map m = new HashMap<>();
  while (true){
   for (int i=0;i<10000;i++){
    if(m.containsKey(new Key(i))){
     m.put(new Key(i),"Num:"+i);
    }
   }
  }
 }
}

执行上面代码时,可能会期望它永远运行,不会出现任何问题,假设单纯的缓存解决方案只将底层映射扩展到10,000个元素,而不是所有键都已经在HashMap中。然而事实上元素将继续被添加,因为key类并没有重写它的equals()方法。

随着时间的推移,随着不断使用的泄漏代码,“缓存”的结果最终会消耗大量Java堆空间。当泄漏内存填充堆区域中的所有可用内存时,垃圾收集无法清理它,java . lang.OutOfMemoryError。

解决办法

相对来说对应的解决方案比较简单:重写equals方法即可。

6.3 垃圾回收超时内存溢出

问题描述

当应用程序耗尽所有可用内存时,GC开销限制超过了错误,而GC多次未能清除它,这时便会引发java.lang.OutOfMemoryError。当JVM花费大量的时间执行GC,而收效甚微,而一旦整个GC的过程超过限制便会触发错误(默认的jvm配置GC的时间超过98%,回收堆内存低于2%)。

// 垃圾回收超时内存溢出
public class OverheadLimitOomError {

 public static void main(String[] args) {
  Map map = System.getProperties();
  Random random = new Random();
  while (true){
   map.put(random.nextInt(),"猿码天地");
  }
 }
}

解决方法

要减少对象生命周期,尽量能快速的进行垃圾回收。

6.4 栈内存溢出

问题描述

当一个线程执行一个Java方法时,JVM将创建一个新的栈帧并且把它push到栈顶。此时新的栈帧就变成了当前栈帧,方法执行时,使用栈帧来存储参数、局部变量、中间指令以及其他数据。

当一个方法递归调用自己时,新的方法所产生的数据(也可以理解为新的栈帧)将会被push到栈顶,方法每次调用自己时,会拷贝一份当前方法的数据并push到栈中。因此,递归的每层调用都需要创建一个新的栈帧。这样的结果是,栈中越来越多的内存将随着递归调用而被消耗,如果递归调用自己一百万次,那么将会产生一百万个栈帧。这样就会造成栈的内存溢出。

// 栈内存溢出
public class StackOomError {

 int num = 1;
 public void testStack(){
  num ++;
  this.testStack();
 }

 public static void main(String[] args) {
  StackOomError stackOomError = new StackOomError();
  stackOomError.testStack();
 }
}

解决办法

如果程序中确实有递归调用,出现栈溢出时,可以调高-Xss大小,就可以解决栈内存溢出的问题了。递归调用防止形成死循环,否则就会出现栈内存溢出。

6.5 创建本地线程内存溢出

问题描述

线程基本只占用heap以外的内存区域,也就是这个错误说明除了heap以外的区域,无法为线程分配一块内存区域了,这个要么是内存本身就不够,要么heap的空间设置得太大了,导致了剩余的内存已经不多了,而由于线程本身要占用内存,所以就不够用了。

public class UnableCreateNativeThreadError {
 
 public static void main(String[] args) {
  while (true){
   Executor pool = Executors.newCachedThreadPool();
   pool.execute(()-> System.out.println("猿码天地"));
  }
  
 }
}

解决方法

首先检查操作系统是否有线程数的限制,使用shell也无法创建线程,如果是这个问题就需要调整系统的最大可支持的文件数。

日常开发中尽量保证线程最大数的可控制的,不要随意使用线程池。不能无限制的增长下去。

7. 总结

通过以上的6种出现内存溢出情况,大家在实际碰到问题时也就会知道怎么解决了,在实际编码中也要记得:

1.第三方jar包要慎重引入,坚决去掉没有用的jar包,提高编译的速度和系统的占用内存。

2.对于大的对象或者大量的内存申请,要进行优化,大的对象要分片处理,提高处理性能,减少对象生命周期。

3.尽量固定线程的数量,保证线程占用内存可控,同时需要大量线程时,要优化好操作系统的最大可打开的连接数。

4.对于递归调用,也要控制好递归的层级,不要太高,超过栈的深度。

5.分配给栈的内存并不是越大越好,因为栈内存越大,线程多,留给堆的空间就不多了,容易抛出OOM。JVM的默认参数一般情况没有问题(包括递归)。

优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。

你多学一样本事,就少说一句求人的话,现在的努力,是为了以后的不求别人,实力是最强的底气。记住,活着不是靠泪水博得同情,而是靠汗水赢得掌声。——《写给程序员朋友》 

点赞&在看是最大的支持

猿人于2021年3月24日 0点30分 于深圳整理,整理完这篇文章头发还有10万零547根,今天掉了1根头发,持续记录头发根数,加油!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿码天地

相互学习,谢谢您的打赏。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值