1概要
Dalivik虚拟机有两个堆,包括zygote堆和Active 堆,前者是不太变化的,是从父进程公用的相同物理内存的堆,因为很少发生写操作在这个堆上,所以写时复制机制不会发生在这个堆上,这也是android运行时比较巧妙的地方。
Live Heap Bitmap 和 Mark Heap Bitmap 这两个位图是是java垃圾回收机制的关键,用来标记java对象是否被引用的, 可以映射最大堆大小的所有对象。 live heap bitmap标记表示上次垃圾回收后存活的对象,Mark heap bitmap 标记本次垃圾回被引用的对象。有了这两个信息我们就知道哪些对象是需要回收的,即在live bitmap标记为1,但在mark bitmap标记为0的对象。 (为什么需要两个bitmap呢,因为mark bitmap被标记为0的对象有可能是不存在的对象,也有可能是要回收的对象,有了live bitmap就可以进行区分)
Mark Stack就是标记过程中对象的引用关系是错综复杂的,也会有循环引用,Mark stack通过栈的方式解决这个问题。(思路就是保证优先检查地址小的对象)
如何标记对象: 首先java虚拟机使用标记清除算法,而不是引用计数方法。 这里引入根级对象概念, 根级对象是指在gc的瞬间,被全局变量,栈变量和寄存器引用的对象,试想 顺着这些对象标记下去的对象就是全部被使用的对象,不被根级对象引用的对象其实就是没有的。 这里补充下引用计数存在的问题。 比如A,B两个对象互相引用,他们的引用计数都是1,但是不被根级对象引用,这样他们俩也是无用的对象,如存在引用计数。
Card Table : 在并行gc的过程中使用, 并行gc的过程分为3步
1 挂起所有java线程,gc线程确定根级别对象。
2 唤醒java线程,同时间gc线程标记被根级对象引用的对象,但是在这个过程中java线程有可能创建或者修改对象关系,这个变化的对象地址要记录在card table中
3 gc线程标记完所有对象的时候,挂起所有java进程,再从card table中找到在过程2中发生变化的对象(少数对象)进行重新标记
所以 Card Tab对象的作用就是用于记录并行gc中发生变化的对象地址
2 Dalvik 虚拟机相关启动参数和含义
-xms: 虚拟机堆的起始大小,启动时申请的物理内存
-xms: 虚拟机堆使用的虚拟内存最大大小
-XX:HeapGrowthLimit: 减少内存碎片的方式
-XX:HeapMinFree 堆最小空闲值,空闲值小于该值时应该扩容堆(调整软限制)
-XX:HeapMaxFree 堆最大空闲值,大于该值时应该堆缩容(调整软限制)
-XX:HeapTargetUtilization 目标利用率,扩容和缩容的根据目标利用率找到合适的目标堆大小(调整软限制)
-XX:+DisableExplicitGC 禁止显式gc
堆最小空闲值(Min Free)、堆最大空闲值(Max Free)和堆目标利用率(Target Utilization)。这三个值可以分别通过Dalvik虚拟机的启动选项-XX:HeapMinFree、-XX:HeapMaxFree和-XX:HeapTargetUtilization来指定。它们用来确保每次GC之后,Java堆已经使用和空闲的内存有一个合适的比例,这样可以尽量地减少GC的次数。举个例子说,堆的利用率为U,最小空闲值为MinFree字节,最大空闲值为MaxFree字节。假设在某一次GC之后,存活对象占用内存的大小为LiveSize。那么这时候堆的理想大小应该为(LiveSize / U)。但是(LiveSize / U)必须大于等于(LiveSize + MinFree)并且小于等于(LiveSize + MaxFree)。
3 创建对象的内存申请
static void *tryMalloc(size_t size)
{
void *ptr;
......
ptr = dvmHeapSourceAlloc(size);
if (ptr != NULL) {
return ptr;
}
if (gDvm.gcHeap->gcRunning) {
......
dvmWaitForConcurrentGcToComplete();
} else {
......
gcForMalloc(false);
}
ptr = dvmHeapSourceAlloc(size);
if (ptr != NULL) {
return ptr;
}
ptr = dvmHeapSourceAllocAndGrow(size);
if (ptr != NULL) {
......
return ptr;
}
gcForMalloc(true);
ptr = dvmHeapSourceAllocAndGrow(size);
if (ptr != NULL) {
return ptr;
}
......
return NULL;
}
1. 调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存。如果分配成功,那么就将分配得到的地址直接返回给调用者了。函数dvmHeapSourceAlloc在不改变Java堆当前大小的前提下进行内存分配,这是属于轻量级的内存分配动作。
2. 如果上一步内存分配失败,这时候就需要执行一次GC了。不过如果GC线程已经在运行中,即gDvm.gcHeap->gcRunning的值等于true,那么就直接调用函数dvmWaitForConcurrentGcToComplete等到GC执行完成就是了。否则的话,就需要调用函数gcForMalloc来执行一次GC了,参数false表示不要回收**全部**软引用对象引用的对象。
3. GC执行完毕后,再次调用函数dvmHeapSourceAlloc尝试轻量级的内存分配操作。如果分配成功,那么就将分配得到的地址直接返回给调用者了。
4. 如果上一步内存分配失败,这时候就得考虑先将Java堆的当前大小设置为Dalvik虚拟机启动时指定的Java堆最大值,再进行内存分配了。这是通过调用函数dvmHeapSourceAllocAndGrow来实现的。
5. 如果调用函数dvmHeapSourceAllocAndGrow分配内存成功,则直接将分配得到的地址直接返回给调用者了。
6. 如果上一步内存分配还是失败,这时候就得出狠招了。再次调用函数gcForMalloc来执行GC。参数true表示要回收**全部**软引用对象引用的对象。
7. GC执行完毕,再次调用函数dvmHeapSourceAllocAndGrow进行内存分配。这是最后一次努力了,成功与事都到此为止。
4 GC的类型
GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。
GC_CONCURRENT: 表示是在已分配内存达到一定量之后触发的GC。
GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。
GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。
实际上,GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三种类型的GC都是在分配对象的过程触发的。
GC_FOR_MALLOC | GC_BEFORE_OOM 就是在我们前面看到的申请对象过程中gcForMalloc(bool soft) 函数触发的gc
static void gcForMalloc(bool clearSoftReferences)
{
const GcSpec *spec = clearSoftReferences ? GC_BEFORE_OOM : GC_FOR_MALLOC;
dvmCollectGarbageInternal(spec);
}
GC_CONCURRENT 在每次对象申请成功后都会做检查,代码如下
if (heap->bytesAllocated > heap->concurrentStartBytes) {
dvmSignalCond(&gHs->gcThreadCond);
}
GC_EXPLICIT 是明确的要进行gc
void dvmCollectGarbage()
{
if (gDvm.disableExplicitGc) {
return;
}
dvmLockHeap();
dvmWaitForConcurrentGcToComplete();
dvmCollectGarbageInternal(GC_EXPLICIT);
dvmUnlockHeap();
}
从函数可以看到,GC_EXPLICIT并不一定会被执行,可以通过-XX:+DisableExplicitGC参数设置disableExplicitGc变量禁止显方gc
另外还要等待并行gc完成,所以调用System.gc 执行gc的时机是不确定的
5 GC的主体流程
static void *gcDaemonThread(void* arg)
{
dvmChangeStatus(NULL, THREAD_VMWAIT);
dvmLockMutex(&gHs->gcThreadMutex);
while (gHs->gcThreadShutdown != true) {
bool trim = false;
if (gHs->gcThreadTrimNeeded) {
int result = dvmRelativeCondWait(&gHs->gcThreadCond, &gHs->gcThreadMutex,
HEAP_TRIM_IDLE_TIME_MS, 0);
if (result == ETIMEDOUT) {
/* Timed out waiting for a GC request, schedule a heap trim. */
trim = true;
}
} else {
dvmWaitCond(&gHs->gcThreadCond, &gHs->gcThreadMutex);
}
......
dvmLockHeap();
if (!gDvm.gcHeap->gcRunning) {
dvmChangeStatus(NULL, THREAD_RUNNING);
if (trim) {
trimHeaps();
gHs->gcThreadTrimNeeded = false;
} else {
dvmCollectGarbageInternal(GC_CONCURRENT);
gHs->gcThreadTrimNeeded = true;
}
dvmChangeStatus(NULL, THREAD_VMWAIT);
}
dvmUnlockHeap();
}
dvmChangeStatus(NULL, THREAD_RUNNING);
return NULL;
}
该函数运行在gc进程中
执行完并行gc后会设置gcThreadTrimNeeded 为真,那么过HEAP_TRIM_IDLE_TIME_MS 后会执行trimHeaps 然后再设置gcThreadTrimNeeded为flase后才能再次执行并行gc,所以并行gc和trimHeaps是交替执行的, trimHeaps就是请求操作系统释放一些物理内存。
6 GC的详细过程
上图就是所谓的标记清除机制,分别沿着并行gc和串行gc的两条路径。
主要实现的功能有三点
1 标记引用对象,回收内存
2 执行 强引用,弱引用,影子引用的回收,在异步线程执行重写了finalizer方法同时要被回收对象的finalizer方法
3 根据目标利用率调整堆大小,和设置heap->concurrentStartBytes阈值
这里还有一点需要注意
整个过程中 Lock Heap 后由于heap被锁定,其他要分配内存的线程不能获取该锁,就不能分配内存,所以在某些阶段是不能在堆上创建新对象的。
Suspend AllThread 的目的是不允许线程改变栈贞,虽然Lock heap锁定了内存分配,但是栈贞的变化还是会影响根对象集。 当要挂起一个线程的时候会给该线程的suspendCount计数+1,线程运行过程中会在关键点检查自己的计数是否>0,如果大于0则挂起自己。
实际上就是碰到IF指令、GOTO指令、SWITCH指令、RETURN指令和THROW指令等时。
所以gc过程中线程挂起是有两种原因的 1 等锁 2 检查点挂起
对于软引用,弱引用,影子引用和finalizer 对象的收集,是放在各自的堆队列中
static void delayReferenceReferent(Object *obj, GcMarkContext *ctx)
{
......
GcHeap *gcHeap = gDvm.gcHeap;
size_t pendingNextOffset = gDvm.offJavaLangRefReference_pendingNext;
size_t referentOffset = gDvm.offJavaLangRefReference_referent;
Object *pending = dvmGetFieldObject(obj, pendingNextOffset);
Object *referent = dvmGetFieldObject(obj, referentOffset);
if (pending == NULL && referent != NULL && !isMarked(referent, ctx)) {
Object **list = NULL;
if (isSoftReference(obj)) {
list = &gcHeap->softReferences;
} else if (isWeakReference(obj)) {
list = &gcHeap->weakReferences;
} else if (isFinalizerReference(obj)) {
list = &gcHeap->finalizerReferences;
} else if (isPhantomReference(obj)) {
list = &gcHeap->phantomReferences;
}
......
enqueuePendingReference(obj, list);
}
}
引用对象回收的过程
void dvmHeapProcessReferences(Object **softReferences, bool clearSoftRefs,
Object **weakReferences,
Object **finalizerReferences,
Object **phantomReferences)
{
assert(softReferences != NULL);
assert(weakReferences != NULL);
assert(finalizerReferences != NULL);
assert(phantomReferences != NULL);
/*
* Unless we are in the zygote or required to clear soft
* references with white references, preserve some white
* referents.
*/
if (!gDvm.zygote && !clearSoftRefs) {
preserveSomeSoftReferences(softReferences);
}
/*
* Clear all remaining soft and weak references with white
* referents.
*/
clearWhiteReferences(softReferences);
clearWhiteReferences(weakReferences);
/*
* Preserve all white objects with finalize methods and schedule
* them for finalization.
*/
enqueueFinalizerReferences(finalizerReferences);
/*
* Clear all f-reachable soft and weak references with white
* referents.
*/
clearWhiteReferences(softReferences);
clearWhiteReferences(weakReferences);
/*
* Clear all phantom references with white referents.
*/
clearWhiteReferences(phantomReferences);
/*
* At this point all reference lists should be empty.
*/
assert(*softReferences == NULL);
assert(*weakReferences == NULL);
assert(*finalizerReferences == NULL);
assert(*phantomReferences == NULL);
}
preserveSomeSoftReferences 函数值得注意的是,软引用并非全部要到内存紧张的时候回收,preserveSomeSoftReferences会把一般的软引用救活,所以每次gc都会回收一半。 这才是软引用的语义。
最后设置目标利用率的函数
/*
* Given the size of a live set, returns the ideal heap size given
* the current target utilization and MIN/MAX values.
*/
static size_t getUtilizationTarget(const HeapSource* hs, size_t liveSize)
{
/* Use the current target utilization ratio to determine the
* ideal heap size based on the size of the live set.
*/
size_t targetSize = (liveSize / hs->targetUtilization) * HEAP_UTILIZATION_MAX;
/* Cap the amount of free space, though, so we don't end up
* with, e.g., 8MB of free space when the live set size hits 8MB.
*/
if (targetSize > liveSize + hs->maxFree) {
targetSize = liveSize + hs->maxFree;
} else if (targetSize < liveSize + hs->minFree) {
targetSize = liveSize + hs->minFree;
}
return targetSize;
}
和我们所说的两个虚拟机启动参数是一样的,设置的是申请内存时候的软限制
在回头看tryMalloc(size_t size)函数,每次调用dvmHeapSourceAlloc的时候都首先判断
if (heap->bytesAllocated + n > hs->softLimit) {
......
return NULL;
}
这样当快要达到软限制的时候先执行一次gc,再进行内存分配,这样实在不能分配后,才突破软限制分配对象。 这就有效的控制内存增长,和提前进行gc。
接下来就是heap->concurrentStartBytes 阈值的设计
/*
* Given the current contents of the active heap, increase the allowed
* heap footprint to match the target utilization ratio. This
* should only be called immediately after a full mark/sweep.
*/
void dvmHeapSourceGrowForUtilization()
{
HS_BOILERPLATE();
HeapSource *hs = gHs;
Heap* heap = hs2heap(hs);
/* Use the current target utilization ratio to determine the
* ideal heap size based on the size of the live set.
* Note that only the active heap plays any part in this.
*
* Avoid letting the old heaps influence the target free size,
* because they may be full of objects that aren't actually
* in the working set. Just look at the allocated size of
* the current heap.
*/
size_t currentHeapUsed = heap->bytesAllocated;
size_t targetHeapSize = getUtilizationTarget(hs, currentHeapUsed);
/* The ideal size includes the old heaps; add overhead so that
* it can be immediately subtracted again in setIdealFootprint().
* If the target heap size would exceed the max, setIdealFootprint()
* will clamp it to a legal value.
*/
size_t overhead = getSoftFootprint(false);
setIdealFootprint(targetHeapSize + overhead);
size_t freeBytes = getAllocLimit(hs);
if (freeBytes < CONCURRENT_MIN_FREE) {
/* Not enough free memory to allow a concurrent GC. */
heap->concurrentStartBytes = SIZE_MAX;
} else {
heap->concurrentStartBytes = freeBytes - CONCURRENT_START;
}
/* Mark that we need to run finalizers and update the native watermarks
* next time we attempt to register a native allocation.
*/
gHs->nativeNeedToRunFinalization = true;
}
/* Start a concurrent collection when free memory falls under this
* many bytes.
*/
#define CONCURRENT_START (128 << 10)
/* The next GC will not be concurrent when free memory after a GC is
* under this many bytes.
*/
#define CONCURRENT_MIN_FREE (CONCURRENT_START + (128 << 10))
getAllocLimit 实际上返回的是softLImit,
当空闲内存<CONCURRENT_MIN_FREE(256k) 的时候禁止并行gc,也就是堆还很小的时候, 当堆距离软限制<CONCURRENT_START(128k)的时候开始执行并行gc 。