内存泄露、溢出、逃逸

内存泄露和溢出及常见的排查和原因

内存泄漏

内存泄漏:不再使用的对象,仍占用内存(存在GCRoots引用),无法回收,导致程序运行过程中,可使用的内存越来越少,或者固定占用了部分无法释放的内存。

情况:

  1. 静态集合持有非静态变量
    • 静态的对象、字段生命周期是与程序是一致的;无法释放;
    • 如果元素不再使用,需要手动删除;
  2. 长生命周期的对象,持有短生命周期对象的引用
    • 即使短生命周期的对象不用了,依然无法释放,因为有引用;
    • 此类情况需要考虑业务进行避免;
  3. 存在无法释放的数据库连接、IO连接等
    • 连接使用过程中,垃圾回收不会回收连接使用的对象;
    • 用完资源一定要释放,且要考虑异常的可能,所以一般在
      finally
      中释放链接,或者使用
      try-with-resources
      语法
  4. 内部类持有外部类对象
    • 即使外部类对象不再使用,也不会被回收;
  5. ThreadLocal
    • 使用线程池的情况下,线程使用完ThreadLocal本地变量,只要线程不被回收,本地变了不会自动清空;
    • 用完即清理,且不要存放大对象;

内存溢出

内存溢出:程序申请的内存超过了系统能够提供的最大值,或者超过了程序栈所能申请的最大内存,则会发生OOM;

三种常见内存溢出:

  1. 元空间内存不足->OOM
    • OutOfMemoryError: PermGen space:元空间主要存储着大量的class信息,静态数据;
  2. 堆内存不足->OOM
    • OutOfMemoryError: Java heap space
  3. 栈内存溢出
    • OutOfMemory:栈动态内存扩展,内存不足时抛出; ==JVM没有动态扩展的栈,所以Java进程的栈不会OOM==
    • StackOverflowError:栈创建栈帧,没有足够的内存,抛SOF; 一般是递归调用过程中抛出; 可以减少栈内存大小,提高栈帧深度;

内存逃逸

程序中最高效的内存结构是:栈 内存逃逸:当编译器无法保证一个变量的声明周期只在当前的函数内时,就会将其分配到中,以保证函数返回,栈帧回收后,此变量仍然有效;

  • 闭包是一个典型的内存逃逸情况; 内存逃逸影响:栈内存是没有回收的,直接释放,速度很快,当发生内存逃逸,分配在堆上,就需要依靠GC来回收,会增加GC的压力;

内存逃逸场景

1、超出栈的大小,只能分配到堆上; 2、闭包; 3、golang中入参为不确定类型,如:interface;函数无法确定其大小,全都会逃逸,分配在堆上; 4、函数内创建的对象,被堆上对象引用;

逃逸分析

1、JVM的JIT动态编译代码时,会进行逃逸分析,如果对象没有逃逸,则不会分配到堆上;

JDK1.7之后,逃逸分析是默认开启的;