Java 开发,内存泄漏不会排查,这下溴大了

110 篇文章 2 订阅
55 篇文章 0 订阅

 

什么是内存泄漏

内存泄漏:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。

在 Java 中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。

在 C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于 C++中没有 GC(Garbage Collection 垃圾回收),这些内存将永远收不回来。在 Java 中,这些不可达的对象都由 GC 负责回收,因此程序员不需要考虑这部分的内存泄露。

通过分析,我们得知,对于 C++,程序员需要自己管理边和顶点,而对于 Java 程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java 提高了编程的效率。

因此,通过以上分析,我们知道在 Java 中也有内存泄漏,但范围比 C++要小一些。因为 Java 从语言上保证,任何对象都是可达的,所有的不可达对象都由 GC 管理。

对于程序员来说,GC 基本是透明的,不可见的。虽然,我们只有几个函数可以访问 GC,例如运行 GC 的函数 System.gc(),但是根据 Java 语言规范定义, 该函数不保证 JVM 的垃圾收集器一定会执行。因为,不同的 JVM 实现者可能使用不同的算法管理 GC。通常,GC 的线程的优先级别较低。JVM 调用 GC 的策略也有很多种,有的是内存使用到达一定程度时,GC 才开始工作,也有定时执行的,有的是平缓执行 GC,有的是中断式执行 GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC 的执行影响应用程序的性能,例如对于基于 Web 的实时系统,如网络游戏等,用户不希望 GC 突然中断应用程序执行而进行垃圾回收,那么我们需要调整 GC 的参数,让 GC 能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun 提供的 HotSpot JVM 就支持这一特性。

下面给出一个 Java 内存泄漏的典型例子:

Vector v = new Vector(10);
for (int i = 0; i < 100; i++) {
   Object o = new Object();
   v.add(o);
   o = null;
}

复制代码

在这个例子中,我们循环申请 Object 对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到 Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

v = null

复制代码

要想理解这个定义,我们需要先了解一下对象在内存中的状态。下面的这张图就解释了什么是无用对象以及什么是未被引用对象

上面图中可以看出,里面有被引用对象和未被引用对象。未被引用对象会被垃圾回收器回收,而被引用的对象却不会。未被引用的对象当然是不再被使用的对象,因为没有对象再引用它。然而无用对象却不全是未被引用对象。其中还有被引用的。就是这种情况导致了内存泄漏。

详细解读

1.Java 回收机制

不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java 中对象是采用 new 或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由 Java 虚拟机通过垃圾回收机制完成的。GC 为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java 会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题。在 Java 语言中,判断一个内存空间是否符合垃圾收集的标准有两个:一个是给对象赋予了空值 null,以下再没有调用过另一个是给对象赋予了新值,这样重新分配了内存空间。

2.Java 内存泄漏引起的原因

Java 内存泄漏的根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是 Java 中内存泄漏的发生场景。

来先看看下面的例子,为什么会发生内存泄漏。下面这个例子中,A 对象引用 B 对象,A 对象的生命周期(t1-t4)比 B 对象的生命周期(t2-t3)长的多。当 B 对象没有被应用程序使用之后,A 对象仍然在引用着 B 对象。这样,垃圾回收器就没办法将 B 对象从内存中移除,从而导致内存问题,因为如果 A 引用更多这样的对象,那将有更多的未被引用对象存在,并消耗内存空间。

B 对象也可能会持有许多其他的对象,那这些对象同样也不会被垃圾回收器回收。所有这些没在使用的对象将持续的消耗之前分配的内存空间。

具体主要有如下几大类:

2.1 静态集合类引起内存泄漏

像 HashMap、Vector 等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象 Object 也不能被释放,因为他们也将一直被 Vector 等引用着。

例如:

Static Vector v = new Vector(10);
for (int i = 0; i < 100; i++) {
   Object o = new Object();
   v.add(o);
   o = null;
}

复制代码

在这个例子中,循环申请 Object 对象,并将所申请的对象放入一个 Vector 中,如果仅仅释放引用本身(o=null),那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到 Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

2.2 监听器

在 java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如 addXXXListener() 等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

2.3 各种连接

比如数据库连接(dataSourse.getConnection()),网络连接(socket)和 io 连接,除非其显式的调用了其 close() 方法将其连接关闭,否则是不会自动被 GC 回收的。对于 Resultset 和 Statement 对象可以不进行显式回收,但 Connection 一定要显式回收,因为 Connection 在任何时候都无法自动回收,而 Connection 一旦回收,Resultset 和 Statement 对象就会立即为 NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭 Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的 Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在 try 里面去的连接,在 finally 里面释放连接。

2.4 内部类和外部模块的引用

内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员 A 负责 A 模块,调用了 B 模块的一个方法如:

public void registerMsg(Object b);

复制代码

这种调用就要非常小心了,传入了一个对象,很可能模块 B 就保持了对该对象的引用,这时候就需要注意模块 B 是否提供相应的操作去除引用。

2.5 单例模式

不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被 JVM 正常回收,导致内存泄漏,考虑下面的例子:

ublic class A {
   public A() {
       B.getInstance().setA(this);
   }
   ...
}

//B类采用单例模式
class B{
   private A a;
   private static B instance = new B();
   
   public B(){}
   
   public static B getInstance() {
       return instance;
   }
   
   public void setA(A a) {
       this.a = a;
   }

   public A getA() {
       return a;
   }
}

复制代码

Java 内存泄漏策略

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆区 :又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

3.1 栈与堆的区别

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

举个例子:

public class Sample {
   int s1 = 0;
   Sample mSample1 = new Sample();
   
   public void method() {
       int s2 = 1;
       Sample mSample2 = new Sample();
   }
}
Sample mSample3 = new Sample();

复制代码

Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。

mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储于堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被 new 出来使用的。

了解了 Java 的内存分配之后,我们再来看看 Java 是怎么管理内存的。

3.2 Java 如何管理内存

Java 的内存管理就是对象的分配和释放问题。在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了 JVM 的工作。这也是 Java 程序运行速度较慢的原因之一。因为 GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC 将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。

以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示 JVM 的内存分配情况。以下右图,就是左边程序运行到第 6 行的示意图。

blic class Test {

   public static void main(String[] args) {
       // TODO Auto-generated method stub
       Object o1 = new Object();
       Object o2 = new Object();
       o2 = o1;//此行为第6行
   }
}

复制代码

 


小伙伴们有兴趣想了解内容和更多相关学习资料的请点赞收藏+评论转发+关注我,后面会有很多干货。

 

 

原文出处:xie.infoq.cn/article/88015fb1bfa936a54b8a0f54c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值