当前位置: 主页 > JAVA语言

java内存泄漏-java内存泄漏检测工具

发布时间:2023-02-08 22:21   浏览次数:次   作者:佚名

最近在熟悉Java内存泄漏的相关知识。 我在网上查了一些资料。 这里总结一下收获。 希望对您有所帮助。 有什么问题可以在文末留言讨论补充。

以下为全文结构,所需阅读时间约20分钟

java内存泄漏_java内存泄漏_java内存泄漏检测工具

1.什么是内存泄漏?

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

在 Java 中,内存泄漏是指存在一些已分配的对象。 这些对象具有以下两个特征。 首先,这些对象是可达的,即在有向图中,存在可以连接到它们的路径; 其次,这些对象是无用的。 ,也就是程序以后不会用到这些对象。 如果对象满足这两个条件,那么这些对象在Java中就可以判断为内存泄漏。 这些对象不会被GC回收,但是会占用内存。

在C++中,内存泄漏的范围更大。 有些对象被分配了内存空间,但随后它们就无法访问了。 由于C++中没有GC(Garbage Collection垃圾回收),这些内存永远不会被回收。 在Java中,这些不可达对象被GC回收,所以程序员不需要考虑这部分内存泄漏。

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

java内存泄漏_java内存泄漏_java内存泄漏检测工具

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

对于程序员来说,GC基本上是透明不可见的。 虽然我们只有少数几个函数可以访问GC,比如运行GC的函数System.gc(),但是根据Java语言规范的定义,这个函数并不能保证JVM的垃圾回收器将被执行。 因为,不同的 JVM 实现者可能使用不同的算法来管理 GC。

通常,GC 线程具有较低的优先级。 JVM调用GC的策略也有很多。 当内存使用达到一定水平时,其中一些开始工作。 但一般来说,我们不需要关心这些。 除非在某些特定的场合,GC的执行会影响应用程序的性能。 例如,对于基于网络的实时系统,例如网络游戏,用户不希望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对象java内存泄漏,并将申请到的对象放入一个Vector中。 如果我们只释放引用本身,那么Vector仍然引用这个对象,所以这个对象是不可回收的GC。 因此,如果一个对象在添加到 Vector 后必须从 Vector 中移除,最简单的方法是将 Vector 对象设置为 null。

v = null

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

java内存泄漏_java内存泄漏检测工具_java内存泄漏

java内存泄漏_java内存泄漏检测工具_java内存泄漏

内存泄漏示意图

从上图可以看出,有引用对象和未引用对象之分。 未引用的对象会被垃圾回收,但引用的对象不会。 未引用的对象当然是不再使用的对象,因为不再有对象引用它。 然而,无用对象并不都是未被引用的对象。 这也被引用。 这就是导致内存泄漏的情况。

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 对象还可能持有许多其他对象,这些对象也不会被垃圾收集器回收。 所有这些未使用的对象将继续消耗以前分配的内存空间。

java内存泄漏_java内存泄漏_java内存泄漏检测工具

生命周期图

主要有以下几类:

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

java内存泄漏检测工具_java内存泄漏_java内存泄漏

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

2.2.2 听众

在java编程中,我们都需要和监听器打交道。 通常一个应用程序中会使用很多监听器。 我们会调用控件的addXXXListener()等方法来添加监听器,但是往往在释放对象时,没有及时删除这些监听器,增加了内存泄漏的几率。

2.2.3 各种连接

比如数据库连接(dataSource.getConnection())、网络连接(socket)和io连接,除非显式调用它们的close()方法关闭它们的连接,否则它们不会被GC自动回收。 Resultset和Statement对象不能显式回收,但是Connection必须显式回收,因为Connection不能随时自动回收,一旦Connection被回收,Resultset和Statement对象会立即为NULL。

但是如果使用连接池,情况就不同了。 除了显式关闭连接外,还必须显式关闭ResultsetStatement对象(关闭其中一个,另一个也会关闭),否则会造成大量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;

java内存泄漏_java内存泄漏检测工具_java内存泄漏

    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会在栈上为该变量分配内存空间。 当超出变量的作用域时,变量就会失效,分配给它的内存空间也会被释放。 dropped,内存空间可以重复使用。

java内存泄漏_java内存泄漏检测工具_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)**中分配空间。 另外java内存泄漏,对象的释放是由GC决定并执行的。

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

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

为了更好的理解GC的工作原理,我们可以把对象看成有向图的顶点,把引用关系看成图的有向边,有向边从referrer指向被引用对象。 此外,每个线程对象都可以用作图的起始顶点。 例如,大多数程序都是从主进程执行的,因此该图是从主进程顶点开始的有根树。

java内存泄漏_java内存泄漏检测工具_java内存泄漏

在这个有向图中,根顶点可达的对象都是有效对象,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行
    }
}

java内存泄漏检测工具_java内存泄漏_java内存泄漏

有向图

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 注意事件监听和回调函数

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

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