写点什么

Java 中关于内存泄漏分析和解决方案,都在这里了

  • 2022 年 4 月 29 日
  • 本文字数:3346 字

    阅读完需:约 11 分钟

}


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


2.2.2 监听器


=========


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


2.2.3 各种连接


==========


比如数据库连接(dataSourse.getConnection()),网络连接(socket)和 io 连接,除非其显式的调用了其 close() 方法将其连接关闭,否则是不会自动被 GC 回收的。对于 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 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区


静态存储区( 《一线大厂 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 行的示意图。


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。另外建议几点:


  • 在确认一个对象无用后,将其所有引用显式的置为 null;

  • 当类从 Jpanel 或 Jdialog 或其它容器类继承的时候,删除该对象之前不妨调用它的 removeall() 方法;

  • 在设一个引用变量为 null 值之前,应注意该引用变量指向的对象是否被监听,若有,要首先除去监听器,然后才可以赋空值;

  • 当对象是一个 Thread 的时候,删除该对象之前不妨调用它的 interrupt() 方法;

  • 内存检测过程中不仅要关注自己编写的类对象,同时也要关注一些基本类型的对象,例如:int[]、String、char[] 等等;

  • 如果有数据库连接,使用 try…finally 结构,在 finally 中关闭 Statement 对象和连接。


4.2 好的测试工具


==========


在开发中不能完全避免内存泄漏,关键要在发现有内存泄漏的时候能用好的测试工具迅速定位问题的所在。市场上已有几种专业检查 Java 内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测 Java 程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
Java中关于内存泄漏分析和解决方案,都在这里了_Java_爱好编程进阶_InfoQ写作社区