从根本上探究Android内存溢出和泄露产生的原因以及如何快速方便的定位内存泄露和相应的解决办法
“A small leak will sink a great ship.” - Benjamin Franklin
Android 和 Linux关系
我们都知道Android是基于Linux内核开发的,Android广义上来说是类似Ubuntu,Fedora的分支,也曾和Linux基金会产生分歧,那么问题来了
为什么Android要基于Linux内核开发?
- 强大的内存管理和进程管理方案
- 基于权限的安全机制
- 支持共享库
- 经过认证的驱动模型(当然Android后期也添加了一些自己独特的驱动)
- 最重要的一点是Linux开源
Android运行机制
Android运行时库包含了4.4之前的Dalvik虚拟机以及5.0之后的ART运行环境,它使得每一个Android应用都能运行在独立的进程中,并且拥有一个自己的虚拟机实例
可以参考大神的文章 https://www.jianshu.com/p/c80d8ae33cd1
什么是虚拟机(Virtual Machine )
Virtual Machine (简称VM)实际上有不同的种类,包含两种,第一种是System VM;第二种是Process VM,jvm就是属于进程VM
System VM
是一种完全虚拟化的虚拟机,可以替代硬件机器,我们可以在上面运行未经修改的操作系统,如Linux或Windows,VirtualBox和VMware Server就属于这类虚拟机
Process VM
设计在独立与平台的环境中执行计算机程序,众所周知的JVM就是这种类型,我们可以在JVM上运行Java程序
Java虚拟机(JVM),Dalvik虚拟机(DVM)以及ART的区别
共同点
-
都是每个进程对应一个VM,并运行单独的程序
-
在较新版本中(Froyo/Sun Jdk 1.5)都实现了相当程度的JIOutOfMemoryError: Java heap spaceT compiler(即时编译)用于提速
不同点
-
DVM执行的文件格式为.dex, JVM执行的为.class。class文件和dex之间可以相互转换
-
DVM是基于寄存器的虚拟机,而JVM是基于虚拟栈的虚拟机,这就导致了以下本质区别:
-
由于寄存器存取速度比栈快的多,DVM可以根据硬件实现最大优化,比较适合小巧的移动设备,
JVM基于栈结构,导致程序在运行时虚拟机需要频繁的从栈中读写数据,这个过程需要更多的指令分派与内存访问次数,会耗费很多CPU时间
-
JVM基于栈所以好处就是做的足够简单,保证在
注:寄存器是CPU内部的元件,寄存器拥有非常高的读写速度,访问时间短,并且具有比内存更高的吞吐量,使得寄存器中的数据可以更快的访问和更易于使用
-
Java内存分配策略
20180306153110830.jpgJava程序运行时的策略分为三种,分别是静态分配,栈分配,堆分配,三种方式所使用的内存空间分别为静态存储区(方法区)、栈区和堆区。
静态存储区(方法区):主要存放静态变量。这块「内存」在程序编译时就已经分配好了,并且在程序整个运行期间都存在。
栈区: 当方法被执行时,方法内的局部变量(包括基础数据类型,对象的引用)都在栈上创建,并在方法执行结束时,这些局部变量持有的内存会自动释放,因为栈内存分配运算内置于处理器的指令中,效率高,但是可分配的空间有限。
堆区:又称动态分配内存,通常是指程序运行直接new出来的内存,也就是对象的实例。这部分在不使用时将会被java垃圾回收器负责回收。
什么是内存溢出?
由于国内环境对维基百科限制,我们只能看百度的解释了:
内存溢出(out of memory)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件。
堆内存溢出
// Java program to illustrate
// Heap error
import java.util.*;
public class Heap {
static List<Integer> list = new ArrayList<Integer>();
public static void main(String args[]) throws Exception
{
Integer[] array = new Integer[100000000 * 100000000];
for (Integer index:
array) {
list.add(index);
}
}
}
上述代码运行必现:
OutOfMemoryError: Java heap spaceException in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.eegets.leaks.Heap.main(Heap.java:10)
Process finished with exit code 1
我们可以看到上述代码报的是我们讨厌的“OutOfMemoryError: Java heap space”,原因其实就是因为我们要创建的空间太大,导致堆内存中中空间不足以存放新创建的对象,就会引发该错误。其实说白了,就是多大的脚,不能43的脚穿39的鞋,那肯定是不行的。
栈内存溢出
先看如下例子
class Stack : Activity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
stackMethod()
}
fun stackMethod(){
stackMethod()
}
}
上述代码运行结果如下
E/AndroidRuntime: FATAL EXCEPTION: main
Process: method()
}com.eegets.leaks, PID: 6998
java.lang.StackOverflowError: stack size 8MB
at com.eegets.leaks.Stack.stackMethod(StackOrHeap.kt:28)
抛出这个异常我们可以看到,是因为递归太深,其实真正的原因是java线程的操作是基于栈的,当调用方法的内部方法也就是执行一次递归时,就会把当前方法压入栈中。
什么是内存泄漏?
指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
静态变量
静态变量存储在静态存储区(方法区),这块存储区在程序编译时就已经分配好了,并且在程序运行期间一直存在,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才释放
比如如下情况
public class MainActivity extends AppCompatActivity {
private static Info mInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (mInfo != null) {
mInfo = new Info(this);
}
}
}
class Info {
public Info(Activity activity) {
}
}
Info作为Activity的静态成员,并且持有Activity的引用,但是sInfo作为静态变量,生命周期肯定比Activity长。所以当Activity退出后,sInfo仍然引用了Activity,Activity不能被回收,这就导致了内存泄露。
解决办法有两种方式,第一种尽量避免在项目中使用静态变量,第二种就是在不需要的时候手动将静态变量设置为null,使其不再持有引用,这样也可以避免内存泄露。
匿名内部类和非静态内部类
非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。
public class TestListActivity : SimpleTestActivity() {
balabala...
addTestItem("LeakCanry制造一个内存泄露") {
handler.postDelayed({
debugMessage("内存泄露")
}, 600000)
finish()
}
private val handler = Handler{
false
}
}
public class TestListActivity : SimpleTestActivity() {
balabala...
ddTestItem("LeakCanry制造一个内存泄露") {
handler.postDelayed({
debugMessage("内存泄露")
}, 600000)
finish()
}
class MyHandler(activity: Activity?) : Handler() {
private val activityWeakReference: WeakReference<Activity?> = WeakReference(activity)
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
val handlerActivity = activityWeakReference.get()
if (handlerActivity != null) {
}
}
}
单例模式使用不当
单例模式我们在日常使用很广,如果使用不当也是有可能造成内存泄露的。因为单例的静态特性使得他的生命周期同应用的生命周期一样长。如果实际开发中使用了Activity或者是Fragment的上下文,那么该单例将一直持有了Act或Frag的引用,导致GC尝试回收Act时发现该类还存在引用(单例持有了该引用),则会发生内存泄露。
public class SpUtils {
private Context context;
private static volatile SpUtils preferenceUtils;
public static SpUtils getInstance(Context context) {
if(preferenceUtils == null) {
preferenceUtils = new SpUtils(context);
}
return preferenceUtils;
}
}
错误的调用
SpUtils.getInstance(activity)
正确的使用
SpUtils.getInstance(activity.getApplicationContext())
监听器未释放
-
广播注册之后未关闭
-
Rxjava 的观察者模式也可能造成内存泄露
如下情况
fun rxJavaTImerTask(){
observable = Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(object : Action {
@Throws(Exception::class)
override fun run() {
debugMessage("Unsubscribing subscription from onCreate()")
}
})
// .compose(bindToLifecycle())
.subscribe(object : Consumer<Long> {
@Throws(Exception::class)
override fun accept(num: Long?) {
debugMessage("rxjava running until", num!!)
}
})
}
上述代码输出日志
11-12 15:27:43.444 8854 9218 D MVPArms : .RxjavaTestActivity$rxJavaLifTImerTask$2;rxjava running until 0
11-12 15:27:44.444 8854 9218 D MVPArms : .RxjavaTestActivity$rxJavaLifTImerTask$2;rxjava running until 1
11-12 15:27:45.444 8854 9218 D MVPArms : .RxjavaTestActivity$rxJavaLifTImerTask$2;rxjava running until 2
11-12 15:27:46.444 8854 9218 D MVPArms : .RxjavaTestActivity$rxJavaLifTImerTask$2;rxjava running until 3
11-12 15:27:47.444 8854 9218 D MVPArms : .RxjavaTestActivity$rxJavaLifTImerTask$2;rxjava running until 4
11-12 15:27:48.444 8854 9218 D MVPArms : .RxjavaTestActivity$rxJavaLifTImerTask$2;rxjava running until 5
11-12 15:27:49.444 8854 9218 D MVPArms : .RxjavaTestActivity$rxJavaLifTImerTask$2;rxjava running until 6
11-12 15:27:50.444 8854 9218 D MVPArms : .RxjavaTestActivity$rxJavaLifTImerTask$2;rxjava running until 7
11-12 15:27:51.444 8854 9218 D MVPArms : .RxjavaTestActivity$rxJavaLifTImerTask$2;rxjava running until 8
我们用Rxjava发布了一个订阅后,当Activity执行finish时,此时订阅还在继续执行,就导致了内存泄露。具体的解决办法有两种:
1:使用Rxlifecycle绑定Activity生命周期
2:使用google Jetpack推荐的Lifecycle来管理
集合数据未及时清理
List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object o = new Object();
objectList.add(o);
o = null;
}
在循环中把引用o释放了,但是它被添加到了objectList中,所以objectList也持有对象的引用,此时该对象是无法被GC的。因此对象如果添加到集合中,还必须从中删除,最简单的方法
置空集合对象即可
//释放objectList
objectList.clear();
objectList=null;
资源未及时关闭(不一定是内存泄露导致)
可以移步大神的文章
https://droidyue.com/blog/2019/06/09/will-unclosed-stream-objects-cause-memory-leaks/
WebView造成泄漏
定位内存泄漏
AndroidStudio Profiler
19}3)5PQX(V494PL8ILM_ON.pngLeakCanary定位内存泄露
LeakCanary is a memory leak detection library for Android.
Why should I use LeakCanary?
When we first enabled LeakCanary in the Square Point Of Sale app, we were able to find and fix several leaks and reduced the OutOfMemoryError crash rate by 94%.
how work?
可以移步大神LeakCanary原理浅析
https://www.jianshu.com/p/70de36ea8b31
主要的逻辑代码
public @NonNull RefWatcher buildAndInstall() {
if (LeakCanaryInternals.installedRefWatcher != null) {
throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
}
RefWatcher refWatcher = build();
if (refWatcher != DISABLED) {
if (enableDisplayLeakActivity) {
LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
}
if (watchActivities) {
ActivityRefWatcher.install(context, refWatcher);
}
if (watchFragments) {
FragmentRefWatcher.Helper.install(context, refWatcher);
}
}
LeakCanaryInternals.installedRefWatcher = refWatcher;
return refWatcher;
}
LeakCanary监控内存泄漏的类图
我们需要明确一个概念
- 弱引用(Weak Reference)——WeakReference
弱引用也是用来描述非必需对象的,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存释放足够,都会回收掉只被弱引用关联的对象。(只要发生了垃圾收集,弱引用对象都会被回收)
内存泄漏的监控过程都是在异步线程中处理的,主要体现在以下几个方面:
- 在RefWatcher.ensureGoneAsync()方法中,如果是在主线程,则等到主线程消息队列空闲时,再由子线程延迟执行;如果是在子线程中,则直接由子线程延迟执行。每次延迟的时间都不一样,回退的时间呈指数增长。在该方法中,主要是检测是否发生了内存泄漏。
- AndroidHeapDumper.dumpHeap()操作是在子线程执行的,该方法也是等到主线程消息队列空闲后才执行的,具体是通过闭锁CountDownLatch来协调主线程与子线程间的同步操作。在该方法中,主要是Dump内存快照,生成hprof文件。
- HeapAnalyzerService.runAnalysis()操作是在子线程中执行的,因为HeapAnalyzerService继承了IntentService服务,在onHandleIntent()方法中执行具体的解析hprof文件操作。
- DisplayLeakService.onHeapAnalyzed()操作是在线程中执行的,因为DisplayLeakService继承了AbstractAnalysisResultService服务,而AbstractAnalysisResultService服务又继承了IntentService,并实现了onHandleIntent()方法。在onHeapAnalyzed()方法中,主要是发送一个通知告诉用户内存泄漏,并将内存泄漏结果在DisplayLeakActivity中展示。
避免和解决内存泄漏
多想,多注意
DisplayLeakService实现了统一上报
class LeakCanaryMonitorService : DisplayLeakService(){
override fun afterDefaultHandling(heapDump: HeapDump, result: AnalysisResult, leakInfo: String) {
result.leakTrace?.elements?.map {
val classHierarchayList = ArrayList<String>()
it.classHierarchy.map { classIT ->
classHierarchayList.add(classIT.toString())
}
val element = Element(classHierarchayList, it.className, it.referenceName)
element
}.let {
LeakCanaryBean(result.className, LeakTrace(it)).let { data ->
val leakJson = Gson().toJson(data)
val gitCommitHash = BuildConfig.currentCommitHash
val gitCurrentBranch = BuildConfig.currentBranch
val url = "http://10.185.240.240/app-stable-website/leankcanary/leakcanary.php?" +
"className=${result.className}" +
"&contentJson=$leakJson" +
"&isHandle=false" +
"&addtime=${StringUtil.getNowTimeString("yyyy-MM-dd")}" +
"&modulePackage=com.secoo" +
"&gitCommitHash=$gitCommitHash" +
"&gitCurrentBranch=$gitCurrentBranch"
debugMessage(url)
RetrofitExt(LeakCanaryApi::class.java).reportLeakCanaryMsg(url)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ExcuteObserverExt({msg ->
debugMessage( msg.value?.retCode , msg.value?.retMsg)
}))
}
}
}
}
对泄露数据以网站的形式统一管理
1574388133(1).png
网友评论