JVMの内存泄漏内存溢出案例分析

1、内存溢出

        内存溢出指的是程序在申请内存时,没有足够的内存可供分配,导致无法满足程序的内存需求,常见的内存溢出情况包括堆内存溢出(Heap Overflow)和栈溢出(Stack Overflow):     

  • 堆内存溢出通常发生在程序申请的对象过多,堆内存无法满足这些对象的存储需求时。
  • 栈溢出则通常发生在方法调用层次过深,导致栈空间耗尽。(例如递归没有正确设置退出条件)

2、内存泄漏

        内存泄漏指的是程序在使用完内存后未能正确释放(回收)这些内存,导致程序长时间运行后占用的内存逐渐增加,最终耗尽系统的可用内存,内存泄漏通常是由于程序中存在未释放的无用对象或资源(如文件句柄、数据库连接等)引起的。

        简单的说,就是正常情况下,某个对象不再被程序使用的同时,理应不存在GC Root的引用链上,在下一次GC时被回收,而造成内存泄漏的情况下,即使某个对象不再被程序使用,依旧存在于GC Root的引用链上,导致一直无法被回收,最终会导致内存溢出

3、监控内存

        监控内存的方式有很多种,这里介绍一种使用JDK 1.8自带的VisualVM工具(JDK 1.8之后需要自行下载):

        位于JDK的bin目录下:

        也可以通过IDEA集成VisualVM插件的方式:

        插件安装完毕后需要进行设置,路径为JDK下的bin目录中的文件。

         在启动程序时选择:

        会自动弹出Visual界面,进行监控:


使用案例:

        这里有一段程序:

public class Demo0 {public static long count = 0;public static void main(String[] args) throws InterruptedException {while (true){byte[] bytes = new byte[1024 * 1024 * 5];}}
}

        byte[]数组虽然是强引用,但是作用域只是在每次循环中,一旦循环结束,它们就会超出作用域而无法再被访问到,所以不会发生内存溢出,对应的内存图如下:

         接下来改写一下这个程序:

public class Demo0 {public static long count = 0;public static void main(String[] args) throws InterruptedException {List<byte[]> byteList = new ArrayList<>();while (true){byte[] bytes = new byte[1024 * 1024 * 5];byteList.add(bytes);}}
}

        不同于上一个案例。在循环外创建了一个集合,每次都将循环中的bytes的引用放入集合中,最终集合无法被垃圾回收,导致内存溢出:

         通过上面两种情况,可以发现,在正常情况下,内存曲线应该是在一个固定的范围内起伏的,而内存溢出的情况则是曲线持续增长,即使手动进行GC也无法回收大部分的对象。

4、内存溢出原因分析

        在实际应用中,造成内存溢出的原因一般会有两种,第一种是因为代码中的不规范做法/bug,第二种则是因为某个接口同一时间的并发请求过多,而处理速度慢造成的。

        代码中的内存溢出:

        4.1、未正确重写hashCode()equals()方法

        我现在有一个Student类:

public class Student {private String name;private Integer id;private byte[] bytes = new byte[1024 * 1024];public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}
}

        在主类中通过一个静态HashMap在一个死循环中存放Student对象(关于静态问题后面会分析):

public class Demo2 {public static long count = 0;public static Map<Student,Long> map = new HashMap<>();public static void main(String[] args) throws InterruptedException {while (true){if(count++ % 100 == 0){Thread.sleep(10);}Student student = new Student();student.setId(1);student.setName("张三");map.put(student,1L);}}
}

        结果是发生了内存溢出。

        要了解为什么上面的做法会导致内存溢出,我们首先复习一下一个元素是如何放入HashMap的,当向 HashMap 中放入一个元素时,会经历以下过程:

  • HashMap 会调用键的hashCode()方法来计算键的哈希值。哈希值是用来确定键值对在哈希表中存储位置的重要依据。
  • HashMap 会根据计算得到的哈希值和哈希表的大小,确定键值对在哈希表中的存储位置。(通过取模运算记录桶下标)
  • 如果存在hash冲突就会进行处理(链表+红黑树),HashMap 将键值对插入到确定的存储位置中,如果存在相同键(根据equals()方法判断),则会更新对应的值。

        由此可见,hashCode()equals()方法在上面的过程中至关重要。如果我们没有重写hashCode()equals()方法,默认会使用Object类中的,我们可以点进去看一下:

        Object中的hashCode() 方法使用的是本地方法,equals()方法使用的是==,比较的是地址值。

        在上面的案例中,使用了Object中的hashCode()equals() 方法,可能会导致相同ID的对象,计算出的hash值却不一样,就会放在hashMap不同的槽位上。而equals() 方法比较的是地址值:

 Student student = new Student();

        每一个创建出的对象的地址值都是不一样的,导致即使学生的id和name相同,也是不同的对象,导致hashMap中存在的无法被回收的对象持续增加,最终OOM

        而我们想要的效果是,后一个相同的key覆盖前一个相同的key。就需要重写hashCode()equals() 方法:

    @Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}Student student = (Student) o;return new EqualsBuilder().append(id, student.id).isEquals();}@Overridepublic int hashCode() {return new HashCodeBuilder(17, 37).append(id).toHashCode();}

        所以在定义Java Bean时,需要手动重写hashCode()equals() 方法 ,并且在定义HashMap时,不建议使用对象作为Key的类型,推荐使用String类型,提高查找效率。

        4.2、内部类引用外部类

        首先来简单复习一下什么是外部类和内部类:

  • OuterClass是一个外部类,外部类可以直接访问其内部定义的成员变量和方法,但无法直接访问内部类的成员。
  • InnerClass是一个内部类,内部类可以访问外部类的所有成员,包括私有成员,并且可以直接访问外部类的方法和字段。
public class OuterClass {private int outerVar;public void outerMethod() {// 可以访问内部类}// 内部类的定义public class InnerClass {public void innerMethod() {// 可以访问外部类的成员变量和方法outerVar = 10;outerMethod();}}
}

        在创建内部类的实例时,需要用到外部类的实例

 public static void main(String[] args) {OuterClass outer = new OuterClass();OuterClass.InnerClass inner = outer.new InnerClass();  // 创建内部类的实例需要使用外部类的实例inner.innerMethod();  // 调用内部类的方法}

        一个内部类引用外部类导致内存溢出的案例:

public class Outer {private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据private String name = "测试";class Inner {private String name;public Inner() {this.name = Outer.this.name;}}public static void main(String[] args) throws IOException, InterruptedException {
//        System.in.read();int count = 0;//集合存放的是Outer外部类中Inner内部类的对象ArrayList<Inner> inners = new ArrayList<>();while (true) {if (count++ % 100 == 0) {Thread.sleep(10);}//创建内部类,需要用到外部类的实例inners.add(new Outer().new Inner());}}
}

        我们在inners.add(new Outer().new Inner());这一行打一个断点:

        内部类中持有了一个外部类的引用 ,导致外部类此时也在GC Root的引用链上,不会被回收。

        如果需要解决这样的问题,我们可以使用静态内部类,再简单的复习一下一般内部类和静态内部类的区别:

  1. 静态内部类可以直接通过外部类访问静态内部类是独立的,不依赖于外部类的实例,因此可以直接通过外部类来访问。(解决内部类引用外部类内存溢出的关键)

  2. 静态内部类不能访问外部类的非静态成员:由于静态内部类是独立的,因此无法访问外部类的非静态成员变量和方法。

  3. 静态内部类可以直接创建实例:可以直接通过"外部类.内部类"的方式创建静态内部类的实例,而不需要先创建外部类的实例。

         改造案例中的代码:

public class Outer {private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据private static String name = "测试";static class Inner {private String name;public Inner() {this.name = Outer.name;}}public static void main(String[] args) throws IOException, InterruptedException {
//        System.in.read();int count = 0;ArrayList<Inner> inners = new ArrayList<>();while (true) {if (count++ % 100 == 0) {Thread.sleep(10);}inners.add(new Inner());}}
}

        此时内部类完全独立,不再持有外部类的引用,所以外部类可以正常被回收。

        4.3、ThreadLocal的不正确使用

        如果是在手动创建线程的线程中使用ThreadLocal,一般不会造成内存溢出:

public class Demo5_1 {public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();public static void main(String[] args) throws InterruptedException {while (true) {new Thread(() -> {threadLocal.set(new byte[1024 * 1024 * 10]);}).start();Thread.sleep(10);}}
}

        每个线程对ThreadLocal中存储的对象都有独立的副本,线程一旦结束,其中的内存便会得到释放,即使不使用.remove()方法,如果每次存放入ThreadLocal的数据量不大,也不一定会发生内存溢出。

        当使用线程池统一创建线程时,线程不一定是立刻被回收,如果没有使用.remove()方法 ,则大概率会造成内存溢出。

public class Demo5 {public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();public static void main(String[] args) throws InterruptedException {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(Integer.MAX_VALUE, Integer.MAX_VALUE,0, TimeUnit.DAYS, new SynchronousQueue<>());int count = 0;while (true) {System.out.println(++count);threadPoolExecutor.execute(() -> {threadLocal.set(new byte[1024 * 1024]);});Thread.sleep(10);}}
}

        解决方式也很简单,线程中的逻辑执行完成后,手动调用ThreadLocal的.remove()方法。

        4.4、String的Intern()方法

        Intern() 方法的作用是,将调用该方法的字符串放入字符串常量池中,在JDK 1.8中,字符串常量池位于堆中。

        如果不同字符串的Intern() 方法被大量调用,达到堆内存上限后也会造成内存溢出的问题。(在实际开发中很少遇到,了解即可)

       4.5、通过static字段修饰的容器保存对象

        在前篇中提到,如果某个类的静态资源被引用,即使该类的实例全部不可达,该类也无法被回收。 并且static 字段属于类级别的,而不是属于某个实例,生命周期和类一样长,因此保存在其中的对象也会持续存在直到类被卸载。

        当大量对象被保存在 static 字段所属的类中时,这些对象将随着类的加载而被创建并持续存在于堆内存中。如果这些对象没有被及时释放,就会导致堆内存不断被占用,最终导致内存溢出。

        static 字段属于类级别的,所有实例共享同一个 static 字段,因此如果保存在其中的对象过多或者对象占用过多内存,就会对整个应用产生影响,容易导致内存资源的耗尽。

        所以被static关键字修饰的变量,当不再使用时,需要手动将引用设置为null方便下次回收。

       4.6、IO或数据库连接资源没有及时关闭

       IO或数据库连接资源没有及时关闭,并不一定会100%导致内存泄漏,其原因与在手动创建线程中使用完ThreadLocal后没有手动调用.remove()方法类似。如果是在连接池中使用,或者短时间内连接数过多,依旧有可能会造成内存溢出。

        推荐使用JDK 7 的新特性try..with...resources进行连接管理。

        前提是被管理的连接需要实现AutoCloseable接口。


        另一个可能导致内存溢出的原因,在于多线程并发访问时:

        4.7、多线程并发访问

        通常,用户在页面上点击按钮发送请求,服务器端会通过数据库进行处理,将查询的结果集读取到内存中并且返回给页面,然后就可以释放这部分内存。但是如果处理逻辑复杂,过程消耗时间较久,同时又有大量的请求,会导致数据全部积压在内存中,最终导致内存溢出。

        例如下面这一段代码,模拟了数据量大,并且处理时间长的场景:

    @GetMapping("/test")public void test1() throws InterruptedException {byte[] bytes = new byte[1024 * 1024 * 100];//100mThread.sleep(10 * 1000L);}

        如果需要演示高并发的场景,可以通过压力测试工具实现,我这里使用Jmeter。

        准备工作:将最大堆内存和初始堆内存设置成1g

        配置Jmeter

        经过了100次/s的请求,发生了内存溢出:


        再模拟一种使用静态关键字修饰的容器存放大量数据的情况:

    /*** 登录接口 传递名字和id,放入hashmap中*/@PostMapping("/login")public void login(String name, Long id) {userCache.put(id, new UserEntity(id, name));}

        Jmeter配置

         

        最终同样会造成内存溢出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/340636.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《数字图像处理-OpenCV/Python》第15章:图像分割

《数字图像处理-OpenCV/Python》第15章&#xff1a;图像分割 本书京东 优惠购书链接 https://item.jd.com/14098452.html 本书CSDN 独家连载专栏 https://blog.csdn.net/youcans/category_12418787.html 第15章&#xff1a;图像分割 图像分割是由图像处理到图像分析的关键步骤…

spark的简单学习二

一 spark sql基础 1.1 Dataframe 1.介绍&#xff1a; DataFrame也是一个分布式数据容器。然而DataFrame更像传统数据库的二维表 格&#xff0c;除了数据以外&#xff0c;还掌握数据的结构信息&#xff0c;即schema。同时&#xff0c;与Hive类似&#xff0c;DataFrame也支 持…

STM32_HAL_I2C_串行接口

电气特性 I2C&#xff08;Inter-Integrated Circuit&#xff09;是一种由飞利浦公司&#xff08;现恩智浦半导体&#xff09;开发的串行通信协议&#xff0c;用于连接低速外围设备。I2C总线只需要两根线&#xff08;SDA&#xff1a;串行数据线&#xff0c;SCL&#xff1a;串行…

免费生物蛋白质的类chatgpt工具助手copilot:小分子、蛋白的折叠、对接等

参考: https://310.ai/copilot 可以通过自然语言对话形式实现小分子、蛋白质的相关处理:生成序列、折叠等 应该是agent技术调用不同工具实现 从UniProt数据库中搜索和加载蛋白质。使用ESM Fold方法折叠蛋白质。使用310.ai基础模型设计新蛋白质。使用TM-Align方法比较蛋白质…

【Spring Cloud】微服务链路跟踪Sleuth

目录 为什么要使用微服务链路跟踪微服务的现状多服务协同工作复杂的调用链条容易出错 微服务链路跟踪需要实现的需求实现监控决策避免技术债务快速定位故障 微服务链路跟踪的技术要求低消耗应用透明延展性可控采样率可视化 Spring Cloud Sleuth简介Spring Cloud Sleuth的4个特点…

Shell脚本快速入门

为什么要学shell&#xff1f;能做什么&#xff1f; 答&#xff1a;CI/CD 持续集成&#xff0c;自动化部署作业方式&#xff0c;需要将一系列linux命令程序化&#xff0c;shell 就能做到&#xff0c;提高运维人员的工作效率。 指定解析器&#xff1a; (1) shell解析器 #…

Linux网络-自定义协议、序列化和反序列化、网络计算服务器的实现和Windows端客户端

文章目录 前言一、自定义协议传结构体对象 序列化和反序列化什么是序列化&#xff1f;反序列化 二、计算器服务端&#xff08;线程池版本&#xff09;1.main.cc2.Socket.hpp3.protocol.hpp4.Calculator.hpp5.serverCal.hpp6.threadPool.hpp7.Task.hpp8. log.hpp 客户端Windows客…

小白级教程—安装Ubuntu 20.04 LTS服务器

下载 本教程将使用20.04版进行教学 由于官方速度可能有点慢&#xff0c;可以下方的使用清华镜像下载 https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/ 点击20.24版本 选择 ubuntu-20.04.6-live-server-amd64.iso 新建虚拟机 下载好后 我们使用 VMware 打开它 这里选…

一篇文章讲透排序算法之归并排序

0.前言 本篇文章将详细解释归并排序的原理&#xff0c;以及递归和非递归的代码原理。 一.概念 归并排序是建立在归并操作上的一种有效的排序算法&#xff0c;该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并&#xff0c;得到完全有序的序列&#xff1b;即先使…

苹果或面临退一赔三,新iPad悄悄砍了核心规格

618 快过去了一半&#xff0c;各家都卖的如火如荼&#xff0c;这其中当属苹果搞得最热火朝天。 某东手机竞速榜中&#xff0c;iPhone15 Pro Max 销量稳坐头把交椅&#xff0c;平板方面虽然没有统计表&#xff0c;但是相信销量也是不差。 加上今年刚刚发布的新系列的 iPad&…

求助!什么软件可以人声分离?手机上可以进行人声分离操作吗?

在数字时代&#xff0c;音频处理变得越来越重要&#xff0c;而人声分离技术则是其中的一项关键技术。很多人可能都有过这样的疑问&#xff1a;什么软件可以实现人声分离&#xff1f;手机上能否进行人声分离操作&#xff1f;今天&#xff0c;我们就来为大家解答这些问题&#xf…

提取伴奏与人声分离软件:5款手机必备音频软件

在数字音乐的浪潮中&#xff0c;音频处理软件已经成为手机用户不可或缺的工具。特别是在音乐制作、卡拉OK伴奏制作以及日常音频编辑中&#xff0c;人声与伴奏的分离显得尤为重要。本文将为您介绍五款免费且实用的手机音频软件&#xff0c;它们都具有人声与伴奏分离的功能&#…

spring boot 3.x版本 引入 swagger2启动时报错

一&#xff0c;问题 Spring Boot 3.x版本的项目里&#xff0c;准备引入Swagger2作为接口文档&#xff0c;但是项目启动报错&#xff1a; java.lang.TypeNotPresentException: Type javax.servlet.http.HttpServletRequest not present at java.base/sun.reflect.generics.…

安装windows x64的开源录屏软件GifCapture

下载压缩包 GIF软件 安装报错 下载.NET桌面版运行 .NET 即可在最近安装部分找到GifCapture打开使用

容器项目之前后端分离

容器化部署ruoyi项目 #需要的镜像nginx、java、mysql、redis、 #导入maven镜像、Java镜像和node镜像 docker load -i java-8u111-jdk.tar docker load -i maven-3.8.8-sapmachine-11.tar docker load -i node-18.20.3-alpine3.20.tar #拉取MySQL和nginx镜像 docker pull mysql…

【JavaScript】ECMAS6(ES6)新特性概览(二):解构赋值、扩展与收集、class类全面解析

🔥 个人主页:空白诗 🔥 热门专栏:【JavaScript】 文章目录 🌿 引言五、 Destructuring Assignment - 解构赋值,数据提取的艺术 🎨📌 数组解构📌 对象解构&

文件夹突变解析:类型变文件的数据恢复与预防

在数字化时代&#xff0c;文件夹作为我们存储和组织数据的基本单元&#xff0c;其重要性不言而喻。然而&#xff0c;有时我们可能会遇到一种令人困惑的情况——文件夹的类型突然变为文件&#xff0c;导致无法正常访问其中的内容。这种现象不仅会影响我们的工作效率&#xff0c;…

Solon2分布式事件总线的应用价值探讨

随着现代软件系统的复杂性日益增加&#xff0c;微服务架构逐渐成为开发大型应用的主流选择。在这种架构下&#xff0c;服务之间的通信和协同变得至关重要。Solon2作为一个高性能的Java微服务框架&#xff0c;其分布式事件总线&#xff08;Distributed Event Bus&#xff09;为微…

FastAPI给docs/配置自有域名的静态资源swagger-ui

如果只是要解决docs页面空白的问题&#xff0c;可先看我的这篇博客&#xff1a;FastAPI访问/docs接口文档显示空白、js/css无法加载_fastapi docs打不开-CSDN博客 以下内容适用于需要以自用域名访问swagger-ui的情况&#xff1a; 1. 准备好swagger-ui的链接&#xff0c;如&am…

读后感:《SQL数据分析实战》运营SQL实用手册

学习SQL&#xff0c;先有用起来&#xff0c;有了使用价值&#xff0c;之后才是去了解它的原理&#xff0c;让使用更加顺畅。 在大部分业务场景中&#xff0c;通过SQL可以快速的实现数据处理与统计。《SQL数据分析实战》区别于其他工具书&#xff0c;它并没有介绍SQL是什么&…