signed

QiShunwang

“诚信为本、客户至上”

内存溢出与内存泄露

2021/3/21 9:54:34   来源:

文章目录

      • 1 内存泄露与内存溢出的区别
        • 1.1 内存泄露
        • 1.2 内存溢出
      • 2 出现的场景
        • 2.1 内存泄露出现的场景
          • 2.2.1 更改对象哈希值运算相关的参数
          • 2.2.2 ThreadLocal使用不当导致内存泄露
        • 2.2 内存溢出出现的场景
          • 2.2.1 堆内存溢出
          • 2.2.2 方法区内存溢出
          • 2.2.3 线程栈溢出

1 内存泄露与内存溢出的区别

1.1 内存泄露

内存泄露(Memory Leak),指的是堆内存中被分配的对象无用处了,仍然GC ROOT可达,无法回收

简单来说,就是程序执行时时,临时对象已无用处了,按理对象需要被取消引用,但是对象仍然存在强引用,导致此部分内存属于无用内存,程序的内存中存在一部分无法被使用的内存

可以这么理解,泄露的意思就是丢了、没有了,即程序原有的内存缺少了一部分

1.2 内存溢出

内存溢出(out of memory),指的是程序执行过程中,无法申请到足够的内存,导致的错误。

即程序当前可用的空闲内存不够了,无法满足程序执行所需要的内存大小。如程序中需要缓存1000个对象到List中,需要占用10m内存,而当前可用内存只有5m,就会内存溢出。

溢出的意思,就是当前内存容量,无法满足程序执行需要的内存大小,超过程序的内存上限了

内存泄露,会损失掉程序的一部分内存,是内存溢出的诱因之一。

2 出现的场景

2.1 内存泄露出现的场景

2.2.1 更改对象哈希值运算相关的参数

将对象存在HashSet中时,一般会手动重写对象的equals方法和hashCode方法。

HashSet判断对象是否已存在时,会先。如果hashCode的计算结果,如果存在,再判断equals方法的执行结果。

如果hashCode的计算结果依赖了对象中的某个属性,人为地更改了HashSet中的此属性(比如判断用户是否存在时,通过userNunber计算了HashCode,此时更改了HashSet中的某个已有对象的userName),会导致此对象的前后Hash值不一样。如果通过contains来判断对象是否存在,只通过原来的对象contains来获取HashSet中的对象时,会导致被变更的对象,有可能永远无法被使用到(说明:如果通过对HashSet遍历的方式操作数据,不会造成内存泄露情况)。

个人觉得,此部分泄露情况,一般情况下可以忽略,如果更改了hashCode计算方式相关的属性,此对象在定义上就属于不同的对象了。通过对应的不同的对象来判断contains的时候,就可以得到数据了。
但是,如果在判断对象是否存在时,在前面缓存了已经存在的对象信息后,更改了影响hash计算的属性,再根据前面缓存的对象对hashSet中的对象进行操作,则会出现内存泄露情况。暂时能想到的此种情况只此一例,且不常见。

2.2.2 ThreadLocal使用不当导致内存泄露
	TheadLocal指的是java中的线程变量,主要用于多线程操作一个变量时,每个线程存储一份变量的副本,各操作各的,最常见的是`spring的声明式事务`和mybatis的分页插件`PageHelper`。

	以PageHelper为例,当手动调用分页插件代码,后面的mapper查询时,会自动分页。内部原理为,手动调用代码时会写入一份待消费的状态到当前线程中(通过ThreadLocal实现),如果后面执行mybatis的查询操作,则会自动实现分页,消费掉写入到TheadLocal中的待分页状态。
	**但是**,如果在生成待消费数据和消费之前,执行了别的代码且代码执行出错了。导致当前操作失败,线程被回收到线程池中(如tomcat的线程池),然后此线程中仍然有未被消费的ThreadLocal的内容。
	**此时会有两个很坏的情况:**

	(1)回归线程池线程的TheadLocalMap中存在未被消费掉的分页属性,存在内存泄露

	(2)如线程未被线程池销毁,当线程被再次使用时。正好遇到一个mybatis的非分页查询,线程中的分页插件信息将被消费掉,导致了`不想分页查询,但是分页查询的情况`,此种情况排查起来,非常难。因此建议在使用mybatis分页插件的时候,需要在调用分页插件后,紧接着执行查询代码,并将执行代码进行异常捕获,在finally块中,手动调用下分页插件的clear待消费数据的方法。

以上内存泄露情况,属于研发人员代码不规范导致的泄露情况,使用ThreadLocal还有一种特殊情况,会在正常操作的情况下,一定概率地情况下发生内存泄露。

以下情况,是线程会被线程池回收的情况下

在线程中有个theadLocals的类似Map的结构存储相关信息,key存储的是ThreadLocal变量本身,value存储的是ThreadLocal对应的在本线程中存的值。

其中,keye是弱引用(执行垃圾回收的时候会被回收),key只有在线程中还存在一份强引用的情况下,key才不会在垃圾回收的时候回收掉。如果key在线程中的强引用没有了,而且没有手动执行remove操作,key会在执行gc的时候被回收掉,但是,只有key被回收掉了,key对应的value还在。而此种情况,只有在对应的线程再次执行其他ThreadLocal的set/get/remove时,才会检查此线程对应的threadLocals中有没有key为null的,如果有,则会删除value的内容。

如果对应的线程一直不执行TheadLocals中的上述三个操作,对应的value会一直存在内存中,存在内存泄露问题。

此种情况,也可以避免,那就是使用ThreadLocal时,如果此变量无用了,需调用remove方法,移除无用的value。

2.2 内存溢出出现的场景

2.2.1 堆内存溢出

java的堆中主要是用来存放数组和对象的相关的jvm属性配置为:-Xms -Xmx

堆内存溢出(outOfMemoryError:java heap space)主要指的是Java堆中没有足够的空间去容纳创建的对象所需要的内存大小。

比如当前堆的总内存为8G,可用内存为5G,程序中创建了一个100万对象List,需要10G,5G小于10G,内存空间不够,则内存溢出。

堆内存溢出的情况,主要如下:

  • 查询数据库时数据较多的表时,查询条件中能筛选绝大部分数据的条件为空,且未采用游标等查询优化方式,将数据全部读取放到内存中。因此需要很大的堆内存,然后溢出。
  • 程序中一些操作,需要占用不至于溢出,但也不算少的内存时(比如一个申请,将需要500M的内存,但是系统中一共5G内存),当有12个请求一起发送到系统时,将会占用6G左右的内存,5G<6G,内存溢出。
  • 程序死循环,无限创建变量
2.2.2 方法区内存溢出

java中的方法区主要用来存储类信息、常量、静态变量等。相关的jvm属性配置为:-XX:PermSize、-XX:MaxPermSize

方法区内存溢出(outOfMemoryError:permgem space)一般情况下不会出现,除非是加载类太多,或者java动态代理、CGLIB使用不当导致

2.2.3 线程栈溢出

线程栈溢出(java.lang.StackOverflowError)主要指的是方法调用层级太多导致的溢出。

生产上很大一部分原因是maven配置依赖时,同一个gva被多个关联依赖,且每个关联依赖的版本不一致,在程序自动选择版本的时候,有一定几率出现此问题,且诡异的是,不同的系统环境,有的环境出现此问题,有的环境不出现此问题,甚至上线很长时间后,突然出现此问题。