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的引用链上,不会被回收。

如果需要解决这样的问题,我们可以使用静态内部类,再简单的复习一下一般内部类和静态内部类的区别:
-
静态内部类可以直接通过外部类访问:静态内部类是独立的,不依赖于外部类的实例,因此可以直接通过外部类来访问。(解决内部类引用外部类内存溢出的关键)
-
静态内部类不能访问外部类的非静态成员:由于静态内部类是独立的,因此无法访问外部类的非静态成员变量和方法。
-
静态内部类可以直接创建实例:可以直接通过"外部类.内部类"的方式创建静态内部类的实例,而不需要先创建外部类的实例。
改造案例中的代码:
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配置
最终同样会造成内存溢出。
相关文章:
JVMの内存泄漏内存溢出案例分析
1、内存溢出 内存溢出指的是程序在申请内存时,没有足够的内存可供分配,导致无法满足程序的内存需求,常见的内存溢出情况包括堆内存溢出(Heap Overflow)和栈溢出(Stack Overflow): …...
v31支架固定方式
CK_Label_v31 夹子固定方式 底座粘贴固定方式...
Jenkins从入门到精通面试题及参考答案(3万字长文)
目录 什么是Jenkins? Jenkins是如何工作的? Jenkins与持续集成(CI)有什么关系?...
如何使用电阻器?创建任何电阻的简单过程
您可能有一整盒E12 系列电阻器,但仍然无法获得足够接近您所需电阻的值。如果您需要 50 kΩ 电阻,接近的电阻是 47 kΩ。当然,这个误差在 10% 以内,但这对于您的应用程序来说可能还不够好。你会怎样做? 本文将介绍一个…...
学Python,看一篇就够
学Python,看一篇就够 python基础注释变量标识符命名规则使用变量认识bugDebug工具打断点 数据类型输出转义字符输入输入语法输入的特点 转换数据类型pycharm交互运算符的分类赋值运算符复合赋值运算符比较运算符逻辑运算符拓展 条件语句单分支语法多分支语法拓展 if…...
数据仓库核心:维度表设计的艺术与实践
文章目录 1. 引言1.1基本概念1.2 维度表定义 2. 设计方法2.1 选择或新建维度2.2 确定维度主维表2.3 确定相关维表2.14 确定维度属性 3. 维度的层次结构3.1 举个例子3.2 什么是数据钻取?3.3 常见的维度层次结构 4. 高级维度策略4.1 维度整合维度整合:构建…...
SQL实验 连接查询和嵌套查询
一、实验目的 1.掌握Management Studio的使用。 2.掌握SQL中连接查询和嵌套查询的使用。 二、实验内容及要求(请同学们尝试每道题使用连接和嵌套两种方式来进行查询,如果可以的话) 1.找出所有任教“数据…...
【JAVA WEB实用技巧与优化方案】Maven自动化构建与Maven 打包技巧
文章目录 一、MavenMaven生命周期介绍maven生命周期命令解析二、如何编写maven打包脚本maven 配置详解setting.xml主要配置元素setting.xml 详细配置使用maven 打包springboot项目maven 引入使用package命令来打包idea打包三、使用shell脚本自动发布四、使用maven不同环境配置加…...
详细分析Mysql中的SQL_MODE基本知识(附Demo讲解)
目录 前言1. 基本知识2. Demo讲解2.1 ONLY_FULL_GROUP_BY2.2 STRICT_TRANS_TABLES2.3 NO_ZERO_IN_DATE2.4 NO_ENGINE_SUBSTITUTION2.5 ANSI_QUOTES 前言 了解Mysql内部的机制有助于辅助开发以及形成整体的架构思维 对于基本的命令行以及优化推荐阅读: 数据库中增…...
vue3+uniapp
1.页面滚动 2.图片懒加载 3.安全区域 4.返回顶部,刷新页面 5.grid布局 place-self: center; 6.模糊效果 7.缩放 8.微信小程序联系客服 9.拨打电话 10.穿透 11.盒子宽度 12.一般文字以及盒子阴影 13.选中文字 14.顶部安全距离 15.onLoad周期函数在setup语法糖执行后…...
组织病理学结合人工智能之后,如何实际应用于临床?|顶刊精析·24-06-06
小罗碎碎念 今天这篇文章选自21年5月发表的nature medicine,标题名为——Deep learning in histopathology: the path to the clinic,这篇文章也是我规划的病理组学文献精析的第三篇,如果你能坚持把七篇都看完,相信你脑海中一定会…...
VCAST创建单元测试工程
1. 设置工作路径 选择工作目录,后面创建的 UT工程 将会生成到这个目录。 2. 新建工程 然后填写 工程名称,选择 编译器,以及设置 基础路径。注意 Base Directory 必须要为代码工程的根目录,否则后面配置环境会失败。 这样工程就创建好了。 把基础路径设置为相对路径。 …...
数据结构之归并排序算法【图文详解】
P. S.:以下代码均在VS2019环境下测试,不代表所有编译器均可通过。 P. S.:测试代码均未展示头文件stdio.h的声明,使用时请自行添加。 博主主页:LiUEEEEE …...
设计模式基础
什么是设计模式 设计模式是一种在软件设计过程中反复出现的问题和相应解决方案的描述。它是一种被广泛接受的经验总结,可以帮助开发人员解决常见的设计问题并提高代码的重用性、可维护性和可扩展性。 设计模式可以分为三类: 创建型模式(Crea…...
Glide支持通过url加载本地图标
序言 glide可以在load的时候传入一个资源id来加载本地图标,但是在开发过程中。还得区分数据类型来分别处理。这样的使用成本比较大。希望通过自定义ModelLoader实现通过自定义的url来加载Drawab。降低使用成本 实现 一共四个类 类名作用GlideIcon通过自定义url的…...
网络安全形势与WAF技术分享
我一个朋友的网站,5月份时候被攻击了,然后他找我帮忙看看,我看他的网站、网上查资料,不看不知道,一看吓一跳,最近几年这网络安全形势真是不容乐观,在网上查了一下资料,1、中国信息通…...
【实战JVM】-实战篇-06-GC调优
文章目录 1 GC调优概述1.1 调优指标1.1.1 吞吐量1.1.2 延迟1.1.3 内存使用量 2 GC调优方法2.1 发现问题2.1.1 jstat工具2.1.2 visualvm插件2.1.3 PrometheusGrafana2.1.4 GC Viewer2.1.5 GCeasy 2.2 常见GC模式2.2.1 正常情况2.2.2 缓存对象过多2.2.3 内存泄漏2.2.4 持续FullGC…...
深入解析智慧互联网医院系统源码:医院小程序开发的架构到实现
本篇文章,小编将深入解析智慧互联网医院系统的源码,重点探讨医院小程序开发的架构和实现,旨在为相关开发人员提供指导和参考。 一、架构设计 智慧互联网医院系统的架构设计是整个开发过程的核心,直接影响到系统的性能、扩展性和维…...
获取 Bean 对象更加简单的方式
获取 bean 对象也叫做对象装配,是把对象取出来放到某个类中,有时候也叫对象注⼊。 对象装配(对象注⼊)即DI 实现依赖注入的方式有 3 种: 1. 属性注⼊ 2. 构造⽅法注⼊ 3. Setter 注⼊ 属性注入 属性注⼊是使⽤ Auto…...
ChatGPT基本原理
技术背景与基础: 深度学习:ChatGPT建立在深度学习技术之上,通过复杂的神经网络结构模拟人类的语言处理过程。深度学习使得ChatGPT能够处理海量的文本数据,并从中提取出复杂的语言模式和规律。GPT架构:ChatGPT基于GPT&a…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
C++初阶-list的底层
目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...
Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...
《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
