Android内存分配/回收的一个问题-为什么内存使用很少的时候也GC

简介: Android内存分配/回收的一个问题-为什么内存使用很少的时候也GC

Android应用建立在Java虚拟机之上的,Google为了保证同时多个APP运行并及时唤醒,就为每个虚拟机设置了最大可使用内存,通过adb命令可以查看相应的几个参数,


* [dalvik.vm.heapgrowthlimit]: [192m]
* [dalvik.vm.heapmaxfree]: [8m]
* [dalvik.vm.heapminfree]: [512k]
* [dalvik.vm.heapsize]: [512m]
* [dalvik.vm.heapstartsize]: [8m]
* [dalvik.vm.heaptargetutilization]: [0.75]

其中dalvik.vm.heapsize是最大可以使用的内存,这个数值同厂商跟版本都有关系,随着配置的提高,都在逐渐增大,既然虚拟机能使用的最大内存是dalvik.vm.heapsize,那么在申请内存的时候是不是一直到最大值才会GC呢?答案肯定是否定的,从我们检测的曲线来看,在内存使用很低的时候,也会GC,看下图APP运行时情况:


image.png

从上图看到,1,2,3这三个点好像是都发生了GC,但是这个时候,APP内存的占用并不是很高,距离最大内存还有很远,那么这个时候为什么会发生内存GC呢,其实直观上也比较好理解,如果一直等到最大内存才GC,那么就会有两个弊端:首先,内存资源浪费,造成系统性能降低,其次,GC时内存占用越大,耗时越长,应尽量避免。那GC的时机到底是什么时候呢?是不是每次内存块分配的时候都会GC,这个应该也是否定的,本文就来简单的了解下内存分配、GC、内存增长等机制。


Android Dalvik虚拟机分配及GC


首先看一下虚拟机的配置参数的意义,上面只讲述了dalvik.vm.heapstartsize,是最大内存申请尺寸,


  • dalvik.vm.heapgrowthlimit和dalvik.vm.heapsize都是java虚拟机的最大内存限制,一般heapgrowthlimit< heapsize,如果在Manifest中的application标签中声明android:largeHeap=“true”,APP直到heapsize才OOM,否则达到heapgrowthlimit就OOM
  • dalvik.vm.heapstartsize Java堆的起始大小,指定了Davlik虚拟机在启动的时候向系统申请的物理内存的大小,后面再根据需要逐渐向系统申请更多的物理内存,直到达到MAX
  • dalvik.vm.heapminfree 堆最小空闲值,GC后
  • dalvik.vm.heapmaxfree堆最大空闲值
  • dalvik.vm.heaptargetutilization 堆目标利用率


后面三个值用来确保每次GC之后Java堆已经使用和空闲的内存有一个合适的比例,这样可以尽量地减少GC的次数,堆的利用率为U,最小空闲值为MinFree字节,最大空闲值为MaxFree字节,假设在某一次GC之后,存活对象占用内存的大小为LiveSize。那么这时候堆的理想大小应该为(LiveSize / U)。但是(LiveSize / U)必须大于等于(LiveSize + MinFree)并且小于等于(LiveSize + MaxFree),否则,就要进行调整,调整的其实是软上限softLimit,

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,而200M很明显超过了150+8,那么这个时候,堆的尺寸就应该调整到158M,这个softLimit软上限也是下次申请内存时候是否需要GC的一个重要指标,请看以下场景:


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


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

image.png

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

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

image.png

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

由于当前的上限是158M,内存分配失败,需要先GC,GC之后调整softLimit,再次请求分配,如果还是失败,将softLimit调整为最大,再次请求分配,失败就再GC一次软引用,再次请求,还是失败那就是OOM,成功后要调整softLimit

image.png

所以,Android在申请内存的时候,可能先分配,也可能先GC,也可能不GC,这里面最关键的点就是内存利用率跟Free内存的上下限,下面简单看源码了解下堆内存分配流程:

   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 {
                  //false 弱引用
          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引用,-->
                //true 软引用
        gcForMalloc(true);
        <!--再次请求分配,如果还是失败,那就OOM了-->
        ptr = dvmHeapSourceAllocAndGrow(size);
        if (ptr != NULL) {
            return ptr;
        }
        dvmDumpThread(dvmThreadSelf(), false);          return NULL;  
        }

完整版代码 heap.cpp

    static void *tryMalloc(size_t size)
    {
        void *ptr;
    //TODO: figure out better heuristics
    //    There will be a lot of churn if someone allocates a bunch of
    //    big objects in a row, and we hit the frag case each time.
    //    A full GC for each.
    //    Maybe we grow the heap in bigger leaps
    //    Maybe we skip the GC if the size is large and we did one recently
    //      (number of allocations ago) (watch for thread effects)
    //    DeflateTest allocs a bunch of ~128k buffers w/in 0-5 allocs of each other
    //      (or, at least, there are only 0-5 objects swept each time)
          <!--1 首次请求分配内存-->
        ptr = dvmHeapSourceAlloc(size);
        if (ptr != NULL) {
            return ptr;
        }
        /*
         * The allocation failed.  If the GC is running, block until it
         * completes and retry.
         */
         <!--2 分配失败,GC--> 
        if (gDvm.gcHeap->gcRunning) {
            /*
             * The GC is concurrently tracing the heap.  Release the heap
             * lock, wait for the GC to complete, and retrying allocating.
             */
            dvmWaitForConcurrentGcToComplete();
        } else {
          /*
           * Try a foreground GC since a concurrent GC is not currently running.
           */
          //false 弱引用
          gcForMalloc(false);
        }
        ptr = dvmHeapSourceAlloc(size);
        if (ptr != NULL) {
            return ptr;
        }
        /* Even that didn't work;  this is an exceptional state.
         * Try harder, growing the heap if necessary.
         */
        ptr = dvmHeapSourceAllocAndGrow(size);
        if (ptr != NULL) {
            size_t newHeapSize;
             <!--分配成功后要调整softLimit-->
            newHeapSize = dvmHeapSourceGetIdealFootprint();
    //TODO: may want to grow a little bit more so that the amount of free
    //      space is equal to the old free space + the utilization slop for
    //      the new allocation.
            LOGI_HEAP("Grow heap (frag case) to "
                    "%zu.%03zuMB for %zu-byte allocation",
                    FRACTIONAL_MB(newHeapSize), size);
            return ptr;
        }
        /* Most allocations should have succeeded by now, so the heap
         * is really full, really fragmented, or the requested size is
         * really big.  Do another GC, collecting SoftReferences this
         * time.  The VM spec requires that all SoftReferences have
         * been collected and cleared before throwing an OOME.
         */
    //TODO: wait for the finalizers from the previous GC to finish
        LOGI_HEAP("Forcing collection of SoftReferences for %zu-byte allocation",
                size);
        <!--还是分配失败,GC力加强,回收soft引用,-->
         //true 软引用
        gcForMalloc(true);
        ptr = dvmHeapSourceAllocAndGrow(size);
        if (ptr != NULL) {
            return ptr;
        }
    //TODO: maybe wait for finalizers and try one last time
        LOGE_HEAP("Out of memory on a %zd-byte allocation.", size);
    //TODO: tell the HeapSource to dump its state
        dvmDumpThread(dvmThreadSelf(), false);
        return NULL;
    }


总结


本文主要说的一个问题就是,为什么不等到最大内存在GC,以及普通GC的可能时机,当然,对于内存的GC是更加复杂的,不在本文的讨论范围之内,同时这个也解释频繁的分配大内存会导致GC抖动的原因,毕竟,如果你超过了maxFree ,就一定GC,有兴趣可以自行深入分析。


目录
相关文章
|
8天前
|
Java 测试技术 Android开发
Android性能测试——发现和定位内存泄露和卡顿
本文详细介绍了Android应用性能测试中的内存泄漏与卡顿问题及其解决方案。首先,文章描述了使用MAT工具定位内存泄漏的具体步骤,并通过实例展示了如何分析Histogram图表和Dominator Tree。接着,针对卡顿问题,文章探讨了其产生原因,并提供了多种测试方法,包括GPU呈现模式分析、FPS Meter软件测试、绘制圆点计数法及Android Studio自带的GPU监控功能。最后,文章给出了排查卡顿问题的四个方向,帮助开发者优化应用性能。
36 4
Android性能测试——发现和定位内存泄露和卡顿
|
22天前
|
存储 NoSQL 算法
Redis内存回收
Redis 基于内存存储,性能卓越,但单节点内存不宜过大,以免影响持久化或主从同步。可通过配置 `maxmemory` 限制最大内存。内存达到上限时,Redis采用两种策略:内存过期策略和内存淘汰策略。过期策略包括惰性删除和周期删除,后者分为 SLOW 和 FAST 模式。内存淘汰策略有八种,如 LRU、LFU 和随机淘汰等,用于在内存不足时释放空间。官方推荐使用 LFU 算法。
Redis内存回收
|
28天前
|
JavaScript 前端开发 算法
js 内存回收机制
【8月更文挑战第23天】js 内存回收机制
30 3
|
6天前
|
监控 算法 数据可视化
深入解析Android应用开发中的高效内存管理策略在移动应用开发领域,Android平台因其开放性和灵活性备受开发者青睐。然而,随之而来的是内存管理的复杂性,这对开发者提出了更高的要求。高效的内存管理不仅能够提升应用的性能,还能有效避免因内存泄漏导致的应用崩溃。本文将探讨Android应用开发中的内存管理问题,并提供一系列实用的优化策略,帮助开发者打造更稳定、更高效的应用。
在Android开发中,内存管理是一个绕不开的话题。良好的内存管理机制不仅可以提高应用的运行效率,还能有效预防内存泄漏和过度消耗,从而延长电池寿命并提升用户体验。本文从Android内存管理的基本原理出发,详细讨论了几种常见的内存管理技巧,包括内存泄漏的检测与修复、内存分配与回收的优化方法,以及如何通过合理的编程习惯减少内存开销。通过对这些内容的阐述,旨在为Android开发者提供一套系统化的内存优化指南,助力开发出更加流畅稳定的应用。
17 0
|
17天前
|
数据安全/隐私保护 虚拟化
基于DAMON的内存能回收 【ChatGPT】
基于DAMON的内存能回收 【ChatGPT】
|
1月前
|
编解码 Android开发 UED
【性能狂飙!】揭秘Android应用极速变身秘籍:内存瘦身+用户体验升级,打造丝滑流畅新境界!
【8月更文挑战第12天】构建高效Android应用需全方位优化,尤其重视内存管理和用户体验。通过弱引用降低内存占用,懒加载资源减少启动负担。运用Kotlin协程确保UI流畅不阻塞,响应式设计适配多屏需求。这些策略共同提升了应用性能与用户满意度。
44 1
|
2月前
|
运维 Java Linux
(九)JVM成神路之性能调优、GC调试、各内存区、Linux参数大全及实用小技巧
本章节主要用于补齐之前GC篇章以及JVM运行时数据区的一些JVM参数,更多的作用也可以看作是JVM的参数列表大全。对于开发者而言,能够控制JVM的部分也就只有启动参数了,同时,对于JVM的性能调优而言,JVM的参数也是基础。
|
25天前
|
缓存 监控 Android开发
构建高效的Android应用:从内存优化到用户体验
【7月更文挑战第57天】 在竞争激烈的移动市场中,一个高效、流畅且具有优秀用户体验的Android应用是成功的关键。本文将深入探讨如何通过内存管理和界面优化来提升应用性能,包括实用的编程技巧和策略,以及如何利用Android系统提供的工具进行调试和性能监控。读者将学习到如何识别和解决常见的性能瓶颈,以及如何设计出既美观又实用的用户界面。
|
1月前
|
存储 Java 图形学
UNITY性能优化☀️一、GC介绍与Unity内存管理方法
UNITY性能优化☀️一、GC介绍与Unity内存管理方法