Android中缓存理解(二)

2017-04-22  本文已影响277人  狮_子歌歌

PhatomReference的作用

Java对象的状态

参考Java的内存回收机制

对象的周期阶段

当新建一个对象时,会置位该对象的一个内部标识finalizable,当某一点GC检查到该对象不可达时,就把该对象放入finalize queue(F queue),GC会在对象销毁前执行finalize方法并且清空该对象的finalizable标识。

简而言之,一个简单的对象生命周期为,Unfinalized Finalizable Finalized Reclaimed。

Object.finalize()

对象的finalize()被JVM调用后,才算对象被销毁。

一般对象通过覆写Object.finalize(),实现在对象回收之前,对GC无法回收的内存进行释放。

  1. Object.finalize()是JVM调用的,而JVM在回收时不保证一定会调用它,并且不能确定执行的时机;
  2. JVM执行Object.finalize()过程中会导致严重的内存消耗和性能损失;
  3. 由于需要覆写才能被执行,很可能在实现时重获自身引用...不安全且效率低。
public class A {  
    static A a;  
    public void finalize() {  
        a = this;  
    }  
} 

上述代码如果JVM调用了finalize(),那么原本应该被回收的对象重生,GC无法回收该对象。

所以如果想通过Object.finalize()来实现对象相关资源的释放是不可靠的。一般使用PhatomReference来完成对象回收之前的资源释放。

关于finalize()不安全,效率低可以参考Effective Java Item7:Avoid Finalizers,解释为什么finalize是不安全的,不建议使用

PhatomReference回收过程

PhatomReference与SoftReference(WeakReference)最大区别在于JVM回收它们所引用的对象进行的处理:

JVM会把SoftReference(WeakReference)中refernet字段设置为null,然后再将其加入ReferenceQueue中,此时所引用的对象处于可回收的状态;

而JVM不会在把PhatomReference加入队列前做相同的处理,而是直接加入,此时所引用的对象还不可回收。

referent字段是Reference中的私有字段。参考java中虚引用PhantomReference与弱引用WeakReference(软引用SoftReference)的差别

大致的流程:

  1. JVM的DC线程发现只要虚引用的对象时,直接将虚引用加入队列,此时PhatomReference所引用的对象还存在PhatomReference对他的引用;
  2. 当程序通过队列的poll(),将该虚引用从队列中删除时,它所引用的对象(包括虚引用本身)不再要引用,此时DC可以进行回收。

综上所述,可以看到JVM的GC线程不能够对虚引用控制的对象进行自动回收,而需要程序手动回收。

回收对象前的处理

虚引用即无法通过get()获取对象的强引用,也无法自动被回收,那么它存在的意义呢?它的意义就是跟踪对象回收活动,在对象被回收之前做一些处理。例如资源释放等等。

了解了PhatomReference回收的过程,可以通过队列的判断(poll方法)在对象被回收之前进行处理。这样的实现不仅安全,而且高效。

注意在PhatomReference被加入队列之前,所引用的对象已经处于Reclaimed(或者说是不可及状态),下一步就是被JVM回收,而并不是JVM调用finalize()才开始回收。所以在队列中发现PhatomReference保证了对象已经Finalized(销毁,不可能再重获)并且一定能被回收。

相比使用SoftReference或WeakReference去清理对象占用的相关资源(通过finalize方法)更加高效,而且实现起来更安全(finalize方法不一定执行等因素)。

weakReference或SoftReference被放入绑定的队列是因为其引用的对象可及性发生改变,处于可恢复状态(也就是对象周期阶段的finalizable),下一步就是JVM调用finalize(),最后回收。由于可以在finalize()中重获引用,所以加入队列不能作为对象被回收的依据。

总结

PhatomReference所引用的对象只有处于Reclaimed时才会被加入队列(已经被Finalized处于不可及状态)。此时可以在对象被回收之前执行清理工作,比finalize()更加灵活,高效,安全。从而更精细的控制对象的生命周期。

参考Java引用类型分析深入理解ReferenceQueue GC finalize Reference

Java应用程序

Java应用是一个进程,都持有自己的虚拟机。而在Java编程中,由于虚拟机属于单进程多线程,所以并发编程是关于线程的开发。

例如通过Java内置工具 jps指令可以打印出当前正在运行Java程序进程ID号。

由此关于Java中Piped I/O必须在同一个虚拟机中的两个线程才能用于传输信息。

而在Android开发中,Dalvik虚拟机启动时可以通过-XX:HeapGrowthLimit来给Dalvik的GrowthLimit设置大小,因为每个应用程序是一个进程,都持有自己的Dalvik。

Android虚拟机

Android虚拟机主要有两个,ART和Dalvik。

ART在GC上做的比Dalvik好太多了,不光是GC的效率,减少Pause时间,而且还在内存分配上对大内存的有单独的分配区域,同时还能有算法在后台做内存整理,减少内存碎片。

对于开发者来说ART下基本可以避免很多类似GC导致的卡顿问题了。另外根据谷歌自己的数据来看,ART相对Dalvik内存分配的效率提高了10倍,GC的效率提高了2-3倍。

以下参考自Android GC 那点事

Dalvik

Java堆

Dalvik运行时数据区中堆得结构主要由两部分组成:Active堆和Zygote堆。

对于Android操作系统来说,以后所有的应用程序进程都是被Zygote进程Fork出来的,并且持有各自的Dalvik虚拟机。

Fork是类Unix系统创建新进程的方法。如果需要创建新进程,就通过Fork来创建一个自身的副本,该副本就是子进程,而且会创建单独的地址空间。这样一来确保子进程拥有父进程所有内存段的精确副本。

Cow策略

在创建应用程序的过程中,Dalvik虚拟机采用Cow策略复制Zygote进程的地址空间。

Cow策略:当未复制Zygote进程的地址空间时,Zygote和应用进程使用同一块分配对象的堆。当应用进程或Zygote进程对该堆进行写操作时,内核开始执行真正的复制操作。使得Zygote和应用进程拥有各自的拷贝。

为什么是写操作时开始复制

是因为在一块共享内存上,有一方修改了共享变量的值,为了实现数据同步(属于内核操作),必须进行复制,确保共享变量在每一方中副本相同。

也就是在创建应用程序时,对堆内存进行写操作。此时内核就开始执行复制操作,保证子进程拥有父进程精确副本。这个过程就是Zygote进程Fork子进程,采用Cow策略将Zygote进程的内容拷贝给子进程。

减少或避免Copy

因为复制操作十分耗时,所以应该尽量去减少或避免它,所以Dalvik运行时数据区中堆内存划分为Active和Zygote堆。

在Zygote进程Fork第一个子进程之前,把堆内存划分为两块:已使用部分和未使用部分。分别称为Zygote堆和Active堆。

这样做的目的是每当创建一个子进程时,只需要把Zygote堆中的内容复制给应用程序进程。而后续Zygote进程和应用程序进程都是在Active堆中分配对象。减少了Zygote堆的写操作,从而减少了执行写时拷贝的操作(后续都是在Active中执行写,内核拷贝的也是Active堆内容)。

这样的划分还有一个好处,Zygote堆内是Zygote进程启动过程中预加载的类,资源和对象。意味着它们可以在Zygote进程和应用程序进程中做到长期共享,还能减少对内存的需求。

GC指标

设置不同堆内存大小是通过不同的指令,在虚拟机启动时设置的。

最熟悉的就是在AndroidMainFest.xml文件中给Maximum Size赋值largeheap属性获取到系统分配给应用最大堆内存的大小。如果Android在分配对象内存时,超过了Maximum,就会OOM。

还有一些指标:

假设当前虚拟机GC后,堆中存活对象的占用内存大小为LiveSize,那么该堆内存的理想大小应为(LiveSize / TargetUtilization)。但是理想大小不应该小于(MinFree + LiveSize),不应该大于(MaxFree + LiveSize)。每次虚拟机GC后都会尽量把堆利用率向目标利用率靠拢。

当程序去尝试给大对象分配堆内存,甚至去扩大堆内存大小。此时虚拟机GC活动,存活的对象变少了(例如局部变量由于方法的出栈被清除等,导致引用的对象被回收)。但是虚拟机的目标利用率没有改变,而GC就是为了使堆内存利用率向目标利用率靠拢,导致堆内存扩容失败,甚至减小,从而导致频繁的GC。

GC类型

前三者都是在给对象分配堆内存过程中触发的。并发GC和非并发GC的区别主要在于前者在GC过程中,有条件地挂起和唤醒非GC线程,而后者在执行GC的过程中,一直都是挂起非GC线程的。从而可以看出并发GC的程序又更好的响应性。

对象的分配和GC触发时机

  1. 调用dvmHeapSourceAlloc方法分配指定大小的堆内存,成功返回地址给调用者。这个方法不会改变堆当前大小,属于轻量级内存分配操作。
  2. 上一步失败,那么执行GC。如果GC线程正在运行时,调用dvmWaitForConcurrentGCToComplete等待GC完成;否则调用gcForMalloc执行GC,并且传入false参数表示不回收软引用。
  3. GC完毕后,再次执行第一步尝试轻量级内存分配操作,成功就返回地址给调用者。
  4. 上一步失败,调用dvmHeapSourceAllocAndGrow方法进行分配。该方法首先会考虑将堆内存当前大小调整到Dalvik启动时指定的Java堆最大值,然后去分配内存。如果成功,就返回地址比给调用者。
  5. 上一步失败,调用gcForMalloc,并且传入true参数,回收SoftReference引用的对象。
  6. GC完毕后,再次调用dvmHeapSourceAllocAndGrow方法进行内存分配。无论成功与否,就到此为止。
对象分配和GC触发时机-Dalvik.png

Java中对象的回收前提是对象无法到达GC Root,而非强引用更进一步的去控制对象的生命周期(即使可以到达GC Root,也有可能被回收)。

综上所述,在对象的分配中会导致GC,第一次分配对象失败系统会触发GC但是不回收Soft的引用,如果再次分配还是失败系统就会将Soft的内存也给回收。前者触发的GC是GC_FOR_MALLOC类型的GC,后者是GC_BEFORE_OOM类型的GC。而当内存分配成功后,系统会判断当前的内存占用是否是达到了GC_CONCURRENT的阀值,如果达到了那么又会触发GC_CONCURRENT。

总结

所以对于Dalvik虚拟机的手机来说,我们首先要尽量避免掉频繁生成很多临时小变量(比如说:getView, onDraw等函数中new对象),另一个又要尽量去避免产生很多长生命周期的大对象。

ART

Java堆

ART运行时数据区中Java堆主要划分为:Image Space,Zygote Space,Allocation Space和LargeObject Space。

LargeObject Space好处

小时候都玩过俄罗斯方块,为了存放更多的方块,需要合理安排好方块放置的位置和方块的方向。Java堆内存分配也类似,合理的安排好对象的地址空间,可以提高堆内存的使用率,从而减少GC。

Mark and Sweep算法-Dalvik.png

Dalvik使用标记与清理算法,容易产生碎片。这时Active堆没有合理的位置和空间(当内存中有大量不连续的小内存段,图中红色)存放接下来的较大内存(就像俄罗斯方块游戏,无法将新的方块填入有空隙的行,只能新建一个行,这样使用率下降,需要去不断的GC,释放新的空间),再分配一个较大的对象时,例如decode一个图片,很容易导致GC(去清理这些碎片)。

LargeObject Space用途-ART.png

由于ART有了LargeObject Space,大对象都分配到该区域,而Allocation Space分配较小的对象,从而使整个内存使用率更高(就像俄罗斯方块一样,内存形状固定,在Allocation Space中合理安排较小的对象,可以提升整块内存的覆盖率,也就是俄罗斯方块中的规则每一行都填满可以有更多的空间放更多的方块)。

上述图中波浪线表示程序代码,蓝色方块表示较大对象,红色表示小对象,绿色表示大对象。

GC类型

stop world 是指在进行垃圾回收的时候,需要将所有正在执行的线程暂停(Stop World),保证GC线程的运行。

对象的分配和GC触发时机

这点和Dalvik虚拟机一样,可以回忆一下Dalvik知识点。

ART对象分配和GC触发时机.png
上一篇下一篇

猜你喜欢

热点阅读