2024年3月14日

JAVA内存泄漏原因

作者 高劲松

内存使用过高,一定是内存泄露吗?

内存使用过高,并不一定是内存泄露导致的结果,具体要看内存堆的分析。

一般内存泄露最直观的体现就是:

  1. 内存使用高
  2. GC回收不了内存,即GC前后堆大小几乎无变化
  3. JVM疯狂GC,CPU打满
  4. Java进程触发Linux操作系统的OOM-killer,Java进程被杀死
  5. 或者CPU被GC任务打满,服务器实际宕机。

但是这不一定是泄露导致的,也有可能是内存的错误使用导致的,不过大同小异,主要还是需要排查异常内存的使用。

Ps:之所以作者这么说,是因为作者曾经在线上遇到了架构组修改日志框架,错误的将日志内容作为了key存入了map,本应的key-value应该为traceId-日志内容,结果架构组却将key-value搞反了,导致大量的巨大key打满了内存,堆dump文件里全是几十k几十k的字符串。

如何避免内存泄露

根据上面说的内存泄露多数发生的情况,避免内存泄露的策略也就十分简单了。

  1. 尽量使用局部变量
  2. 减少使用static集合
  3. 如果必要的使用static集合,尽量使用弱引用等低级引用。比如参照ThreadLocal中的设计

内存泄露问题如何排查

内存泄露或内存持续使用较高时,通常通过堆的情况来排查。

首先可以通过jmap -histo:live pid|less 命令,查看堆内对象使用情况。此时如果内存泄露,一般都是会某个基本类型对象过多,然后可以与正常的服务作对比,看哪个对象的数量异常的多,此时如果可以判断出来,也没必要dump了。

如果通过jmap无法断定,则可以使用jmap -dump:live,format=b,file= 命令,生成dump文件。

将dump文件通过java原生的软件或者eclipse的mat工具,就可以看到哪些对象占用过多,此时你应该关注的是非基本类型对象的其他对象,因为一般来说都是基本类型的数量和大小最多。

一般来说,你会看到以下现象:

  1. 某个map的Node十分多,有几十万个。
  2. 某个框架的某个对象十分多。
  3. char数据,也就是C[],占用十分多,因为有很多大字符串。

说到垃圾回收(Garbage Collection,GC),很多人就会自然而然地把它和 Java 联系起来。在 Java 中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM 来处理。顾名思义,垃圾回收就是释放垃圾占用的空间,但垃圾回收器并不是万能的,它能够处理大部分场景下的内存清理、内存泄露以及内存优化。但它也并不是万能的,不然我们在项目实践过程中也不会出现那么多的内存泄漏的问题,很多的内存泄漏都是因为开发人员操作不当导致的。

本篇文章我们就来聊聊内存泄露的原因是什么,以及如何在应用程序中进行处理。

什么是内存泄漏
简单地说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终的结果将会使应用程序耗尽内存资源,无法正常服务,导致程序崩溃,抛出 java.lang.OutOfMemoryError 异常。

在任何一个应用程序中,发生内存泄露一般是由很多原因构成。接下来我们就聊聊最常见的一些内存泄露的场景。

比如以下的代码:

1、静态集合类引起内存泄漏
使用 HashMap、Vector 等集合时,最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏,简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

static Vector vector = new Vector(5);
for (int i = 1; i<1000; i++){
Object object = new Object();
vector .add(object);
object = null;
}

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

解决办法:静态引用时注意应用对象置空或者少用静态引用。

2、资源未关闭或释放导致内存泄露
当我们在程序中创建或者打开一个流或者是新建一个网络连接的时候,JVM 都会为这些资源类分配内存做缓存,常见的资源类有网络连接,数据库连接以及 IO 流。如果忘记关闭这些资源,会阻塞内存,从而导致 GC 无法进行清理。特别是当程序发生异常时,没有在finally 中进行资源关闭的情况。这些未正常关闭的连接,如果不进行处理,轻则影响程序性能,重则导致 OutOfMemoryError 异常发生。

所以,最后的方式就是加上 finally,比如:

try {
    //正常
} catch (Throwable t) {
    //异常
} finally {
    //关闭
}


3、不正确的 equals() 和 hashCode()
在HashMap和HashSet这种集合中,常常用到equal()和hashCode()来比较对象,如果重写不合理,将会成为潜在的内存泄露问题。

public class ThreadTest{

private String name;
private Integer id;
private Integer age;

public ThreadTest(Integer id,String name,Integer age){
    this.name = name;
    this.id = id;
    this.age = age;
}

public static void main(String[] args){
    Map<ThreadTest, String> map = new HashMap<>();
    ThreadTest t1 = new ThreadTest(1,"xiaoming", 30);
    ThreadTest t2 = new ThreadTest(1,"xiaoming", 30);
    ThreadTest t3 = new ThreadTest(1,"xiaoming", 30);

      map.put(t1 , "xiaoming");
      map.put(t2, "xiaoming");
      map.put(t3 , "xiaoming");

      System.out.println("运行结果:"+map.entrySet().size());
}

}
ThreadTest 类没有重写 equals 和 hashCode 方法,那 Map 的 put 方法就会调用 Object 默认的 hashCode 方法。

但由于上述代码的 ThreadTest 类并没有重写 equals 和 hashCode 方法,因此在执行 put 操作时,Map会认为每次创建的对象都是新的对象,从而导致内存不断的增长,会导致内存泄漏的可能。

解决的方法就是重写 equals 和 hashCode ,代码如下:

@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof ThreadTest)) {
return false;
}
ThreadTest person = (ThreadTest) o;
return person.name.equals(name);
}

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + name.hashCode();
    return result;
}

重写了 hashCode 确实可以避免重复对象的加入,但是也会引入新的问题,如下代码:

public static void main(String[] args){
Set set = new HashSet();
ThreadTest t1 = new ThreadTest(1,”xiaoming”, 30);
ThreadTest t2 = new ThreadTest(1,”xiaoming”, 30);
ThreadTest t3 = new ThreadTest(1,”xiaoming”, 30);

    set.add(t1);
    set.add(t2);
    set.add(t3);

      System.out.println("删除前:"+set.size()); 
      t1.setName("zhangsan");
      set.remove(t1);
      System.out.println("删除后:"+set.size());
      set.add(t1);
      System.out.println("新增后:"+set.size());
}

从运行结果我们可以看到,很明显 set.remove(t1) 以后没有删除成功,这是因为 t1.setName(“zhangsan”) 后,会重新计算 t1 的 hashCode,并且发生了变化,所以 remove 的时候会找不到相应的 Node,这会导致业务中无用的对象被引用着,会导致内存泄漏的可能。

解决的方法就是先 remove,然后修改属性,最后再重新 add 数据进去。

4、重写了 finalize() 的类
使用 finalize() 方法会存在潜在的内存泄露问题,每当类的 finalize() 方法被重写时,该类的对象不会立即被垃圾回收。相反,GC 将它们排队等待最后确定,在以后的某个时间点进行回收。

如果 finalize() 方法重写的不合理或 finalizer 队列无法跟上 Java 垃圾回收器的速度,那么迟早,应用程序会出现 OutOfMemoryError 异常。

比如运行如下的代码:

public class ThreadTest{

 @Override
    protected void finalize() throws Throwable {
    while (true) {
           Thread.yield();
      }
 }

public static void main(String[] args){
    while (true) {
        for (int i = 0; i < 100000; i++) {
            ThreadTest force = new ThreadTest();
        }
   }
}

}


为了证明这一点,我们为类重写了 finalize() 方法,并且该方法需要一点时间来执行。当此类的大量对象被垃圾回收时,在 VisualVM中的结果会是指数增长。

所以我们应该避免使用 finalizer() 方法。

5、使用 ThreadLocal 造成内存泄露
ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定,从而实现线程的安全,但是使用不当,就会引起内存泄露。

一旦线程不在存在,ThreadLocal 就应该被垃圾收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程就不会被垃圾回收器回收。所以使用到 ThreadLocal 来保留线程池中线程的变量副本时,ThreadLocal 没有显示的删除时,就会一直保留在内存中,不会被垃圾回收。

解决办法是不在使用 ThreadLocal 时,调用 remove() 方法,该方法删除了此变量的当前线程值。不要使用 ThreadLocal.set(null),它只是查找与当前线程关联的 Map 并将键值对设置为当前线程为 null。

try {
threadLocal.set(System.nanoTime());
}
finally {
threadLocal.remove();
}

使用 try/finally 的方式,假如在运行过程中出现异常,还可以在 finally 中 remove 掉。

总结
本文主要介绍了 5 种内存泄露的场景,针对每种内存泄露的场景都提供了解决办法,但是对于内存泄露来说,不同的代码,不同的场景会出现一些不同的内存泄漏问题,我们需要了解内存泄漏的根本原因,同时掌握一些基本分析方法,以便我们能及时解决问题。

1.如何解决内存错误?

1.1。使用静态变量时:

尽量减少系统中静态字段的使用。在这种情况下,您可以使用 «lazy» 而不是紧急加载对象。

1.2. 如果有内部类,参考外部:

如果不需要外部类元素,您可以将内部类转换为静态类。

1.3. 如果应用程序的资源没有关闭:

«finally» 应及时激活以完成资源的使用。

1.4. 使用 finalize() 方法时:

应将与决赛选手的任何工作减少到零。

1.5。使用内部字符串时

尝试将 Java 应用程序升级到最新版本。这可以通过在第 6 个版本之后将字符串池移动到堆的空闲位置来实现。并且为了避免错误« Out Of Memory Error »,在使用批量字符串时,您可以扩展«PermGen»大小。

1.6. 使用 ThreadLocals 时

在不需要时稳定地清理 ThreadLocal 变量。具有 remove() 属性的 ThreadLocal – 删除所有当前线程的变量值。它必须在 «finally» 块中关闭以确保它被停用。

1.7. 当实现不正确的 equals() 和 hashCode()

创建新元素时,最好覆盖 equals() 和 hashCode() 路径。

用于分析 Java 内存的最常用工具:

  • EJ-Technologies 的JProfiler;
  • 甲骨文公司的Java VisualVM;
  • YourKit GmbH 的 YourKit。