Android中缓存理解(二)
PhatomReference的作用
Java对象的状态
- 可达状态:有一个以上的引用变量引用(强引用);
- 可恢复状态:不存在任何引用,且JVM在回收之前会调用finalize()清理资源后才真正回收,如果此期间使对象重获引用变为可达状态;
- 不可达状态:不存在任何引用,且JVM执行完finalize()后仍无引用,变为不可达状态,JVM开始回收该对象。
对象的周期阶段
当新建一个对象时,会置位该对象的一个内部标识finalizable,当某一点GC检查到该对象不可达时,就把该对象放入finalize queue(F queue),GC会在对象销毁前执行finalize方法并且清空该对象的finalizable标识。
简而言之,一个简单的对象生命周期为,Unfinalized Finalizable Finalized Reclaimed。
Object.finalize()
对象的finalize()
被JVM调用后,才算对象被销毁。
一般对象通过覆写Object.finalize()
,实现在对象回收之前,对GC无法回收的内存进行释放。
-
Object.finalize()
是JVM调用的,而JVM在回收时不保证一定会调用它,并且不能确定执行的时机; - JVM执行
Object.finalize()
过程中会导致严重的内存消耗和性能损失; - 由于需要覆写才能被执行,很可能在实现时重获自身引用...不安全且效率低。
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)的差别
大致的流程:
- JVM的DC线程发现只要虚引用的对象时,直接将虚引用加入队列,此时PhatomReference所引用的对象还存在PhatomReference对他的引用;
- 当程序通过队列的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应用程序
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堆。
- Active堆,是Zygote进程Fork第一个子进程之前创建的。
- Zygote堆,用来管理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指标
- Starting Size,在启动Dalvik时,系统分配一块初始大小堆内存给虚拟机使用。可以通过
-Xms
设置; - GrowthLimit,是系统给每一个程序的最大堆上限,超过这个上限,程序就会OOM。可以通过
-XX:HeapGrowthLimit
设置; - Maximum Size,不受控情况下的最大堆内存大小,起始就是我们在用largeheap属性的时候,可以从系统获取的最大堆大小。可以通过
-Xms
设置。
设置不同堆内存大小是通过不同的指令,在虚拟机启动时设置的。
最熟悉的就是在AndroidMainFest.xml文件中给Maximum Size赋值largeheap属性获取到系统分配给应用最大堆内存的大小。如果Android在分配对象内存时,超过了Maximum,就会OOM。
还有一些指标:
- MinFree,堆最小利用率;
- MaxFree,堆最大利用率;
- TargetUtilization,目标利用率。
假设当前虚拟机GC后,堆中存活对象的占用内存大小为LiveSize,那么该堆内存的理想大小应为(LiveSize / TargetUtilization)。但是理想大小不应该小于(MinFree + LiveSize),不应该大于(MaxFree + LiveSize)。每次虚拟机GC后都会尽量把堆利用率向目标利用率靠拢。
当程序去尝试给大对象分配堆内存,甚至去扩大堆内存大小。此时虚拟机GC活动,存活的对象变少了(例如局部变量由于方法的出栈被清除等,导致引用的对象被回收)。但是虚拟机的目标利用率没有改变,而GC就是为了使堆内存利用率向目标利用率靠拢,导致堆内存扩容失败,甚至减小,从而导致频繁的GC。
GC类型
- GC_FOR_MALLOC,在堆内存上分配对象内存不足时触发GC;
- GC_CONCURRENT,当应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发GC操作来释放内存;
- GC_BEFORE_OOM,在准备抛OOM异常之前进行的最后努力而触发的GC;
- GC_EXPLICT,程序调用
System.gc
,VMRuntime.gc
方法或者收到SIGUSR1信号时触发的GC。
前三者都是在给对象分配堆内存过程中触发的。并发GC和非并发GC的区别主要在于前者在GC过程中,有条件地挂起和唤醒非GC线程,而后者在执行GC的过程中,一直都是挂起非GC线程的。从而可以看出并发GC的程序又更好的响应性。
对象的分配和GC触发时机
- 调用
dvmHeapSourceAlloc
方法分配指定大小的堆内存,成功返回地址给调用者。这个方法不会改变堆当前大小,属于轻量级内存分配操作。 - 上一步失败,那么执行GC。如果GC线程正在运行时,调用
dvmWaitForConcurrentGCToComplete
等待GC完成;否则调用gcForMalloc
执行GC,并且传入false参数表示不回收软引用。 - GC完毕后,再次执行第一步尝试轻量级内存分配操作,成功就返回地址给调用者。
- 上一步失败,调用
dvmHeapSourceAllocAndGrow
方法进行分配。该方法首先会考虑将堆内存当前大小调整到Dalvik启动时指定的Java堆最大值,然后去分配内存。如果成功,就返回地址比给调用者。 - 上一步失败,调用
gcForMalloc
,并且传入true参数,回收SoftReference引用的对象。 - GC完毕后,再次调用
dvmHeapSourceAllocAndGrow
方法进行内存分配。无论成功与否,就到此为止。
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。
- Image Space,用于存放一些预加载的类;
- Zygote Space,作用与Dalvik中的Zygote堆一样;
- Allocation Space,作用与Dalvik中的Active堆一样;
- LargeObject Space,一些离散地址的集合,用来分配一些大对象从而提高了GC的管理效率和整体性能。
LargeObject Space好处
小时候都玩过俄罗斯方块,为了存放更多的方块,需要合理安排好方块放置的位置和方块的方向。Java堆内存分配也类似,合理的安排好对象的地址空间,可以提高堆内存的使用率,从而减少GC。
Mark and Sweep算法-Dalvik.pngDalvik使用标记与清理算法,容易产生碎片。这时Active堆没有合理的位置和空间(当内存中有大量不连续的小内存段,图中红色)存放接下来的较大内存(就像俄罗斯方块游戏,无法将新的方块填入有空隙的行,只能新建一个行,这样使用率下降,需要去不断的GC,释放新的空间),再分配一个较大的对象时,例如decode一个图片,很容易导致GC(去清理这些碎片)。
LargeObject Space用途-ART.png由于ART有了LargeObject Space,大对象都分配到该区域,而Allocation Space分配较小的对象,从而使整个内存使用率更高(就像俄罗斯方块一样,内存形状固定,在Allocation Space中合理安排较小的对象,可以提升整块内存的覆盖率,也就是俄罗斯方块中的规则每一行都填满可以有更多的空间放更多的方块)。
上述图中波浪线表示程序代码,蓝色方块表示较大对象,红色表示小对象,绿色表示大对象。
GC类型
- kGcCauseForAlloc: 当要分配内存的时候发现内存不够的情况下引起的GC,这种情况下的GC会Stop World.
- kGcCauseBackground: 当内存达到一定的阀值的时候会去出发GC,这个时候是一个后台GC,不会引起Stop World.
- kGcCauseExplicit,显示调用的时候进行的gc,如果ART打开了这个选项的情况下,在system.gc的时候会进行GC.
- 其他更多。
stop world 是指在进行垃圾回收的时候,需要将所有正在执行的线程暂停(Stop World),保证GC线程的运行。
对象的分配和GC触发时机
这点和Dalvik虚拟机一样,可以回忆一下Dalvik知识点。
ART对象分配和GC触发时机.png