深入理解Java内存泄漏:原因、检测与预防策略详解

最近在熟悉Java内存泄漏的相关知识。我在网上查了一些资料。这是我所学到的总结。希望对您有所帮助。如果有任何疑问,可以在文末留言,共同讨论补充。

深入理解Java内存泄漏:原因、检测与预防策略详解

以下是整篇文章的结构,读完大约需要20分钟。

1.什么是内存泄漏?

内存泄漏:对象不再被应用程序使用,但垃圾收集器无法删除它们,因为它们仍然被引用。

在Java中,内存泄漏是由于某些已分配对象的存在而引起的。这些对象具有以下两个特征。首先,这些对象是可达的,即有向图中存在可以连接它们的路径;其次,这些东西是没有用的。 ,即程序以后不会再使用这些对象。如果对象满足这两个条件,就可以判定这些对象是Java中的内存泄漏。这些对象不会被GC回收,但它们会占用内存。

在 C++ 中,内存泄漏更为普遍。有些对象被分配了内存空间,然后变得不可访问。由于C++中没有GC(垃圾收集),这些内存永远不会被回收。在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

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

内存泄漏图

上图可以看出,有引用的对象和未引用的对象。未引用的对象将被垃圾收集器收集,但引用的对象不会。当然,未引用的对象是不再使用的对象,因为没有对象再引用它。然而,无用的对象并不都是未引用的对象。其中一些被引用。正是这种情况导致了内存泄漏。

2. Java内存泄漏详解2.1 Java内存回收机制

无论任何语言采用何种内存分配方式,都需要返回所分配内存的真实地址,即返回一个指向内存块首地址的指针。 Java 中的对象是使用新方法或反射方法创建的。这些对象的创建是在堆(Heap)中分配的。所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。

为了正确释放对象,GC会监控每个对象的运行状态,监控它们的申请、引用、引用、赋值等。Java会采用有向图的方法来管理内存,实时监控对象是否可以到达,如果不可达,就会被回收,这样也可以消除引用循环的问题。

在Java语言中,判断一块内存空间是否满足垃圾回收标准有两个标准:一是给对象赋null值,就不再被调用;二是给对象赋空值,不再被调用。另一种是给对象赋一个新值,从而重新分配内存空间。 。

2.2 Java内存泄漏的原因

内存泄漏是指无用对象(不再使用的对象)继续占用内存或者无用对象的内存不能及时释放,造成内存空间的浪费,称为内存泄漏。内存泄漏有时并不严重,不易察觉,因此开发者并不知道存在内存泄漏,但有时严重时会提示你Out of memory。

Java内存泄漏的根本原因是什么?如果长寿命对象持有对短寿命对象的引用,则可能会发生内存泄漏。尽管不再需要短寿命对象,但它无法被回收,因为长寿命对象持有对其的引用。这就是 Java 中发生的情况。内存泄漏的场景。

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

对象B还可能持有许多其他对象,这些对象也不会被垃圾收集器回收。所有这些未使用的对象将继续消耗先前分配的内存空间。

生命周期图

具体来说,主要有以下几类:

2.2.1 静态集合类导致内存泄漏

使用HashMap、Vector等时最容易发生内存泄漏。这些静态变量的生命周期与应用一致,它们引用的所有对象都无法释放,因为它们永远会被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.2 监听器

在java编程中,我们都需要和监听器打交道。通常,应用程序中会使用许多侦听器。我们会调用控件的addXXXListener()等方法来添加监听器,但是释放对象时往往没有这样的事情。请记住删除这些侦听器,从而增加内存泄漏的机会。

2.2.3 各种连接

例如,数据库连接(dataSourse.getConnection())、网络连接(socket)和io连接不会被GC自动回收,除非显式调用它们的close()方法来关闭连接。 Resultset和Statement对象不需要显式回收,但是Connection必须显式回收,因为Connection任何时候都不能自动回收,而且一旦Connection被回收,Resultset和Statement对象将立即为NULL。

但如果使用连接池,情况就不同了。除了显式关闭连接之外,还必须显式关闭 Resultset Statement 对象(如果关闭其中之一,则另一个也会被关闭)。否则,大量的Statement对象将无法被检索。释放,导致内存泄漏。在这种情况下,通常在try中建立连接并在finally中释放。

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

内部类的引用很容易被忘记,一旦不释放,一系列的后继类对象也可能不会被释放。此外,程序员必须小心对外部模块的无意引用。例如,程序员A负责模块A,调用模块B的一个方法,如:

public void registerMsg(Object b);

对于这种电话要非常小心。如果传入一个对象,则模块 B 很可能保留对该对象的引用。此时需要关注模块B是否提供了相应的移除引用的操作。

2.2.5 单例模式

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

public 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;
    }
}

3.Java内存分配策略

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

静态存储区(方法区):主要存储静态数据、全局静态数据和常量。该内存在程序编译时分配,并存在于程序的整个运行过程中。

栈区:方法执行时,会在栈上创建方法体中的局部变量(包括基本数据类型和对象引用),这些局部变量所占用的内存会在方法执行结束时自动释放。 。由于堆栈内存分配操作内置于处理器的指令集中,因此非常高效,但分配的内存容量有限。

堆区:也称为动态内存分配,通常指程序运行时直接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为每个对象(基本类型除外)申请内存空间。所有对象都在**堆**中分配空间。另外,对象的释放是由GC决定并执行的。

在Java中,内存分配是由程序完成的,内存释放是由GC完成的。这种两行方法确实简化了程序员的工作。但同时,也增加了JVM的工作量。这就是Java程序运行速度较慢的原因之一。为了让GC正确释放对象,GC必须监控每个对象的运行状态,包括对象申请、引用、引用、赋值等,GC都需要监控。

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

为了更好地理解GC的工作原理,我们可以将对象视为有向图的顶点,将引用关系视为图的有向边。有向边缘从引用者指向被引用对象。此外,每个线程对象都可以用作图的起始顶点。例如,大多数程序都是从主进程开始执行的,那么该图就是一棵从主进程顶点开始的根树。

在这个有向图中,根顶点可达的对象都是有效对象,GC不会回收这些对象。如果一个对象(连通子图)从根顶点不可达(注意该图是有向图),那么我们就认为该对象(这些)不再被引用,可以被GC回收。

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

public class Test {

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

有向图 4.如何防止内存泄漏的发生?

了解了内存泄漏的一些原因之后,就应该尽量避免和发现内存泄漏。

4.1 良好的编码习惯

最基本的建议是尽快释放对无用对象的引用。大多数程序员在使用临时变量时,都会让引用变量在退出活动域后自动设置为null。

使用这种方法时,要特别注意一些复杂的对象图,如数组、列、树、图等,这些对象之间的相互引用关系比较复杂。对于此类对象,GC 回收它们的效率通常较低。如果您的程序允许,请尽早将未使用的引用对象设置为 null。其他一些建议:

4.2 良好的测试工具

开发过程中无法完全避免内存泄漏。关键是当发现内存泄漏时,使用好的测试工具能够快速定位问题。市场上有几种用于检查Java内存泄漏的专业工具。它们的基本工作原理是相似的。它们都监视Java程序运行时所有对象的申请和释放,收集和分析所有内存管理信息。可视化。

开发人员将利用这些信息来判断程序是否存在内存泄漏问题。这些工具包括Optimizeit Profiler、JProbe Profiler、JinSight、Rational的Purify等。

4.3 注意HashMap、ArrayList等集合对象

特别注意一些集合对象,如HashMap、ArrayList,它们经常会导致内存泄漏。当它们被声明为静态时,它们的生命周期与应用程序一样长。

4.4 注意事件监听和回调函数

特别注意事件监听器和回调函数。侦听器在使用时进行注册,但在不再使用时不会取消注册。

“如果一个类管理自己的内存,那么开发人员必须小心内存泄漏。”通常一些成员变量引用其他对象,需要在初始化时置空。

  

往
期
推
荐

1、千万别在有WiFi的房间里摆这种姿势,只因…

2、程序员工作压力大,身体也垮,为什么还这么多人想做程序员?是因为喜欢吗?

3、微软停止发布旧版Win10更新,网友:正合我意

4、为什么国外JetBrains做 IDE 就可以养活自己,国内不行?区别在哪?

5、Java之父偏爱函数式编程,称自己的代码风格被队友批评

点分享

点收藏

点点赞

点在看

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

30阅读

java中冒号,java双冒号

2026年09月06日

28阅读

java中的->,java中的是什么意思

2026年09月06日

28阅读

java中的super用法,Java中super的说明

2026年09月06日

29阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

27阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

29阅读