Java 内存问题 及 LeakCanary 原理分析,mybatis 架构设计层次
方法区 用户存储已被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码等数据。异常状态 OutOfMemoryError,其中包含常量池和用户存放编译器生成的各种字面量和符号引用。
堆 是 JVM 所管理的内存中最大的一块。唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配。Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC 堆”。异常状态 OutOfMemoryError。
虚拟机栈 描述的是 java 方法执行的内存模型,每个方法在执行时都会创建一个栈帧,用户存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 对这个区域定义了两种异常状态 OutOfMemoryError、StackOverflowError。
本地方法栈 虚拟机栈为虚拟机执行 java 方法,而本地方法栈为虚拟机使用到的 Native 方法服务。异常状态 StackOverFlowError、OutOfMemoryError。
程序计数器 一块较小的内存,当前线程所执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
内存模型
Java 内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量,这些变量是从主内存中拷贝而来。线程对变量的所有操作(读,写)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
为了保证内存可见性,常常利用_volatile_关键子特性来保证变量的可见性(并不能保证并发时原子性)。
二、内存如何回收
内存的分配
一个对象从被创建到回收,主要经历阶段有 1:创建阶段(Created)、2: 应用阶段(In Use)、3:不可见阶段(Invisible)、4:不可达阶段(Unreachable)、5:收集阶段(Collected)、6:终结阶段(、Finalized)、7:对象空间重分配阶段(De-allocated)。
内存的分配实在创建阶段,这个阶段要先用类加载器加载目标 class,当通过加载器检测后,就开始为新对象分配内存。对象分配内存大小在类加载完成后便可以确定。 当初始化完成后,虚拟机还要对对象进行必要的设置,如那个类的实例,如何查找元数据、对象的 GC 年代等。
内存的回收(GC)
那些不可能再被任何途径使用的对象,需要被回收,否则内存迟早都会被消耗空。
GC 机制主要是通过可达性分析法,通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链时,即 GC Roots 到对象不可达,则证明此对象是不可达的。
根据**《深入理解 Java 虚拟机》**书中描述,可作为 GC Root 的地方如下:
虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
方法区中的类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中 JNI(Native 方法)引用的对象。
当一个对象或几个相互引用的对象组没有任何引用链时,会被当成垃圾处理,可以进行回收。
如何一个对象在程序中已经不再使用,但是(强)引用还是会被其他对象持有,则称为内存泄漏。内存泄漏并不会使程序马上异常,但是多处的未处理的内存泄漏则可能导致内存溢出,造成不可预估的后果。
引用的分类
在 JDK1.2 之后,为了优化内存的利用及 GC 的效率,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用 4 种。
1、强引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2、软引用,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。SoftReference 表示软引用。
3、弱引用,只要有 GC,无论当前内存是否足够,都会回收掉_只_被弱引用关联的对象。WeakReference 表示弱引用。
4、虚引用,这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。PhantomReference 表示虚引用,需要搭配 ReferenceQueue 使用,检测对象回收情况。
关于 JVM 内存管理的一些建议
1、尽可能的手动将无用对象置为 null,加快内存回收。 2、可考虑对象池技术生成可重用的对象,较少对象的生成。 3、合理利用四种引用。
三、内存泄漏
持有一个生命周期较短的引用时或内部的子模块对象的生命周期超过了外面模块的生命周期,即本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
内存泄漏是造成应用程序 OOM 的主要原因之一,尤其在像安卓这样的移动平台,难免会导致应用所需要的内存超过系统分配的内存限额,这就造成了内存溢出 Error。
安卓平台常见的内存泄漏
1、静态成员变量持有外部(短周期临时)对象引用。 如单例类(类内部静态属性)持有一个 activity(或其他短周期对象)引用时,导致被持有的对象内存无法释放。
2、内部类。当内部类与外部类生命周期不一致时,就会造成内存泄漏。如非静态内部类创建静态实例、Activity 中的 Handler 或 Thread 等。
3、资源没有及时关闭。如数据库、IO 流、Bitmap、注册的相关服务、webview、动画等。
4、集合内部 Item 没有置空。
5、方法块内不使用的对象,没有及时置空。
四、如何检测内存泄漏
Android Studio 供了许多对 App 性能分析的工具,可以方便分析 App 性能。我们可以使用 Memory Monitor 和 Heap Dump 来观察内存的使用情况、使用 Allocation Tracker 来跟踪内存分配的情况,也可以通过这些工具来找到疑似发生内存泄漏的位置。
堆存储文件(hpof)可以使用 DDMS 或者 Memory Monitor 来生成,输出的文件格式为 hpof,而 MAT(Memory Analysis Tool)就是来分析堆存储文件的。
然而 MAT 工具分析内存问题并不是一件容易的事情,需要一定的经验区做引用链的分析,需要一定的门槛。 随着安卓技术生态的发展,LeakCanary 开源项目诞生了,只要几行代码引入目标项目,就可以自动分析 hpof 文件,把内存泄漏的地方展示出来。
五、LeakCanary 原理解析
A small leak will sink a great ship.
LeakCanary内存检测工具是由 squar 公司开源的著名项目,这里主要分析下源码实现原理。
基本原理
主要是在 Activ
ity 的 &onDestroy 方法中,手动调用 GC,然后利用 ReferenceQueue+WeakReference,来判断是否有释放不掉的引用,然后结合 dump memory 的 hpof 文件, 用HaHa分析出泄漏地方。
源码分析
LeakCanary 集成很方便,只要几行代码,所以可以从入口跟踪代码,分析原理
if (!LeakCanary.isInAnalyzerProcess(WeiboApplication.this)) {LeakCanary.install(WeiboApplication.this);}
public static RefWatcher install(Application application) {return ((AndroidRefWatcherBuilder)refWatcher(application).listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build()))//配置监听器及分析数据格式.buildAndInstall();}
从这里可看出,LeakCanary 会单独开一进程,用来执行分析任务,和监听任务分开处理。
方法_install_中主要是构造来一个 RefWatcher,
public RefWatcher buildAndInstall() {RefWatcher refWatcher = this.build();if(refWatcher != RefWatcher.DISABLED) {LeakCanary.enableDisplayLeakActivity(this.context);ActivityRefWatcher.install((Application)this.context, refWatcher);}
return refWatcher;}
public static void install(Application application, RefWatcher refWatcher) {(new ActivityRefWatcher(application, refWatcher)).watchActivities();}
private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}public void onActivityStarted(Activity activity) {}public void onActivityResumed(Activity activity) {}public void onActivityPaused(Activity activity) {}public void onActivityStopped(Activity activity) { }public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
public void onActivityDestroyed(Activity activity) {ActivityRefWatcher.this.onActivityDestroyed(activity);}};
void onActivityDestroyed(Activity activity) {this.refWatcher.watch(activity);}
具体监听的原理在于 Application 的_registerActivityLifecycleCallbacks_方法,该方法可以对应用内所有 Activity 的生命周期做监听, LeakCanary 只监听了 Destroy 方法。
在每个 Activity 的 OnDestroy()方法中都会回调 refWatcher.watch()方法,那我们找到的 RefWatcher 的实现类,看看具体做什么。
public void watch(Object watchedReference, String referenceName) {if(this != DISABLED) {Preconditions.checkNotNull(watchedReference, "watchedReference");Preconditions.checkNotNull(referenceName, "referenceName");long watchStartNanoTime = System.nanoTime();String key = UUID.randomUUID().toString();//保证 key 的唯一性 this.retainedKeys.add(key);KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);this.ensureGoneAsync(watchStartNanoTime, reference);}}
final class KeyedWeakReference extends WeakReference<Object> {public final String key;public final String name;
KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {//ReferenceQueue 类监听回收情况 super(Preconditions.checkNotNull(referent, "referent"), (ReferenceQueue)Preconditions.checkNotNull(referenceQueue, "referenceQueue"));this.key = (String)Preconditions.checkNotNull(key, "key");this.name = (String)Preconditions.checkNotNull(name, "name");}}
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {this.watchExecutor.execute(new Retryable() {public Result run() {return RefWatcher.this.ensureGone(reference, watchStartNanoTime);}});}
_KeyedWeakReference_是 WeakReference 类的子类,用了 KeyedWeakReference(referent, key, name, ReferenceQueue )的构造方法,将监听的对象(activity)引用传递进来,并且 New 出一个 ReferenceQueue 来监听 GC 后 的回收情况。
以下代码 ensureGone()方法就是 LeakCanary 进行检测回收的核心代码:
Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {long gcStartNanoTime = System.nanoTime();long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);this.removeWeaklyReachableReferences();//先将引用尝试从队列中 poll 出来 if(this.debuggerControl.isDebuggerAttached()) {//规避调试模式 return Result.RETRY;} else if(this.gone(reference)) {//检测是否已经回收 return Result.DONE;} else {//如果没有被回收,则手动 GCthis.gcTrigger.runGc();//手动 GC 方法 this.removeWeaklyReachableReferences();//再次尝试 poll,检测是否被回收 if(!this.gone(reference)) {// 还没有被回收,则 dump 堆信息,调起分析进程进行分析 long startDumpHeap = System.nanoTime();long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);File heapDumpFile = this.heapDumper.dumpHeap();if(heapDumpFile == HeapDumper.RETRY_LATER) {return Result.RETRY;//需要重试}
long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, this.excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));}
return Result.DONE;}}
private boolean gone(KeyedWeakReference reference) {return !this.retainedKeys.contains(reference.key);}
private void removeWeaklyReachableReferences() {KeyedWeakReference ref;while((ref = (KeyedWeakReference)this.queue.poll()) != null) {this.retainedKeys.remove(ref.key);}}
方法 ensureGone 中通过检测 referenceQueue 队列中的引用情况,来判断回收情况,通过手动 GC 来进一步确认回收情况。 整个过程肯定是个耗时卡 UI 的,整个过程会在 WatchExecutor 中执行的,那 WatchExecutor 又是在哪里执行的呢?
评论