转载

内存泄漏预防与治理

内存泄漏是指不再被使用的对象的内存不能被GC回收,同时频繁的GC会造成卡顿。Android系统为每个应用程序分配的内存是有限的,如果应用中内存泄漏较多,就很容易造成OOM

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

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

• 栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

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

内存泄漏预防

static修饰成员变量时,那么该变量就属于该类,而不是该类的实例。如果你这样做,那意味此成员变量的生命周期,会被拉长到与整个app进程生命周期一致。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的对象,就容易出现内存泄露的情况

使用static静态变量,应注意:

第一,应该尽量避免static成员变量引用资源耗费过多对象。

第二,在static变量引用对象的时候,在被引用对象不再使用的时候,应及时释放引用,做置null操作 。

单例模式只允许应用程序存在一个实例对象,并且这个实例对象的生命周期和应用程序的生命周期一样长,如果单例对象中拥有另一个对象的引用的话,这个被引用的对象就不能被及时回收。(使用弱引用)

如果传入的context是Activity将会造成内存泄漏,如果是Application就不至于内存泄漏,因为Application的生命周期长。

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}
复制代码

长生命周期造成的内存泄漏,如Application,因为它的生命周期和整个应用的生命周期一样长:

public class MyApplication extends Application {


private Activity currenActivity;

public void setCurrenActivity(Activity currenActivity){
    this.currenActivity = currenActivity;
}


}
复制代码
public class TestActivity4 extends Activity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyApplication application = (MyApplication) getApplication();
        application.setCurrenActivity(this);
    }
}
复制代码
public class TestActivity2 extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.i("TestActivity2","启动了");
            }
        },1000000);
    
    }
}
复制代码

如果启动Activity打开后,然后立即关闭了,这种情况下就会发生内存泄漏。我们知道,Handler、Message、MessageQueue是相互关联在一起的,Handler通过发送消息Message与主线程进行交互,如果Handler发送的消息Message尚未被处理,该Message及发送它的Handler对象将被MessageQueue一直持有,这样就可能会导致Message无法被回收。本例中Runable为被内存泄漏的消息,又因为匿名内部类会持有外部类的引用,所有造成Activity的泄漏。不过本例中因为只会延迟一秒执行消息,所以这种内存泄漏的危害不是很大。对于Handler的使用,可以以如下方式:

public class TestActivity2 extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        new NewHandler(this).sendEmptyMessageDelayed(0,1000000);
    
    }

    private static class NewHandler extends Handler{
    
        private WeakReference<Activity> weakActivity;
    
        NewHandler(Activity activity){
            weakActivity = new WeakReference<>(activity);
        }
    
        @Override
        public void handleMessage(Message msg) {
            Activity activity = weakActivity.get();
            if(activity!=null){
                Log.i("TestActivity2",activity.getClass().getSimpleName()+"启动");
            }
        }
    }
}

复制代码

以静态类的方式定义Handler,这样就不会直接持有Activity的引用,而Activity由弱引用的方式持有,当一个对象仅仅只有弱引用,那它和没有引用是一样的,当GC启动时,它将会立即被回收。

所以:Handler类需要声明为static,否则会发生泄漏。 原因是Message进入消息队列时,会持有对目标Handler的引用,如果Handler是内部类,内部类还会持有对外部类的引用, 为了避免对外部类的泄漏,Handler应该声明为静态嵌套类,持有对外部类的弱引用。

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

5.非静态内部类的静态实例容易造成内存泄漏,如静态的view或者单例模式

blog.csdn.net/linyukun642…

非静态内部类中创建了一个静态实例,导致该实例的生命周期和应用ClassLoader级别,又因为该静态实例又会隐式持有其外部类的引用,所以导致其外部类无法正常释放,出现了泄漏问题。

6.对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等

7.创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等

8.尽可能的复用资源;譬如系统本身有很多字符串、颜色、图片、动画、样式以及简单布局等资源可供我们直接使用,我们自己也要尽量复用style等资源达到节约内存

9.对于有缓存等存在的应用尽量实现onLowMemory()和onTrimMemory()方法

10.不要加载过大的Bitmap对象;譬如对于类似图片加载我们要通过BitmapFactory.Options设置图片的一些采样比率和复用等

11.对批量加载等操作进行缓存设计,譬如列表图片显示,Adapter的convertView缓存等

内存泄漏治理

当您首次打开 Memory Profiler 时,您将看到一条表示应用内存使用量的详细时间线,并可访问用于强制执行垃圾回收、捕捉堆转储和记录内存分配的各种工具。

内存泄漏预防与治理
图 1. Memory Profiler

如图1 所示,Memory Profiler 的默认视图包括以下各项:

1.用于强制执行垃圾回收 Event 的按钮。

2.用于捕获堆转储的按钮。

3.用于记录内存分配情况的按钮。 此按钮仅在连接至运行 Android 7.1 或更低版本的设备时才会显示。

4.用于放大/缩小时间线的按钮。

5.用于跳转至实时内存数据的按钮。

6.Event 时间线,其显示 Activity 状态、用户输入 Event 和屏幕旋转 Event。

7.内存使用量时间线,其包含以下内容:

◦ 一个显示每个内存类别使用多少内存的堆叠图表,如左侧的 y 轴以及顶部的彩色键所示。

◦ 虚线表示分配的对象数,如右侧的 y 轴所示。

◦ 用于表示每个垃圾回收 Event 的图标。

不过,如果您使用的是运行 Android 7.1 或更低版本的设备,则默认情况下,并不是所有分析数据均可见。 如果您看到一条消息,其显示“Advanced profiling is unavailable for the selected process”,则需要启用高级分析以查看下列内容:

• Event 时间线

• 分配的对象数

• 垃圾回收 Event

在 Android 8.0 及更高版本上,始终为可调试应用启用高级分析。

您在 Memory Profiler(图 2)顶部看到的数字取决于您的应用根据 Android 系统机制所提交的所有私有内存页面数。 此计数不包含与系统或其他应用共享的页面。

内存泄漏预防与治理
图 2. Memory Profiler 顶部的内存计数图例

内存计数中的类别如下所示:

• Java:从 Java 或 Kotlin 代码分配的对象内存。

• Native:从 C 或 C++ 代码分配的对象内存。 即使您的应用中不使用 C++,您也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表您处理各种任务,如处理图像资源和其他图形时,即使您编写的代码采用 Java 或 Kotlin 语言。

• Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。 (请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)

• Stack: 您的应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与您的应用运行多少线程有关。

• Code:您的应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存。

• Other:您的应用使用的系统不确定如何分类的内存。

• Allocated:您的应用分配的 Java/Kotlin 对象数。 它没有计入 C 或 C++ 中分配的对象。

当连接至运行 Android 7.1 及更低版本的设备时,此分配仅在 Memory Profiler 连接至您运行的应用时才开始计数。 因此,您开始分析之前分配的任何对象都不会被计入。 不过,Android 8.0 附带一个设备内置分析工具,该工具可记录所有分配,因此,在 Android 8.0 及更高版本上,此数字始终表示您的应用中待处理的 Java 对象总数。 与以前的 Android Monitor 工具中的内存计数相比,新的 Memory Profiler 以不同的方式记录您的内存,因此,您的内存使用量现在看上去可能会更高些。 Memory Profiler 监控的类别更多,这会增加总的内存使用量,但如果您仅关心 Java 堆内存,则“Java”项的数字应与以前工具中的数值相似。 然而,Java 数字可能与您在 Android Monitor 中看到的数字并非完全相同,这是因为应用的 Java 堆是从 Zygote 启动的,而新数字则计入了为它分配的所有物理内存页面。 因此,它可以准确反映您的应用实际使用了多少物理内存。 注:目前,Memory Profiler 还会显示应用中的一些误报的原生内存使用量,而这些内存实际上是分析工具使用的。 对于大约 100000 个对象,最多会使报告的内存使用量增加 10MB。 在这些工具的未来版本中,这些数字将从您的数据中过滤掉。

堆转储显示在您捕获堆转储时您的应用中哪些对象正在使用内存。 特别是在长时间的用户会话后,堆转储会显示您认为不应再位于内存中却仍在内存中的对象,从而帮助识别内存泄漏。 在捕获堆转储后,您可以查看以下信息:

• 您的应用已分配哪些类型的对象,以及每个类型分配多少。

• 每个对象正在使用多少内存。

• 在代码中的何处仍在引用每个对象。

• 对象所分配到的调用堆栈。 (目前,如果您在记录分配时捕获堆转储,则只有在 Android 7.1 及更低版本中,堆转储才能使用调用堆栈。)

内存泄漏预防与治理
图 3. 查看堆转储

要捕获堆转储,在 Memory Profiler 工具栏中点击 Dump Java heap

内存泄漏预防与治理

在转储堆期间,Java 内存量可能会暂时增加。 这很正常,因为堆转储与您的应用发生在同一进程中,并需要一些内存来收集数据。 堆转储显示在内存时间线下,显示堆中的所有类类型,如图 4 所示。 注:如果您需要更精确地了解转储的创建时间,可以通过调用 dumpHprofData() 在应用代码的关键点创建堆转储。 要检查您的堆,请按以下步骤操作:

1 浏览列表以查找堆计数异常大且可能存在泄漏的对象。 为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。 然后点击一个类名称。 此时在右侧将出现 Instance View 窗格,显示该类的每个实例。

2 在 Instance View 窗格中,点击一个实例。此时下方将出现 References,显示该对象的每个引用。 或者,点击实例名称旁的箭头以查看其所有字段,然后点击一个字段名称查看其所有引用。 如果您要查看某个字段的实例详情,右键点击该字段并选择 Go to Instance。

3 在 References 标签中,如果您发现某个引用可能在泄漏内存,则右键点击它并选择 Go to Instance。 这将从堆转储中选择对应的实例,显示您自己的实例数据。 默认情况下,堆转储不会向您显示每个已分配对象的堆叠追踪。 要获取堆叠追踪,在点击 Dump Java heap 之前,您必须先开始记录内存分配。 然后,您可以在 Instance View 中选择一个实例,并查看 Call Stack 标签以及 References 标签。不过,在您开始记录分配之前,可能已分配一些对象,因此,调用堆栈不能用于这些对象。 包含调用堆栈的实例在图标

内存泄漏预防与治理

上用一个“堆栈”标志表示。 在您的堆转储中,请注意由下列任意情况引起的内存泄漏:

• 长时间引用 Activity、Context、View、Drawable 和其他对象,可能会保持对 Activity 或 Context 容器的引用。

• 可以保持 Activity 实例的非静态内部类,如 Runnable。

• 对象保持时间超出所需时间的缓存。

内存泄漏预防与治理
图 4. 捕获堆转储需要的持续时间标示在时间线中

在类列表中,您可以查看以下信息:

• Heap Count:堆中的实例数。

• Shallow Size:此堆中所有实例的总大小(以字节为单位)。

• Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)。

在类列表顶部,您可以使用左侧下拉列表在以下堆转储之间进行切换:

• Default heap:系统未指定堆时。

• App heap:您的应用在其中分配内存的主堆。

• Image heap:系统启动映像,包含启动期间预加载的类。 此处的分配保证绝不会移动或消失。

• Zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的。

默认情况下,此堆中的对象列表按类名称排列。 您可以使用其他下拉列表在以下排列方式之间进行切换:

• Arrange by class:基于类名称对所有分配进行分组。

• Arrange by package:基于软件包名称对所有分配进行分组。

• Arrange by callstack:将所有分配分组到其对应的调用堆栈。 此选项仅在记录分配期间捕获堆转储时才有效。 即使如此,堆中的对象也很可能是在您开始记录之前分配的,因此这些分配会首先显示,且只按类名称列出。 默认情况下,此列表按 Retained Size 列排序。 您可以点击任意列标题以更改列表的排序方式。

在 Instance View 中,每个实例都包含以下信息:

• Depth:从任意 GC 根到所选实例的最短 hop 数。

• Shallow Size:此实例的大小。

• Retained Size:此实例支配的内存大小(根据 dominator 树)。

原文  https://juejin.im/post/5cea89b05188250fa51ab4c2
正文到此结束
Loading...