什么是内存泄漏?该如何检测?又该如何解决?

简介: 这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?

一、介绍


1、什么是内存泄漏


java的优势之一就是内置了垃圾回收器GC,它帮助我们实现了自动化内存管理。但是GC再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,

v2-7b3da0fa8e9644dd32ecf62a3c57e01e_1440w.jpg

也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。


正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。


2、内存泄漏的危害


  • 长时间运行,程序变卡,性能严重下降
  • 程序莫名其妙挂掉
  • OutOfMemoryError错误
  • 乱七八糟的错误,还不易排查


反正内存泄漏不是好事。


二、内存泄漏原因


内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。


1、大量使用静态变量


静态变量的生命周期与程序一致。因此常驻内存。

public class StaticTest {
    public static  List<Integer> list = new ArrayList<>();
    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add((int)Math.random());
        }
        System.out.println("running......");
    }
    public static void main(String[] args) {
      System.out.println("before......");
        new StaticTest().populateList();
        System.out.println("after......");
    }
}

现在可以使用jvisualvm运行一边,看看内存效果。


  • 带static关键字(使用静态变量)

v2-125caa4526725a7b943b7db3616b2468_1440w.jpg

从上图可以看到,堆内存从一开始的135M左右飙升了到了200M。直接占据了65M的内存。


  • 不使用static关键字(不使用静态变量)

v2-239d5be403722a58608c5c713f1943ad_1440w.jpg

由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150M。

总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏


2、连接资源未关闭


每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。

public class FileTest {
 public static void main(String[] args) throws IOException {
  File f=new File("G:\\nginx配套资料\\笔记资料.zip");
  System.out.println(f.exists());
        System.out.println(f.isDirectory());
 }
}

依然使用jvisualvm运行一边,看看内存效果。

v2-267bd9a052fa4770d8c91d63b052deee_1440w.jpg

可以看出,在连接文件资源时,jvm会为本资源分配内存。


3、equals()和hashCode()方法使用不当


定义新类时,如果没有重新equals()和hashCode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:

public class User{
 public String name;
 public int age;
 public User(String name, int age) {
  this.name = name;
  this.age = age;
 }
}
public class EqualTest {
 public static void main(String[] args) {
  Map<User, Integer> map = new HashMap<>();
     for(int i=0; i<100; i++) {
         map.put(new User("", 1), 1);
     }
        System.out.println(map.size() == 1);//输出为false
 }
}

然后运行一下,看看内存情况:

v2-01c8306227220ec548367b20ee250762_1440w.jpg

内存从150M一下子飙升到225M,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像HashMap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个User其hashcode不同。


现在重写这俩方法再运行一边:

public class User{
 public static String name;
 public User(String name) {
  this.name = name;
 }
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof User)) {
            return false;
        }
        User user = (User) o;
        return User.name.equals(name);
    }
    @Override
    public int hashCode() {
        return name.hashCode();
    } 
}

在EqualTest类再测试一遍,首先看看内存变化:

v2-b11593c807ad23a5940706c26b27cdb3_1440w.jpg

上图可以看到上升幅度没那么大。而且输出为true,这是肯定的,由于重写了hashcode和equal,所以HashMap添加的肯定是同一个对象。


4、内部类持有外部类


这个场景和上面类似。


5、finalize方法


这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图

v2-04dd22d3a4b3ab081dd70ee3bfeb8b23_1440w.jpg

这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。


6、ThreadLocal的错误使用


ThreadLocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。

v2-f6c29fc4c25d1e7ee985952ed7e8318c_1440w.jpg

上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。


1、Thread中有一个map,就是ThreadLocalMap


2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。


3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收


4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。


解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。


三、检测内存泄漏


检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:


1、工具分析


这个工具比较多,比如说JProfiler、YourKit、Java VisualVM和Netbeans Profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存CPU的等等各种情况。上面多次演示到了。


2、垃圾回收分析


这个其实也可以用工具进行分析。上面的VisualVM中,可以打印堆。也可以从外部导入dump文件进行分析。

如果不用工具的话,我们可以通过IDE看到。JVM配置添加-verbose:gc。然后就会打印出相关信息。

v2-90956649565bac428657a0e56338ed8f_1440w.jpg

3、基准测试


也就是使用科学的方式进行分析java代码的性能。进而判断分析。


四、结论


内存泄漏是个很严重的问题,也比较常见。最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。

相关文章
|
8天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
29 6
|
17天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
114 9
|
17天前
|
监控 JavaScript 前端开发
如何检测和解决 JavaScript 中内存泄漏问题
【10月更文挑战第25天】解决内存泄漏问题需要对代码有深入的理解和细致的排查。同时,不断优化和改进代码的结构和逻辑也是预防内存泄漏的重要措施。
34 6
|
20天前
|
Web App开发 缓存 JavaScript
如何检测和解决闭包引起的内存泄露
闭包引起的内存泄露是JavaScript开发中常见的问题。本文介绍了闭包导致内存泄露的原因,以及如何通过工具检测和代码优化来解决这些问题。
|
2月前
|
C语言 Android开发 C++
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
本文介绍了使用MTuner软件进行Qt MinGW编译程序的内存泄漏检测的方法,提供了MTuner的下载链接和测试代码示例,并通过将Debug程序拖入MTuner来定位内存泄漏问题。
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
|
4月前
|
存储 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
65 0
|
1月前
|
Web App开发 开发者
|
23天前
|
缓存 监控 Java
内存泄漏:深入理解、检测与解决
【10月更文挑战第19天】内存泄漏:深入理解、检测与解决
39 0
|
1月前
|
设计模式 Java Android开发
安卓应用开发中的内存泄漏检测与修复
【9月更文挑战第30天】在安卓应用开发过程中,内存泄漏是一个常见而又棘手的问题。它不仅会导致应用运行缓慢,还可能引发应用崩溃,严重影响用户体验。本文将深入探讨如何检测和修复内存泄漏,以提升应用性能和稳定性。我们将通过一个具体的代码示例,展示如何使用Android Studio的Memory Profiler工具来定位内存泄漏,并介绍几种常见的内存泄漏场景及其解决方案。无论你是初学者还是有经验的开发者,这篇文章都将为你提供实用的技巧和方法,帮助你打造更优质的安卓应用。
|
1月前
|
数据处理 Python
Python读取大文件的“坑“与内存占用检测
Python读取大文件的“坑“与内存占用检测
47 0