Android内存管理

转载请注明出处:https://blog.csdn.net/mythmayor/article/details/123117007

前言

在Android或Java中,我们一般不用担心内存管理,这是因为Java虚拟机(JVM:Java Virtual Machine)存在垃圾回收机制(GC:Garbage Collection),垃圾回收器会对内存进行管理。相比于其它语言(例如C语言),会要求主动释放申请的内存,所以在编程的时候需要考虑内存申请和内存释放的时机。Java GC的存在从一定程度上减少了我们的工作量,但带来的后果就是很多时候我们会滥用内存,比如说申请了一块较大的内存,很少会去关注这块内存何时释放以及如何释放,从而导致应用占用的内存越来越大,甚至到最后产生了内存溢出(OOM:Out Of Memory),进而引发程序崩溃。所以,关注内存变化并了解一些内存优化和监控是有必要的。

一、内存概述

1.JVM内存结构

在这里插入图片描述

上图描述了HelloWorld.java文件被JVM加载到内存中的过程:首先需要经过编译器编译,生成HelloWorld.class 字节码文件,然后通过ClassLoader(类加载器)将HelloWorld.class加载到JVM的内存中。JVM中的内存可以划分为若干个不同的数据区域,主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区。

程序计数器(Program Counter Register)

Java程序是多线程的,CPU可以在多个线程中分配执行时间片段。当某一个线程被CPU挂起时,需要记录代码已经执行到的位置,方便CPU重新执行此线程时,知道从哪行指令开始执行。这就是程序计数器的作用。

程序计数器是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。

虚拟机栈

JVM是基于栈的解释器执行的,DVM是基于寄存器解释器执行的。

这里说到的栈就是虚拟机栈。

虚拟机栈是线程私有的,与线程的生命周期同步。在Java虚拟机规范中,对这个区域规定了两种异常情况:

  • StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。
  • OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出。

虚拟机栈的初衷是用来描述Java方法执行的内存模型,每个方法被执行的时候,JVM都会在虚拟机栈中创建一个栈帧(栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等)。

本地方法栈

本地方法栈和上面介绍的虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及JNI可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。

Java堆(Heap)是JVM所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆里面分配,因此它也是Java垃圾收集器(GC)管理的主要区域,有时候也叫作“GC堆”。同时它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。

方法区

方法区(MethodArea)也是JVM规范里规定的一块运行时数据区。方法区主要是存储已经被JVM加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域同堆一样,也是被各个线程共享的内存区域。

2.Android中内存分配

每一个Android设备都会有不同的RAM总大小与可用空间,因此不同设备为App提供了不同大小的内存限制(这里的内存主要指的是堆内存)。如果你觉得分配的内存不够用,还可以通过在清单文件中开启android:largeHeap="true"来获取更多内存。然而,能够获取更大内存的设计本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用),所以不要轻易地因为需要使用大量内存而去申请largeHeap,只有当你清楚的知道哪里会使用大量的内存并且为什么这些内存必须被保留时再去使用largeHeap。因为使用额外的内存会影响系统整体的用户体验,并且会使得GC的每次运行时间更长。在切换任务时,系统的性能会大打折扣。另外, largeHeap并不一定能够获取到更大的内存。在某些有严格限制的机器上,largeHeap的大小和通常的heapSize是一样的。因此即使你申请了largeHeap,你还是应该通过执行getMemoryClass()来检查实际获取到的内存大小。

获取应用内存大小:

ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
//获取正常情况下内存的大小
int memoryClass = manager.getMemoryClass();
//获取开启largeHeap最大的内存大小
int largeMemoryClass = manager.getLargeMemoryClass();

二、内存优化方案

我们在做内存优化时,无非有两个方向:

  • 内存溢出(OOM)
  • 内存泄漏(Memory Leak)
1.内存溢出(OOM)

通常在排查内存溢出相关的问题时,可以从以下几个方面入手:

  • 加载了大量的Bitmap
  • 创建了过多的对象
  • 加载过大的对象
  • 加载的资源过多,来不及释放
  • 内存泄漏

总之,内存溢出的产生都是由于App申请的内存超过了应用的内存上限,这是JVM就会抛出OutOfMemoryError,从而导致应用程序的崩溃。解决内存溢出的方法主要有:(1)使用LruCache对图片进行缓存;(2)避免在for循环中创建对象;(3)加载大图片时使用图片压缩、图片裁剪等技术;(4)及时释放不再使用的对象占用的内存;(5)不要将Context和View存储在静态变量中。(6)使用内存监控机制,例如在Application或Activity中监听onTrimMemory(int level)方法的调用,根据不同的内存状态释放无用资源或者清除图片缓存等;(7)内存泄漏同样是内存溢出的关键因素之一,可针对内存泄漏问题进行分析并处理。

2.内存泄漏(Memory Leak)

通常在排查内存泄漏相关的问题时,可以从以下几个方面入手:

  • 注册后未取消注册造成的内存泄漏(例如:广播)
  • 静态变量持有Activity的引用
  • 单例模式持有Activity的引用
  • 查询数据库后没有关闭游标Cursor
  • 构造Adapter时,没有使用convertView重用
  • Bitmap对象不再使用时未调用recycle()释放内存
  • 对象被生命周期长的对象引用(例如:Activity被静态集合引用导致Activity不能释放)
  • Handler造成的内存泄漏
  • 非静态的内部类中持有外部类的引用
  • 匿名内部类/非静态内部类和异步线程

使用到的工具有:LeakCanary、Profiler等。

三、低内存监控方案

Android中提供了两个低内存监控方法:onLowMemory()与onTrimMemory(int level)。可以通过这两个方法判断当前是否是低内存的状态,并且根据不同的情况释放自身内存,以避免应用程序被系统杀掉,提高应用程序的用户体验。

1.onLowMemory()
/**
  * This is called when the overall system is running low on memory, and
  * actively running processes should trim their memory usage.  While
  * the exact point at which this will be called is not defined, generally
  * it will happen when all background process have been killed.
  * That is, before reaching the point of killing processes hosting
  * service and foreground UI that we would like to avoid killing.
  *
  * <p>You should implement this method to release
  * any caches or other unnecessary resources you may be holding on to.
  * The system will perform a garbage collection for you after returning from this method.
  * <p>Preferably, you should implement {@link ComponentCallbacks2#onTrimMemory} from
  * {@link ComponentCallbacks2} to incrementally unload your resources based on various
  * levels of memory demands.  That API is available for API level 14 and higher, so you should
  * only use this {@link #onLowMemory} method as a fallback for older versions, which can be
  * treated the same as {@link ComponentCallbacks2#onTrimMemory} with the {@link
  * ComponentCallbacks2#TRIM_MEMORY_COMPLETE} level.</p>
  */
void onLowMemory();

Google对该方法的说明:

当整个系统的内存不足时调用它,并且主动运行的进程应该减少它们的内存使用量。虽然没有定义调用它的确切时间,但通常会在所有后台进程都被杀死时发生。也就是说,在达到终止托管服务和前台 UI 的进程之前,我们希望避免程序终止。

您应该实现此方法以释放您可能持有的任何缓存或其他不必要的资源。从这个方法返回后,系统会为你执行一次垃圾回收。

最好,您应该根据不同级别的内存需求实现ComponentCallbacks2#onTrimMemory从 增量卸载您的资源。ComponentCallbacks2该 API 可用于 API 级别 14 及更高级别,因此您应该只将此onLowMemory()方法用作旧版本的后备,可以将其视为ComponentCallbacks2#onTrimMemoryComponentCallbacks2.TRIM_MEMORY_COMPLETE级别相同。

onLowMemory()方法定义在ComponentCallbacks接口中,实现该接口的有Application、Activity、Fragment、Service、ContentProvider。当整个系统运行内存不足时,就会被调用。此时为了避免程序崩溃,可以采取一些措施,例如在Application中清除缓存,在Activity和Fragment中释放无用资源(例如Bitmap、数组、控件资源等)等。

根据Google官方的说法,该方法是在后台进程都被杀死时调用,此时面临的局面是下一步很有可能会将当前的前台进程杀死,但是该方法没有确切的调用时间,只是用于Android旧版本低内存监控。Android4.0以后可以直接用onTrimMemory(int level)方法来监听内存状态,并且onLowMemory()调用时可以理解为达到了onTrimMemory(int level)方法的ComponentCallbacks2.TRIM_MEMORY_COMPLETE级别,即最高级别。

在实际测试中并未监测到onLowMemory()方法的调用,只监测到onTrimMemory(int level)方法的调用,并且onTrimMemory(int level)方法的level达到最高级别时也未调用onLowMemory()方法。

2.onTrimMemory(int level)
/**
  * Called when the operating system has determined that it is a good
  * time for a process to trim unneeded memory from its process.  This will
  * happen for example when it goes in the background and there is not enough
  * memory to keep as many background processes running as desired.  You
  * should never compare to exact values of the level, since new intermediate
  * values may be added -- you will typically want to compare if the value
  * is greater or equal to a level you are interested in.
  *
  * <p>To retrieve the processes current trim level at any point, you can
  * use {@link android.app.ActivityManager#getMyMemoryState
  * ActivityManager.getMyMemoryState(RunningAppProcessInfo)}.
  *
  * @param level The context of the trim, giving a hint of the amount of
  * trimming the application may like to perform.
  */
void onTrimMemory(@TrimMemoryLevel int level);

Google对该方法的说明:

当操作系统确定现在是从其进程中减少不需要的内存的好时机时调用。例如,当它进入后台并且没有足够的内存来保持尽可能多的后台进程运行时,就会发生这种情况。您永远不应与级别的确切值进行比较,因为可能会添加新的中间值——您通常希望比较该值是否大于或等于您感兴趣的级别。

要在任何时候检索进程当前的修剪级别,您可以使用ActivityManager.getMyMemoryState(RunningAppProcessInfo);

onTrimMemory(int level)是Android4.0提供的API(Android4.0以下的版本中使用onLowMemory()方法),方法定义在ComponentCallbacks2接口中,实现该接口的有Application、Activity、Fragment、Service、ContentProvider。该方法和onLowMemory()方法类似,当整个系统运行内存不足时,就会被调用,不过onTrimMemory(int level)方法中多了当前内存级别(水平)情况,系统会根据不同的内存状态,来响应不同的内存释放策略,使用起来更加灵活,场景也更加丰富。

onTrimMemory(int level)方法的内存级别:

  • TRIM_MEMORY_RUNNING_MODERATE(5)

    应用程序正在运行,并且不会被杀死,但设备已经处于低内存状态,并且开始杀死LRU缓存里的内存。

    内存不足(后台进程超过5个),并且当前进程优先级比较高,需要清理内存。

  • TRIM_MEMORY_RUNNING_LOW(10)

    应用程序正在运行,并且不会被杀死,但设备处于内存更低的状态,所以你应该释放无用资源以提高系统性能(直接影响App性能)。

    内存不足(后台进程不足5个),并且当前进程优先级比较高,需要清理内存。

  • TRIM_MEMORY_RUNNING_CRITICAL(15)

    应用程序还在运行,但系统已经杀死了LRU缓存里的大多数进程,所以你应该在此时释放所有非关键的资源。如果系统无法回收足够的内存,它会清理掉所有LRU缓存,并且开始杀死之前优先保持的进程,像那些运行着Service的。

    内存不足(后台进程不足3个),并且当前进程优先级比较高,需要清理内存。

  • TRIM_MEMORY_UI_HIDDEN(20)

    当应用程序中所有的UI组件全部不可见的时候会触发,这和onStop方法的区别是:onStop方法只是当一个Activity完全不可见的时候就会调用,此时可以释放一些Activity的资源,比如取消网络连接或注销广播接收器等,但是UI相关的资源要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)回调之后再去释放。

    内存不足,并且该进程的UI已经不可见了。

  • TRIM_MEMORY_BACKGROUND(40)

    系统运行在低内存状态,并且你的进程已经接近LRU列表的顶端(即将被清理)。虽然你的App进程还没有很高的被杀死风险,系统可能已经清理LRU里的进程,你应该释放那些容易被恢复的资源,如此可以让你的进程留在缓存里,并且当用户回到App时快速恢复。

    内存不足,并且当前进程是后台进程。

  • TRIM_MEMORY_MODERATE(60)

    系统运行在低内存状态,你的进程在LRU列表中间附近。如果系统变得内存紧张,可能会导致你的进程被杀死。

    内存不足,并且当前进程在后台进程列表的中部。

  • TRIM_MEMORY_COMPLETE(80)

    系统运行在低内存状态,如果系统没有恢复内存,你的进程是首先被杀死的进程之一。你应该释放所有不重要的资源来恢复你的App状态。

    内存不足,并且当前进程在后台进程列表最后一个,马上就要被清理。

3.onLowMemory()与onTrimMemory(int level)触发时机总结

(1)onLowMemory()被回调时,已经没有后台进程;而onTrimMemory(int level)被回调时,还有后台进程。
(2)onLowMemory()是在最后一个后台进程被杀时调用,一般情况是LowMemoryKiller杀进程后触发;而onTrimMemory(int level)的触发更频繁,每次计算进程优先级时,只要满足条件,都会触发。

(3)通过一键清理后,onLowMemory()不会被触发,而onTrimMemory(int level)会被触发一次。

(4)在Application、 Activity、Fragement、Service、ContentProvider中都可以重写回调方法,对onLowMemory()/onTrimMemory(int level)进行回调,在回调方法中实现资源释放。

4.使用场景及优化策略

通常在架构阶段就要考虑清楚,我们有哪些东西是要常驻内存的,有哪些是伴随界面存在的。一般情况下,有下面几种资源需要进行释放:

  • 缓存:包括一些文件缓存,图片缓存等,在用户正常使用的时候这些缓存很有作用,但当你的应用程序UI不可见的时候,这些缓存就可以被清除以减少内存的使用。比如第三方图片库的缓存。
  • UI:包括一些动态生成动态添加的View等,这些动态生成和添加的View且少数情况下才使用到的View,这时候可以被释放,下次使用的时候再进行动态生成即可。
  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值