记一次内存泄漏引发的生产事故

简介: 记一次内存泄漏引发的生产事故

概述


本篇文章主要记录在生产环境上发生了内存泄漏导致系统OOM,造成了严重的后果,以致于本人吃土一个月,5555~本文主要对该问题做一个复盘,同时分析下什么是内存泄漏,如何识别你的系统中存在潜在可能的内存泄漏风险,并且哪些常见的场景可能会引发内存泄漏,需要引起我们特别注意。


事件回顾


事故背景


某商业银行项目上部署了我们的风控系统,其中一个节点挂了,同时报了OOM的错误,还好其他节点正常work,不然本人要吃土一整年了,第二天本人就被要求出差到现场分析解决该问题。


事故排查和修复


通过dump生成的堆信息,用jprofiler(后面专门出文章介绍使用)工具发现:

1671110875250.jpg

上面这个字符串对象特别多,几十万个,占用了大量的堆空间,根据这个字符串查询,发现是其他同事的代码中监控项使用不当导致。

代码

1671110882920.jpg

原因分析

监控项的tag值要求是可枚举的,不能生成的每条tag都是唯一的,这样应用不会覆盖,而是保留每一条数据,导致内存爆炸。

而这里的succList,是一个Set对象,虽然列表数量是确定的,但是每次请求过来顺序不一致,比如succList有10个对象,那么就会有10!种可能,最终撑爆内存。

修复方案

1671110889916.jpg

遍历succList, tag使用单条数据, 而不是把整个对象列表放进去,重新上线后监测,没有出现问题。

小结

因为监控项使用不当导致内存泄漏,最终导致OOM。其实内部复盘了下,主要开发人员对监控项不大会使用,对里面的底层原理也不甚了解,所以了解一个技术的底层实现还是非常重要的。而且,团队内部也缺少代码review,缺少生产级别数据的验证,一系列原因,最终导致出现该生产问题。


什么是内存泄漏


通俗的来说,内存泄漏就是占着茅坑不拉屎,你认为该对象用不上了,但是实际上还被程序使用,GC又无法回收,这种情况就是内存泄漏。

如何对象判断是否还在被程序使用呢?

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

1671110901175.jpg

内存泄漏和内存溢出

内存泄漏(Memory Leak)最终的后果会导致内存溢出(Out Of Memory),但是内存溢出并不完全是内存泄漏导致,也有可能分配了一个大对象,数据库未分页查全量的数据等等。


如何发现存在内存泄漏


更关键的是我们应该如何提前发现我们的系统存在潜在的内存泄漏风险,从而规避生产出现OOM问题?

  1. 测试环境模拟测试,开启GC日志输出到文件里
  2. 通过GC Easy工具分析日志,观察下每次垃圾回收后,它的内存使用情况
  3. 如果发现回收完后,堆占用是递增的趋势,那么很有可能有内存泄漏的问题。

此外,大家还可以使用Eclipse Memory Analyzer、JProbe Profiler、JVisualVM 等工具分析程序的内存信息,凭借个人经验判断是否有内存泄漏问题(说了个寂寞,哈哈哈~~)。


内存泄漏的几种常见场景


下面通过几个案例带大家了解下常见的几种内存泄漏的场景,如果大家的程序中有类似的场景,那么就要特别注意了。


静态字段


可能导致潜在内存泄漏的第一种情况是大量使用静态变量。在Java中,静态字段的生命周期通常与应用程序一样。

简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

public class MemoryLeak {
    static List list = new ArrayList();
    public void oomTests(){
        Object obj=new Object();//局部变量
        list.add(obj);
    }
}

如何避免:

  • 最大限度地减少静态变量的使用
  • 使用单例时,依赖于延迟加载对象而不是急切加载的实现


未关闭的连接池资源


在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用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
    }
}

如何避免:

  • 始终使用finally块来关闭资源
  • 可以用sonar等工具检查代码


改变哈希值


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

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

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

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

public class ChangeHashCode1 {
    public static void main(String[] args) {
        HashSet<Point> hs = new HashSet<Point>();
        Point cc = new Point();
        cc.setX(10);//hashCode = 41
        hs.add(cc);
        cc.setX(20);//hashCode = 51  此行为导致了内存的泄漏
        System.out.println("hs.remove = " + hs.remove(cc));//false
        hs.add(cc);
        System.out.println("hs.size = " + hs.size());//size = 2
        System.out.println(hs);
    }
}
class Point {
    int x;
    public int getX() {
        return x;
    }
    public void setX(int x) {
        this.x = x;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        return result;
    }
    @Override
    public 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;
    }
    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                '}';
    }
}

如何避免:

  • 自己写的对象要重写hashCode和equals方法
  • 对象被加入到HashSet、HashMap等容器后,尽量避免修改。


引用外类的内部类


默认情况下,每个非静态内部类都包含对其外部类的隐式引用。如果我们在应用程序中使用这个内部类对象,那么即使在我们的外部类对象不再使用了,它也不会被垃圾收集。因为内部类对象隐式地保存对外部类对象的引用,从而使其成为垃圾收集的无效候选者。在匿名类的情况下也是如此。

如何避免

  • 如果内部类不需要访问当前包含这个内部类的父类的成员时,请考虑将其转换为静态类

ThreadLocal使用不当

ThreadLocal使我们能够将状态隔离到特定线程,从而允许我们实现线程安全。

一旦保持线程不再存在,ThreadLocals应该被垃圾收集。是当ThreadLocals与现代应用程序服务器一起使用时,问题就出现了。现代应用程序服务器使用线程池来处理请求而不是创建新请求(例如 ,在Apache Tomcat的情况下为Executor)。此外,他们还使用单独的类加载器。由于 应用程序服务器中的线程池在线程重用的概念上工作,因此它们永远不会被垃圾收集 - 相反,它们会被重用来处理另一个请求。现在,如果任何类创建 ThreadLocal 变量但未显式删除它,则即使在Web应用程序停止后,该对象的副本仍将保留在工作线程中,从而防止对象被垃圾回收。

如何避免

  • 最好手动调用ThreadLocal.remove()方法删除当前线程值,代码如下:
try {
    threadLocal.set(System.nanoTime());
    //... further processing
}
finally {
    threadLocal.remove();
}


finalize()方法


finalize是潜在的内存泄漏问题的另一个来源。每当重写类的 finalize()方法时,该类的对象不会立即被垃圾收集。相反,GC将它们排队等待最终确定,在稍后的时间点才会发送GC。

如果用finalize()方法编写的代码不是最佳的,并且finalize队列无法跟上Java垃圾收集器,那么迟早,我们的应用程序注定要遇到 OutOfMemoryError。

如何避免

  • 我们应该总是避免使用finalize方法


总结


本文主要分享了自己项目中遇到的一个内存泄漏的问题,然后引出了内存泄漏的几种常见场景,希望大家能够引起重视。

目录
相关文章
|
Kubernetes Linux Docker
Docker容器生产实践1——永远设置容器内存限制
背景 在默认情况下,docker容器并不会对容器内部进程使用的内存大小进行任何限制。对于PaaS系统而言,或者对于直接使用docker的用户而言,这非常危险。
3324 0
|
6月前
|
缓存 Java
《JVM由浅入深学习九】 2024-01-15》JVM由简入深学习提升分(生产项目内存飙升分析)
《JVM由浅入深学习九】 2024-01-15》JVM由简入深学习提升分(生产项目内存飙升分析)
52 0
|
5月前
|
存储 设计模式 监控
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
52 0
|
监控 安全 算法
Go项目一次内存溢出引发的安全事故
本文将介绍一起由于内存溢出引发的安全事故,事故导致系统崩溃和敏感数据泄露。我们将详细描述问题的背景、问题的发现方式、解决方案以及对此问题的思考和未来的应对方案。
151 0
|
存储 测试技术
《vSphere性能设计:性能密集场景下CPU、内存、存储及网络的最佳设计实践》一3.1.3 重现生产问题
本节书摘来华章计算机《vSphere性能设计:性能密集场景下CPU、内存、存储及网络的最佳设计实践》一书中的第3章 ,第3.1.3节,[美] 克里斯托弗·库塞克(Christopher Kusek) 著 吕南德特·施皮斯(Rynardt Spies)姚海鹏 刘韵洁 译, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1253 0
|
存储 测试技术 虚拟化
《vSphere性能设计:性能密集场景下CPU、内存、存储及网络的最佳设计实践》一3.1.1 在投入生产之前测试变化
本节书摘来华章计算机《vSphere性能设计:性能密集场景下CPU、内存、存储及网络的最佳设计实践》一书中的第3章 ,第3.1.1节,[美] 克里斯托弗·库塞克(Christopher Kusek) 著 吕南德特·施皮斯(Rynardt Spies)姚海鹏 刘韵洁 译, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1468 0
|
18天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
153 1
|
7天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
16天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
17天前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
18 3

热门文章

最新文章