揭秘Java开发中易踩的5个内存泄漏陷阱,附案例与方案

检测维修 0 157

内存泄漏有多可怕?

2024年阿里巴巴双十一的技术总结表明,经由处理内存泄漏状况,中心交易平台的运作效率增加了百分之四十。与此形成对照的是,另一个网络销售平台由于内存泄漏,其服务每三十分钟就会中断一次,由此造成的直接经济损失超过一百万。内存泄漏如同程序里的"潜藏祸患",起初难以发现,随着时间推移会致使系统运行缓慢、频繁执行Full GC,最终造成OutOfMemoryError(内存溢出错误)。现在我们就来剖析Java编程中最常见的5个内存泄漏问题,并且每个问题都配有实际案例和应对方法。

静态集合被称作"废弃箱":物品使用后不进行清除,会导致存储空间被完全占满,以下为相关代码片段:

// java
// 缓存用户数据的静态Map
public class UserCache {
private static Map userMap = new HashMap<>();
声明一个静态方法,命名为添加用户,参数为一个用户对象
将用户标识与用户对象关联起来,完成映射操作,记录到用户信息表中,确保对应关系准确无误。
// 没有过期清理机制!
}
}

为什么会泄漏?

静态数据集的存在期限与JVM相同,若在其中加入元素却不去除,这些元素将一直占据内存空间。某个社交应用就遭遇过此类问题,3个月内用户信息缓存消耗的内存从2GB急剧增加到16GB,最终导致系统因内存耗尽而崩溃。

Java内存泄漏 静态集合

正确做法:

1. 使用WeakHashMap:键不再被引用时自动删除

// java
private static Map userMap = new WeakHashMap<>();

2. 添加过期清理机制:

// java
// 使用Guava Cache设置最大容量和过期时间
private static Cache缓存构建器实例化自缓存构建器工厂,接着开始配置相关参数
.maximumSize(10000)
设置写入后过期时间为一个小时,单位是小时
.build();

二、资源不关闭:文件/数据库连接成"僵尸"问题代码示例:

// java
将路径参数作为输入,执行读取文件的操作,可能会引发输入输出异常
创建一个文件输入流对象,它指向指定的文件路径,用于读取文件内容
// 读取文件操作...
// 没有关闭流!如果发生异常,fis永远不会关闭
}

某个支付系统由于该段程序,在并发量大的情况下,打开的文件描述符数量突破了系统上限(标准值为1024个),从而引发"文件已满"的提示,服务因此完全中断。

Java 未关闭资源 代码示例

正确做法:使用try-with-resources自动关闭

// java
public void readFile(String path) throws IOException {
启动文件输入流,指向特定路径,完成资源获取,随后执行相关操作,最终关闭流资源。
// 读取文件操作...
}
// fis会自动关闭,无论是否发生异常
}

三、监听器成"钉子户":注册容易注销难问题代码示例:

// java
public class DataService {
public DataService() {
通过 EventBus 的默认实例,将当前对象作为监听者进行登记。
}
// 没有反注册方法!
}

安卓程序里经常出现这样的情况:界面组件绑定了感应设备观察者,却在界面销毁时没有解除绑定,造成界面对象不能被清除,内存占用不断增加,最终表现为程序运行缓慢。

Java 监听器未注销

正确做法:成对实现注册/注销

// java
public class DataService {
public void register() {
事件总线默认实例进行注册,目标对象为当前类。
}
public void unregister() {
移除该实例在默认事件总线上的注册功能,执行卸载操作
}

Java内存泄漏 静态集合_Java 未关闭资源 代码示例_内存溢出检测工具

// 在组件销毁时调用unregister() }

四、"健忘"特性:线程池重复使用造成内存持续占用状况代码范例:

// java
public class DateUtils {
私有静态最终线程局部变量 sdf =
使用ThreadLocal初始化一个函数,该函数返回一个SimpleDateFormat实例,格式为"yyyy-MM-dd"
公开静态字符串格式化日期参数,
获取日期对象,利用工具方法转换,然后输出结果
// 没有调用remove()!
}
}

在并发执行场景中,执行单元可以重复利用,局部变量如果不主动释放,会持续占用过往数据指针。某个经济领域应用因此造成日期格式化对象不断叠加,程序老空间存储容量已超九成。

Java ThreadLocal 内存泄漏

正确做法:使用后清理

// java
public static String format(Date date) {
try {
return sdf.get().format(date);
} finally {
sdf.remove(); // 手动清理
}
}

五、内部类别强行获取外部类别资源:由于隐式关联造成内存无法正常回收的情况代码范例:

// java
public class OuterClass {
声明一个字节数组bigData, 其容量为1024乘以1024乘以10个字节, 也就是一个10兆字节的大数据结构
生成一个可运行的任务,需要执行以下步骤,首先定义一个方法,然后返回一个实现了Runnable接口的实例,这个实例内部包含要执行的操作逻辑,最后通过这个方法可以获得一个可以在线程中运行的任务对象
创建一个匿名内部类实现Runnable接口,该类不是静态的
@Override
public void run() {
System.out.println("任务执行");
}
};
}
}

内部类会暗中持有外部类的引用,一旦内部类对象存在的时间超过外部类对象,例如被提交到线程管理器中运行,那么外部类中的大型对象将无法被清除。

Java 内部类 外部引用

正确做法:使用静态内部类

// java
public class OuterClass {
开辟了一块内存区域,用来存放大量数据,这块内存区域的大小是十兆字节,具体来说是一千个千字节乘以一千个字节
// 静态内部类不持有外部类引用
有一个公共静态类名为MyTask,该类实现了Runnable接口
@Override
public void run() {
System.out.println("任务执行");
}
}
public Runnable createTask() {
return new MyTask();
}
}

如何避免内存泄漏?

1. 遵循"谁创建谁销毁"原则:资源、监听器等要成对管理

静态集合要少用,最好采用WeakHashMap,或者选用具有过期机制的缓存,比如Caffeine这类实现

内存周期性检查:借助MAT、VisualVM之类的软件,来诊断堆内存状况

代码评审时需注意,ThreadLocal变量调用后是否调用了remove方法,内部类是否必须声明为非静态形式

务必牢记,Java的自动内存管理并非绝对可靠,优良的开发习惯才是防止内存泄漏的关键所在。请将此信息分享给同事,共同规避这些风险。

参考资料:

1. 阿里技术团队《Java内存治理实践》

2. 脚本之家《避免Java内存泄漏的10个黄金法则》

CSDN博客文章《凌晨三点紧急处理故障:Java内存溢出问题分析》

相关推荐: