Java 内存泄露详解

一、Java GC机制

Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存。理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露。

二、Java 中的内存管理

要了解Java中的内存泄露,首先就得知道Java中的内存是如何管理的。在Java程序中,我们通常使用new为对象分配内存,而这些内存空间都在堆(Heap)上。下面看一个示例:

public class JavaGcTest {
    public static void main(String[] args) {
         Object obj1 = new Object(); //Object1 对象
         Object obj2 = new Object(); //Object2 对象
         obj2 = obj1; // obj2引用指向了Object1 对象
         //...此时,已经没有对象引用指向Object2对象了,Object2可以被清理了
    }
}

Java使用有向图的方式进行内存管理:


内存对象有向图

在有向图中,我们叫作Object1对象是可达的,Object2对象就是不可达的,显然不可达的可以被清理。内存的释放,也即清理那些不可达的对象,是由GC决定和执行的,所以GC会监控每一个对象的状态,包括申请、引用、被引用和赋值等。释放对象的根本原则就是对象不会再被使用:

  • 给对象赋予了空值null,之后再没有调用过。
  • 另一个是给对象赋予了新值,这样重新分配了内存空间。

三、Java 中的内存泄露

Java中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露。

我们知道,对象都是有生命周期的,有的长,有的短,如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。我们举一个简单的例子:

public class Simple {
    Object object;

    public void method1(){
        object = new Object();
    //...其他代码
    }
}

这里的object实例,其实我们期望它只作用于method1()方法中,且其他地方不会再用到它,但是,当method1()方法执行完成后,object对象所分配的内存不会马上被认为是可以被释放的对象,只有在Simple类创建的对象被释放后才会被释放,严格的说,这就是一种内存泄露。解决方法就是将object作为method1()方法中的局部变量。当然,如果一定要这么写,可以改为这样:

public class Simple {
    Object object;
 
    public void method1(){
        object = new Object();
        //...其他代码
        object = null;
    }
}

这样,之前“new Object()”分配的内存,就可以被GC回收。

四、发生内存泄露的解决方法

像上面例子中的情况很容易发生,也是我们最容易忽略并引发内存泄露的情况,解决的原则就是尽量减小对象的作用域以及手动设置null值。

五、容器使用时的内存泄露

void method(){
   Vector vector = new Vector();
   for (int i = 1; i<100; i++)
   {
       Object object = new Object();
       vector.add(object);
       object = null;
   }
   //...对vector的操作
   //...与vector无关的其他操作
}

这里内存泄露指的是在对vector操作完成之后,执行下面与vector无关的代码时,如果发生了GC操作,vector不会被回收,vector里的一系列Object对象也不会被回收,而此处的内存泄露可能是短暂的,因为在整个method()方法执行完成后,vector才有可能被回收(要等GC的时候)。这里要解决很简单,手动vector赋值为null即可。

不过上例中,容器是方法内的局部变量,造成的内存泄漏影响可能不算很大,但是,如果这个容器作为一个类的成员变量,甚至是一个静态(static)的成员变量时,就要更加注意内存泄露了。

六、资源未关闭造成的内存泄漏

比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,以及使用其他框架的时候,除非其显式的调用了其close()方法(或类似方法)将其连接关闭,否则是不会自动被GC回收的。其实原因依然是长生命周期对象持有短生命周期对象的引用。

七、监听器

在释放对象的时候却没有去删除这些监听器,增加了内存泄漏的机会。

八、单例模式导致的内存泄露

单例模式,很多时候我们可以把它的生命周期与整个程序的生命周期看做差不多的,所以是一个长生命周期的对象。如果这个对象持有其他对象的引用(成员变量),也很容易发生内存泄露。

其实,正常来说,单例不应该有成员变量,单例应该要保证其无状态性

九、检查和分析内存泄漏工具

因为内存泄漏是在堆内存中,所以对我们来说并不是可见的。通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。

  • MAT (Memory Analyse Tool)是一款强大的内存分析工具,功能繁多而复杂。
  • LeakCanary则是由Square开源的一款轻量级的第三方内存泄漏检测工具,当检测到程序中产生内存泄漏时,它将以最直观的方式告诉我们哪里产生了内存泄漏和导致谁泄漏了而不能被回收。
  • jmap 命令行输入:jmap -histo <pid> | head -20,就可以查看某个pid的java服务占用内存排名前20的类。注:该命令不适用于Windows系统
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。