美文网首页
虚拟机中内存管理

虚拟机中内存管理

作者: gbmaotai | 来源:发表于2019-01-28 11:32 被阅读0次

什么是Heap(堆)

在Dalvik虚拟机中,堆实际上就是一块匿名共享内存. 一个mspace(libc中的概念),可以通过libc提供的函数create_mspace_with_base创建一个mspace,然后再通过mspace_开头的函数管理该mspace。例如,我们可以通过mspace_malloc和mspace_bulk_free来在指定的mspace中分配和释放内存。

函数dvmAllocRegion所做的事情就是调用函数ashmem_create_region来创建一块匿名共享内存,一段连续的内存,大小是gDvm.heapMaximumSize.

libc中的内存管理函数

dlmalloc使用了两套API.一套对应默认的mspace,以dl前缀开头,如dlmalloc, dlrealloc等.如果创建了自定义的mspace,则使用mspace开头的API,如mspace_malloc, mspace_realloc

/* create_mspace_with_base uses the memory supplied as the initial base
  of a new mspace. Part (less than 128*sizeof(size_t) bytes) of this
  space is used for bookkeeping, so the capacity must be at least this
  large. (Otherwise 0 is returned.) When this initial space is
  exhausted, additional memory will be obtained from the system.
  Destroying this space will deallocate all additionally allocated
  space (if possible) but not the initial base.*/
  
mspace create_mspace_with_base(void* base, size_t capacity, int locked);

/*
  malloc_footprint_limit();
  Returns the number of bytes that the heap is allowed to obtain from
  the system, returning the last value returned by
  malloc_set_footprint_limit, or the maximum size_t value if
  never set. The returned value reflects a permission. There is no
  guarantee that this number of bytes can actually be obtained from
  the system.  */
  size_t mspace_footprint_limit(mspace msp);
size_t mspace_set_footprint_limit(mspace msp, size_t bytes);
  
void *dvmAllocRegion(size_t byteCount, int prot, const char *name) {
    void *base;
    int fd, ret;
 
    byteCount = ALIGN_UP_TO_PAGE_SIZE(byteCount);
    fd = ashmem_create_region(name, byteCount);
    if (fd == -1) {
        return NULL;
    }
    base = mmap(NULL, byteCount, prot, MAP_PRIVATE, fd, 0);
    ret = close(fd);
    if (base == MAP_FAILED) {
        return NULL;
    }
    if (ret == -1) {
        munmap(base, byteCount);
        return NULL;
    }
    return base;
}

dvmHeapSourceStartup调用函数createMspace将前面得到的匿名共享内存块封装为一个mspace
mspace的起始大小为Java堆的起始大小。后面我们动态地调整这个mspace的大小,使得它可以使用更多的内存,但是不能超过Java堆的最大值。

static mspace createMspace(void* begin, size_t morecoreStart, size_t startingSize)
{
    // Clear errno to allow strerror on error.
    errno = 0;
    // Allow access to inital pages that will hold mspace.
    mprotect(begin, morecoreStart, PROT_READ | PROT_WRITE);
    // Create mspace using our backing storage starting at begin and with a footprint of
    // morecoreStart. Don't use an internal dlmalloc lock. When morecoreStart bytes of memory are
    // exhausted morecore will be called.
    mspace msp = create_mspace_with_base(begin, morecoreStart, false /*locked*/);
    if (msp != NULL) {
        // Do not allow morecore requests to succeed beyond the starting size of the heap.
        mspace_set_footprint_limit(msp, startingSize);
    } else {
        ALOGE("create_mspace_with_base failed %s", strerror(errno));
    }
    return msp;
}

堆的作用

虚拟机要解决的问题之一就是帮助应用程序自动分配和释放内存。
虚拟机在启动的时候向操作系统申请一大块内存当作对象堆。之后当应用程序创建对象时,虚拟机就会在堆上分配合适的内存块。而当对象不再使用时,虚拟机就会将它占用的内存块归还给堆。

Active堆和一个Zygote堆

应用程序进程是由Zygote进程fork出来的。

应用程序进程和Zygote进程共享了同一个用来分配对象的堆。然而,当Zygote进程或者应用程序进程对该堆进行写操作时,内核就会执行真正的拷贝操作,使得Zygote进程和应用程序进程分别拥有自己的一份拷贝。

不利因素:
拷贝是一件费时费力的事情。因此,为了尽量地避免拷贝,Dalvik虚拟机将自己的堆划分为两部分。

Dalvik虚拟机的堆最初是只有一个的。也就是Zygote进程在启动过程中创建Dalvik虚拟机的时候,只有一个堆。

函数addNewHeap所做的事情实际上就是将前面创建的Dalvik虚拟机Java堆一分为二,得到两个Heap。
heaps[1]就是Zygote堆,而heaps[0]就Active堆。以后无论是Zygote进程,还是Zygote子进程,需要分配对象时,都在Active堆上进行

作用:
这样就可以使得Zygote堆尽可能少地被执行写操作,因而就可以减少执行写时拷贝的操作。在Zygote堆里面分配的对象其实主要就是Zygote进程在启动过程中预加载的类、资源和对象了。这意味着这些预加载的类、资源和对象可以在Zygote进程和应用程序进程中做到长期共享。这样既能减少拷贝操作,还能减少对内存的需求。

struct HeapSource {
    ......
 //HEAP_SOURCE_MAX_HEAP_COUNT是一个宏,定义为2
 //Java堆最多可以划分为两个Heap,Active堆和Zygote堆。
    /* The heaps; heaps[0] is always the active heap,
     * which new objects should be allocated from.
     */
    Heap heaps[HEAP_SOURCE_MAX_HEAP_COUNT];
 
    /* The current number of heaps.
     */
    size_t numHeaps;
    ......
 
};

Zygote进程会通过调用函数forkAndSpecializeCommon来fork子进程,fork 之前会调用dvmHeapSourceStartupBeforeFork。
Zygote进程只会在fork第一个子进程的时候,才会将Java堆划一分为二来管理(newZygoteHeapAllocated,addNewHeap)

bool dvmHeapSourceStartupBeforeFork()
{
    HeapSource *hs = gHs; // use a local to avoid the implicit "volatile"
 
    HS_BOILERPLATE();
 
    assert(gDvm.zygote);
 
    if (!gDvm.newZygoteHeapAllocated) {
       /* Ensure heaps are trimmed to minimize footprint pre-fork.
        */
        trimHeaps();
        /* Create a new heap for post-fork zygote allocations.  We only
         * try once, even if it fails.
         */
        ALOGV("Splitting out new zygote heap");
        gDvm.newZygoteHeapAllocated = true;
        return addNewHeap(hs);
    }
    return true;
}

GC

Davlk虚拟机使用标记-清除(Mark-Sweep)算法进行GC

数据结构

Card Table是为了记录在垃圾收集过程中对象的引用情况的,以便可以实现Concurrent G

Live Heap Bitmap,用来记录上次GC之后,还存活的对象

Mark Heap Bitmap,用来记录当前GC中还存活的对象
上次GC后存活的但是当前GC不存活的对象,就是需要释放的对象

在标记阶段,通过一个Mark Stack来实现递归检查被引用的对象

bool dvmGcStartup()
{
    dvmInitMutex(&gDvm.gcHeapLock);
    pthread_cond_init(&gDvm.gcHeapCond, NULL);
    return dvmHeapStartup();
}
bool dvmHeapStartup()
{
    GcHeap *gcHeap;
 
    if (gDvm.heapGrowthLimit == 0) {
        gDvm.heapGrowthLimit = gDvm.heapMaximumSize;
    }
 
    gcHeap = dvmHeapSourceStartup(gDvm.heapStartingSize,
                                  gDvm.heapMaximumSize,
                                  gDvm.heapGrowthLimit);
    ......
 
    gDvm.gcHeap = gcHeap;
    ......
 
    if (!dvmCardTableStartup(gDvm.heapMaximumSize, gDvm.heapGrowthLimit)) {
        LOGE_HEAP("card table startup failed.");
        return false;
    }
 
    return true;
}

Heap 使用的内存

申请的物理内存的大小是heapStartingSize。后面再根据需要逐渐向系统申请更多的物理内存,直到达到最大值(Maximum Size)为止。

虚拟内存的总大小却是需要在Dalvik启动的时候就确定的。这个虚拟内存的大小就等于Java堆的最大值(Maximum Size)
是为了避免出现这种情况:
重新创建另外一块更大的虚拟内存。这样就需要将之前的虚拟内存的内容拷贝到新创建的更大的虚拟内存去,并且还要相应地修改各种辅助数据结构。效率低下。

dvmHeapSourceStartup(gDvm.heapStartingSize,
                                  gDvm.heapMaximumSize,
                                  gDvm.heapGrowthLimit);

内存碎片问题

利用C库里面的dlmalloc内存分配器来解决内存碎片问题

内存申请和内存不足的处理

    /* The desired max size of the heap source as a whole.
     */
    size_t idealSize;

    /* The maximum number of bytes allowed to be allocated from the
     * active heap before a GC is forced.  This is used to "shrink" the
     * heap in lieu of actual compaction.
     */
    size_t softLimit;
    //第一个元素描述的是Active堆,第二个元素描述的是Zygote堆
    /* The heaps; heaps[0] is always the active heap,
     * which new objects should be allocated from.
     */
    Heap heaps[HEAP_SOURCE_MAX_HEAP_COUNT];

    /* The current number of heaps.
     */
    size_t numHeaps;    
static void *tryMalloc(size_t size)
{
void *ptr;
<!--1 首次请求分配内存-->
ptr = dvmHeapSourceAlloc(size);
if (ptr != NULL) {
return ptr;
}
<!--2 分配失败,GC-->
if (gDvm.gcHeap->gcRunning) {
dvmWaitForConcurrentGcToComplete();
} else {
gcForMalloc(false);
}
<!--再次分配-->
ptr = dvmHeapSourceAlloc(size);
if (ptr != NULL) {
return ptr;
}
<!--还是分配失败,调整softLimit再次分配-->
ptr = dvmHeapSourceAllocAndGrow(size);
if (ptr != NULL) {
size_t newHeapSize;
<!--分配成功后要调整softLimit-->
newHeapSize = dvmHeapSourceGetIdealFootprint();
return ptr;
}
<!--还是分配失败,GC力加强,回收soft引用,-->
gcForMalloc(true);
<!--再次请求分配,如果还是失败,那就OOM了-->
ptr = dvmHeapSourceAllocAndGrow(size);
if (ptr != NULL) {
return ptr;
}
dvmDumpThread(dvmThreadSelf(), false); return NULL;
} 
  1. 调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存。如果分配成功,那么就将分配得到的地址直接返回给调用者了。函数dvmHeapSourceAlloc在不改变Java堆当前大小的前提下进行内存分配,这是属于轻量级的内存分配动作。
  2. 如果上一步内存分配失败,这时候就需要执行一次GC了。
  3. GC执行完毕后,再次调用函数dvmHeapSourceAlloc尝试轻量级的内存分配操作。如果分配成功,那么就将分配得到的地址直接返回给调用者了
  4. 如果上一步内存分配失败,这时候就得考虑先将Java堆的当前大小设置为Dalvik虚拟机启动时指定的Java堆最大值,再进行内存分配了。这是通过调用函数dvmHeapSourceAllocAndGrow来实现的。

GC的时机到底是什么时候呢?

如果一直等到最大内存才GC,那么就会有两个弊端:首先,内存资源浪费,造成系统性能降低,其次,GC时内存占用越大,耗时越长,应尽量避免.

GC_EXPLICIT

表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。

GC_FOR_ALLOC

内存分配时,发现可用内存不够时触发的GC事件

GC_CONCURRENT

计算已分配的大小达到阈值时会触发的GC事件。
堆的空闲内存(dalvik.vm.heapminfree)不足时,就会触发GC_CONCURRENT类型的GC。

dalvik与GC相关的属性有:

dalvik.vm.heapstartsize = 16m

表示应用程序启动后为其分配的堆初始大小为8m

dalvik.vm.heapgrowthlimit = 192m

极限堆大小 , 超过这个值会有OOM产生

dalvik.vm.heapsize = 512M

使用大堆时,极限堆大小。一旦dalvik heap size超过这个值,直接引发oom。在android开发中,如果要使用大堆,需要在manifest中指定android:largeHeap为true。这样dvm heap最大可达dalvik.vm.heapsize

dalvik.vm.heaptargetutilization: 0.75

可以设定内存利用率的百分比,当实际的利用率偏离这个百分比的时候,虚拟机会在GC的时候调整堆内存大小,让实际占用率向个百分比靠拢

dalvik.vm.heapminfree dalvik.vm.heapmaxfree

用来确保每次GC之后Java堆已经使用和空闲的内存有一个合适的比例

SoftLimit /Ideal Size

假设堆的利用率为U(0.75),最小空闲值为MinFree字节,最大空闲值为MaxFree字节,假设在某一次GC之后,存活对象占用内存的大小为LiveSize。那么这时候堆的理想大小应该为(LiveSize / U)。但是(LiveSize / U)必须大于等于(LiveSize + MinFree)并且小于等于(LiveSize + MaxFree),否则,就要进行调整,调整的其实是软上限softLimit(目标值),区间边界值做为目标值,这个目标值,当做当前允许总的可以分配到的内存。

理想大小(Ideal Size)如果此时只有一个堆,即只有Active堆没有Zygote堆,那么Soft Limit就等于Ideal Size。如果此时有两个堆,那么Ideal Size就等于Zygote堆的大小再加上Soft Limit值.

每一次GC之后,Dalvik虚拟机会根据Active堆已经分配的内存字节数、设定的堆目标利用率和Zygote堆的大小,重新计算Soft Limit

setIdealFootprint设置SoftLimit IdealSize

static void setIdealFootprint(size_t max)
{
    HS_BOILERPLATE();
 
    HeapSource *hs = gHs;
    size_t maximumSize = getMaximumSize(hs);
    if (max > maximumSize) {
        LOGI_HEAP("Clamp target GC heap from %zd.%03zdMB to %u.%03uMB",
                FRACTIONAL_MB(max),
                FRACTIONAL_MB(maximumSize));
        max = maximumSize;
    }
 
    /* Convert max into a size that applies to the active heap.
     * Old heaps will count against the ideal size.
     */
//getSoftFootprint 当参数includeActive等于true时,就表示要返回的是Zygote堆的大小再加上Active堆当前已经分配的内存字节数的值。而当参数includeActive等于false时,要返回的仅仅是Zygote堆的大小.
    size_t overhead = getSoftFootprint(false);
    size_t activeMax;
    if (overhead < max) {
        activeMax = max - overhead;
    } else {
        activeMax = 0;
    }
 //函数setIdealFootprint调用函数setSoftLimit设置Active堆的当前大小,并且将Zygote堆和Active堆的大小之和记录在hs->idealSize中
 
    setSoftLimit(hs, activeMax);
    hs->idealSize = max;
}

GC_CONCURRENT事件的阈值

同时根据目标值减去固定值(200~500K),当做触发GC_CONCURRENT事件的阈值
当下一次分配内存,分配成功时。重新计算已分配的内存大小;若有达到GC_CONCURRENT的阈值,则产生GC。

GC_FOR_ALLOC事件

当下一次分配内存,开始分配失败时。则会产生GC_FOR_ALLOC事件,释放内存;然后再尝试分配。

heap 的扩张

dvmHeapSourceAllocAndGrow

static size_t getUtilizationTarget(const HeapSource* hs, size_t liveSize)
{
size_t targetSize = (liveSize / hs->targetUtilization) * HEAP_UTILIZATION_MAX;
if (targetSize > liveSize + hs->maxFree) {
targetSize = liveSize + hs->maxFree;
} else if (targetSize < liveSize + hs->minFree) {
targetSize = liveSize + hs->minFree;
}
return targetSize;
} 

举例:
假设liveSize = 150M,targetUtilization=0.75,maxFree=8,minFree=512k,那么理想尺寸200M,
由于targetSize(200M)> liveSize(150M)+hs->maxFree(8M),所以调整
targetSize = 150+8
(取区间边界值做为目标值)

举例:
场景一:当前softLimit=158M,liveSize = 150M,如果这个时候,需要分配一个100K内存的对象

由于当前的上限是158M,内存是可以直接分配成功的,分配之后,由于空闲内存8-100K>512k,也不需要调整内存,这个时候,不需要GC

场景二:当前softLimit=158M,liveSize = 150M,如果这个时候,需要分配的内存是7.7M

由于当前的上限是158M,内存是可以直接分配成功的,分配之后,由于空闲内存8-7.7M < 512k,那就需要GC,同时调整softLimit,

函数dvmHeapSourceAlloc是不允许增长Active堆的大小的

void* dvmHeapSourceAlloc(size_t n)
{
    HS_BOILERPLATE();
 
    HeapSource *hs = gHs;
    Heap* heap = hs2heap(hs);
    if (heap->bytesAllocated + n > hs->softLimit) {
        ......
        return NULL;
    }
    void* ptr;
    if (gDvm.lowMemoryMode) {
        ......
        ptr = mspace_malloc(heap->msp, n);
        if (ptr == NULL) {
            return NULL;
        }
        uintptr_t zero_begin = (uintptr_t)ptr;
        uintptr_t zero_end = (uintptr_t)ptr + n;
        ......
        uintptr_t begin = ALIGN_UP_TO_PAGE_SIZE(zero_begin);
        uintptr_t end = zero_end & ~(uintptr_t)(SYSTEM_PAGE_SIZE - 1);
        ......
        if (begin < end) {
            ......
            madvise((void*)begin, end - begin, MADV_DONTNEED);
            ......
            memset((void*)end, 0, zero_end - end);
            ......
            zero_end = begin;
        }
        memset((void*)zero_begin, 0, zero_end - zero_begin);
    } else {
        ptr = mspace_calloc(heap->msp, 1, n);
        if (ptr == NULL) {
            return NULL;
        }
    }
 
    countAllocation(heap, ptr);
    ......
    if (gDvm.gcHeap->gcRunning || !hs->hasGcThread) {
        ......
        return ptr;
    }
    if (heap->bytesAllocated > heap->concurrentStartBytes) {
        ......
        dvmSignalCond(&gHs->gcThreadCond);
    }
    return ptr;
}

snapIdealFootprint来修改Active堆的大小

void* dvmHeapSourceAllocAndGrow(size_t n)
{
    HS_BOILERPLATE();

    HeapSource *hs = gHs;
    Heap* heap = hs2heap(hs);
    //是否超过了softLimit
    void* ptr = dvmHeapSourceAlloc(n);
    if (ptr != NULL) {
        return ptr;
    }
//Zygote堆和Active堆的大小之和oldIdealSize。这是因为后面我们可能会修改Active堆的大小。当修改了Active堆的大小,但是仍然不能成功分配大小为n的内存,那么就需要恢复之前Zygote堆和Active堆的大小

    size_t oldIdealSize = hs->idealSize;
    if (isSoftLimited(hs)) {
        /* We're soft-limited.  Try removing the soft limit to
         * see if we can allocate without actually growing.
         */
        hs->softLimit = SIZE_MAX;
        ptr = dvmHeapSourceAlloc(n);
        if (ptr != NULL) {
            /* Removing the soft limit worked;  fix things up to
             * reflect the new effective ideal size.
             */
             //调用函数snapIdealFootprint来修改Active堆的大小
            snapIdealFootprint();
            return ptr;
        }
        // softLimit intentionally left at SIZE_MAX.
    }

    /* We're not soft-limited.  Grow the heap to satisfy the request.
     * If this call fails, no footprints will have changed.
     */
    ptr = heapAllocAndGrow(hs, heap, n);
    if (ptr != NULL) {
        /* The allocation succeeded.  Fix up the ideal size to
         * reflect any footprint modifications that had to happen.
         */
        snapIdealFootprint();
    } else {
        /* We just couldn't do it.  Restore the original ideal size,
         * fixing up softLimit if necessary.
         */
        setIdealFootprint(oldIdealSize);
    }
    return ptr;
}

void dvmCollectGarbageInternal(const GcSpec* spec)
GC 的时候会调用dvmHeapSourceGrowForUtilization

/*
 * 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;
}

相关文章

网友评论

      本文标题:虚拟机中内存管理

      本文链接:https://www.haomeiwen.com/subject/fhqajqtx.html