性能优化之内存泄露(Memory Leak)分析

来源:互联网 发布:卡盟系统官网源码 编辑:程序博客网 时间:2024/05/22 02:01

1 了解内存分配策略

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

1.1 静态存储区(方法区)

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

1.2 栈区

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

1.3 堆区

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

备注:在C/C++这里是可以自己掌控内存的,需要有很高的素养来解决内存的问题。java在这一块貌似程序员没有很好的方法自己去解决垃圾内存,需要的是编程的时候就要注意自己良好的编程习惯。

2 栈与堆的区别

2.1 小区别

(1)栈式一块连续的内存区域,大小是有操作系统觉决定的。堆是不连续的内存区域,堆空间比较灵活也特别大。
(2)对于栈的话,他先进后出,进出完全不会产生碎片,运行效率高且稳定。
堆管理很麻烦,频繁地new/remove会造成大量的内存碎片,这样就会慢慢导致效率低下。
(3)每一个线程都有一个栈,但是每一个应用程序通常都只有一个堆(尽管为不同类型分配内存使用多个堆的情况也是有的)。
(4)栈附属于线程,因此当线程结束时栈被回收。堆通常通过运行时在应用程序启动时被分配,当应用程序(进程)退出时被回收。
(5)小结:
堆:自己买菜,摘菜,洗菜,做菜,洗完,打扫卫生.比较繁琐,但我想吃什么就吃什么。自由!
栈:去饭店吃饭。吃完付钱,走人。比较省事,但是如果厨师把菜做咸了,你也得吃!

2.2 举例

public class Main{    int a = 1;//堆中    Student s = new Student();//堆中    堆中    public void XXX(){        int b = 1;//栈中        Student s2 = new Student();//栈中    堆中    }}

这里写图片描述

2.3 结论

(1)全局成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)。—-—因为他们都属于类,类对象最终还是要被new出来的。
(2)局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。—因为他们属于方法当中的变量,生命周期会随着方法一起结束。

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

3 Java是如何管理内存

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

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

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。 以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。

这里写图片描述

Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。

4 什么是内存泄露

4.1 基本概念

内存泄漏:内存不在GC掌控之内了。当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。

4.2 Java内存泄漏的根本原因是什么呢?

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

4.3 图例解析

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

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

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

这里写图片描述

5 内存泄露与内存溢出的关系

5.1 内存溢出(OOM)

当应用占用的heap资源超过了Dalvik虚拟机分配的内存就会内存溢出。比如:加载大图片。

5.2 内存泄露(Memory Leak) 会导致内存溢出(OOM)

进程中某些对象已经没有使用价值了,但是他们却还可以直接或者间接地被引用到GC Root导致无法回收。当内存泄露过多的时候,再加上应用本身占用的内存,日积月累最终就会导致内存溢出(OOM)。

6 使用场景

6.1 我们所讨论内存泄露,主要讨论“堆内存”,他存放的就是:引用指向的对象实体。

有时候确实会有一种情况:当需要的时候可以访问,当不需要的时候可以被回收也可以被暂时保存以备重复使用。

eg:ListView或者GridView、RecyclerView加载大量数据或者图片的时候,图片非常占用内存,一定要管理好内存,不然很容易内存溢出。
滑出去的图片就回收,节省内存。看ListView的源码—-回收对象,还会重用ConvertView。如果用户反复滑动或者下面还有同样的图片,就会造成多次重复IO(很耗时),那么需要缓存—平衡好内存大小和IO,算法和一些特殊的java类。

算法:lrucache(最近最少使用先回收)特殊的java类利于回收,StrongReference,SoftReference,WeakReference,PhatomReference

这里写图片描述

软引用比LRU算法更加任性,回收量是比较大的,你无法控制回收哪些对象。比如使用场景:默认头像、默认图标、ListView或者GridView、REcyclerView要使用内存缓存+外部缓存(SD卡)

综述:开发时,为了防止内存溢出,处理一些比较占用内存大并且生命周期长的对象的时候,可以尽量使用软引用(SoftReference)和弱引用(WeakReference)。

7 参考链接

Android 内存泄漏总结

0 0