当前位置: 首页 > 科技观察

Java内存泄漏的分析和解决方法都在这里!

时间:2023-03-12 11:00:45 科技观察

1。什么是内存泄漏?内存泄漏:对象不再被应用程序使用,但垃圾收集器无法删除它们,因为它们仍在被引用。在Java中,内存泄漏是指存在一些已分配的对象。这些对象具有以下两个特征。首先,这些对象是可达的,即在有向图中,存在可以连接到它们的路径;其次,这些对象是无用的,也就是程序以后不会用到这些对象。如果对象满足这两个条件,那么这些对象在Java中就可以判断为内存泄漏。这些对象不会被GC回收,但是会占用内存。在C++中,内存泄漏的范围更大。有些对象被分配了内存空间,但随后它们就无法访问了。由于C++中没有GC(GarbageCollection垃圾回收),这些内存永远不会被回收。在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以一种温和的方式释放内存,比如将垃圾收集分解成一系列的小步骤,Sun提供的HotSpotJVM支持这个特性。下面是一个Java内存泄漏的典型例子,Vectorv=newVector(10);for(inti=0;i<100;i++){Objecto=newObject();v.add(o);o=null;}在本例中,我们循环申请Object对象,并将申请到的对象放入一个Vector中。如果我们只释放引用本身,那么Vector仍然引用这个对象,所以这个对象是不可回收的GC。因此,如果一个对象在添加到Vector后必须从Vector中删除,最简单的方法是将Vector对象设置为null。v=null要理解这个定义,首先要了解对象在内存中的状态。下图解释了什么是无用对象,什么是未引用对象。内存泄漏示意图从上图可以看出,有引用对象和未引用对象之分。未引用的对象会被垃圾回收,但引用的对象不会。未引用的对象当然是不再使用的对象,因为不再有对象引用它。然而,无用对象并不都是未被引用的对象。这也被引用。这就是导致内存泄漏的情况。二、Java内存泄漏详解2.1Java内存回收机制无论任何语言的内存分配方式如何,都需要返回分配内存的真实地址,即返回指向内存块首地址的指针。Java中的对象是使用新方法或反射方法创建的。这些对象的创建是在堆(Heap)中分配的,所有的对象回收都是由Java虚拟机通过垃圾回收机制完成的。为了能够正确释放对象,GC会监控每个对象的运行状态,监控它们的申请、引用、引用、赋值等,Java会使用有向图的方法来管理内存,实时监控对象是否可以到达,如果不可达,则进行回收,这样也可以消除引用循环的问题。在Java语言中,判断一块内存空间是否满足垃圾回收的标准有两个:一是给对象赋null值,二是给对象赋新值,从而使内存空间重新分配.2.2Java内存泄漏的原因内存泄漏是指无用对象(不再使用的对象)继续占用内存或者无用对象的内存不能及时释放,造成内存空间的浪费称为内存泄漏。有的时候内存泄漏并不严重,不易察觉,以至于开发人员不知道有内存泄漏,但有的时候严重了,会提示你Outofmemory。Java内存泄漏的根本原因是什么?当长寿命对象持有对短寿命对象的引用时,很可能会发生内存泄漏。虽然不再需要短寿命对象,但它不能被回收,因为长寿命对象持有它的引用。这是发生内存泄漏的Java场景。我们先来看下面这个例子,为什么会出现内存泄漏。在下面的例子中,对象A引用对象B,对象A的生命周期(t1-t4)远大于对象B的生命周期(t2-t3)。当应用程序不使用B对象时,A对象仍然引用B对象。这样,垃圾收集器就没有办法将B对象从内存中移除,造成内存问题,因为如果A引用了更多这样的对象,就会有更多未引用的对象,消耗内存空间。B对象还可能持有许多其他对象,这些对象也不会被垃圾收集器回收。所有这些未使用的对象将继续消耗以前分配的内存空间。生命周期图主要有以下几类:2.2.1静态集合类导致内存泄漏。HashMap、Vector等的使用最容易出现内存泄漏。这些静态变量的生命周期与应用程序一致。Object对象不能被释放,因为他们会一直被Vector等引用。例如:StaticVectorv=newVector(10);for(inti=0;i<100;i++){Objecto=newObject();v.add(o);o=null;}在本例中,申请Object对象在一个循环中,并将请求的对象放到一个Vector中,如果只是释放引用本身(o=null),那么这个Vector仍然引用这个对象,所以这个对象对于GC是不可回收的。因此,如果一个对象在添加到Vector后必须从Vector中移除,最简单的方法是将Vector对象设置为null。2.2.2Listener在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的一个方法如:publicvoidregisterMsg(Objectb);也许模块B保留对此对象的引用。这时候需要注意模块B是否提供了相应的移除引用的操作。2.2.5单例模式使用不当是导致内存泄露的常见问题。初始化后,单例对象将存在于JVM的整个生命周期中(以静态变量的形式存在)。如果单例对象持有外部引用,那么这个对象将不会被JVM正常回收,导致内存泄漏,考虑下面的例子:publicclassA{publicA(){B.getInstance().setA(this);}...}//B类使用单例模式3.Java内存分配策略Java程序运行时存在三种内存分配策略,即静态分配、栈分配和堆分配。对应的,三种存储策略使用的内存空间主要是静态存储区(也叫方法区)、栈和堆。静态存储区(方法区):主要存放静态数据、全局静态数据和常量。这块内存是在程序编译时分配的,并在整个程序运行时都存在。栈区:方法执行时,方法体中的局部变量(包括基本数据类型和对象引用)被创建在栈上,这些局部变量所占用的内存会在方法执行结束时自动释放。由于堆栈内存分配操作内置于处理器的指令集中,效率很高,但分配的内存容量有限。堆区:又称动态内存分配,通常是指程序运行时直接new的内存,即对象的实例。这部分内存在不用的时候会被Java垃圾回收器回收。3.1栈和堆的区别方法体中定义的一些基本类型的变量和对象引用变量(局部变量)是在方法的栈内存中分配的。当在方法块中定义变量时,Java会在栈上为该变量分配内存空间。当超出变量的作用域时,变量就会失效,分配给它的内存空间也会被释放。dropped,内存空间可以重复使用。堆内存用于存放new创建的所有对象(包括对象中的所有成员变量)和数组。堆上分配的内存将由Java垃圾收集器自动管理。在堆中生成一个数组或对象后,还可以在栈中定义一个特殊的变量。该变量的值等于数组或对象在堆内存中的首地址。这个特殊变量就是我们上面提到的引用变量。.我们可以通过这个引用变量来访问堆中的对象或者数组。举个栗子:publicclassSample{ints1=0;SamplemSample1=newSample();publicvoidmethod(){ints2=1;SamplemSample2=newSample();}}SampleSample3=newSample();Sample类的局部变量s2和引用变量mSample2都存在于栈中,但mSample2指向的对象存在于堆中。mSample3指向的对象实体存放在堆上,包括该对象的所有成员变量s1和mSample1,它本身存在于栈中。结论:局部变量的基本数据类型和引用存放在栈中,引用的对象实体存放在堆中。——因为它们在方法中属于变量,所以生命周期以方法结束。成员变量全部存储在堆中(包括基本数据类型、引用和被引用的对象实体)——因为属于一个类,类对象毕竟要new才能使用。了解了Java的内存分配之后,我们再来看看Java是如何管理内存的。3.2Java如何管理内存Java的内存管理就是对象的分配和释放。在Java中,程序员需要通过关键字new为每个对象(基本类型除外)申请内存空间,所有对象都在**堆(Heap)**中分配空间。另外,对象释放是由GC决定并执行的。在Java中,内存的分配是由程序完成的,内存的释放是由GC完成的。这种收支两行的方法确实简化了程序员的工作。但同时也增加了JVM的工作量。这就是Java程序运行速度较慢的原因之一。因为GC为了正确释放对象,GC必须监控每个对象的运行状态,包括对象的申请、引用、引用、赋值等,GC需要监控。监控对象状态的目的是为了更准确及时地释放对象,而释放对象的根本原则是对象不再被引用。为了更好的理解GC的工作原理,我们可以把对象看成有向图的顶点,把引用关系看成图的有向边,有向边从referrer指向被引用对象。此外,每个线程对象都可以用作图的起始顶点。比如大部分程序都是从主进程开始执行的,那么这个图就是从主进程顶点开始的根树。在这个有向图中,根顶点可达的对象都是有效对象,GC不会回收这些对象。如果一个对象(连通子图)从根顶点不可达(注意该图是有向图),那么我们认为(这些)对象不再被引用,可以被GC回收。下面,我们举例说明如何使用有向图来表示内存管理。对于程序的每个时刻,我们都有一个表示JVM内存分配的有向图。下右图是左边程序运行到第6行的示意图publicclassTest{publicstaticvoidmain(String[]args){//TODOAuto-generatedmethodstubObjecto1=newObject();Objecto2=newObject();o2=o1;//此行为的第6行}}定向图4。如何防止内存泄漏?在了解了内存泄漏的一些原因后,应该尽可能地避免和发现内存泄漏。4.1良好编码习惯最基本的建议是尽快释放对无用对象的引用。大多数程序员在使用临时变量时,都是让引用变量在退出活动域后自动设置为null。使用这种方法时,必须特别注意一些复杂的对象图,如数组、列、树、图等,这些对象之间的相互引用关系比较复杂。对于这样的对象,GC回收它们通常是低效的。如果程序允许,尽早将不用的引用对象赋值给null。多提几点建议:确认一个对象无用后,显式将其所有引用设置为null;当类继承自Jpanel或Jdialog或其他容器类时,您可能希望在删除对象之前调用其removeall()方法;在将引用变量设置为空值之前,需要注意引用变量指向的对象是否被监听。如果是,则必须先移除监听器,然后才可以赋空值;当对象是Thread时,删除对象前不妨调用其interrupt()方法;在内存检测的过程中,不仅要关注自己写的类对象,还要关注一些基本类型的对象,比如:int[]、String、char[]等;如果有数据库连接,使用try...finally结构在finally中关闭Statement对象和连接。4.2好的测试工具并不能完全避免开发过程中的内存泄漏。关键是使用好的测试工具,在发现内存泄漏时能够快速定位问题。市场上有几种专业的Java内存泄漏检查工具。它们的基本工作原理相似。它们都监视Java程序运行时所有对象的申请和释放,收集内存管理的所有信息。可视化。开发人员将使用这些信息来确定程序是否存在内存泄漏。这些工具包括OptimizeitProfiler、JProbeProfiler、JinSight、Rational的Purify等等。4.3注意HashMap、ArrayList等集合对象特别注意HashMap、ArrayList等一些集合对象,它们经常会造成内存泄漏。当它们被声明为静态时,它们与应用程序一样存在。4.4注意事件监听和回调函数特别注意事件监听和回调函数。在使用监听器时注册,但在不再使用时不注销。“如果一个类自己管理内存,那么开发人员就必须小心内存泄漏。”通常一些成员变量引用了其他对象,初始化时需要清空。另外,关注Java知音公众号,回复“后端面试”,送你面试题集!