美文网首页Android进阶
(过时、作废)android 多线程 — GC

(过时、作废)android 多线程 — GC

作者: 前行的乌龟 | 来源:发表于2019-03-17 22:35 被阅读0次

    简单解析下JVM

    先说下 JVM,虽然上篇文章在讲内存时介绍了 JVM ,但是我在这里还是以 JVM 开头,JVM 管理着 GC ,GC 是 JVM 要完成任务的一部分,和内存模型紧密相联

    JVM - java 语言解释器,用于把 java 特有的 class 文件解释成机器可以运行的机器码

    JVM 我们得拆开看,J - java 语言;VM 表示虚拟机,虚拟的计算机。JVM 合起来就是表示:所有遵守相同规范,能解释执行 java class 文件的都叫 JVM

    这里要说明一下,是机器码不是 C ,机器执行的永远是机器码,没有别的,是 0 和 1,为啥有的人会认为是 C 呢,因为 C 可以直接编译成机器码,所以不像 java 语言还需要 JVM 虚拟机的解释, 同样 Flutter 采用的 Dart 语言同样有 Dart 的 VM

    JVM 严格意义上说是一种规范,所有符合 JVM 规范的都叫 JVM ,android 采用的是自己的虚拟机,Dalvik 和 ART,他们其实都不算是 JVM,想成为 JVM 必须要通过 JCK(Java Compliance Kit)的测试并获得授权后才能行,所以严格来说 android 的 Dalvik VM 不能叫做 JVM,因为没授权

    Dalvik VM 和 JVM 最大的却别是中间多了 dex 文件,JVM 运行class 文件,而 Dalvik 会把 class 转换成 dex 文件去执行


    Dalvik VM 是基于寄存器的架构(reg based),而 JVM 是堆栈结构(stack based),这里的寄存器架构和堆栈结构指的是计算机指令系统

    计算机指令系统分为四种:

    x86 一开始并没有使用太多的通用寄存器,原因之一(注意,只是之一)是当时的编译器无力进行寄存器分配,让编译器自动决定程序中众多变量哪些应该装入寄存器、哪些应该换出、哪些变量应该映射到同一个寄存器上,并不是一件易事,JVM 采用堆栈结构的原因之一就是不信任编译器的寄存器分配能力,转而使用堆栈结构,躲开寄存器分配的难题

    如今的CPU早就有足够的晶体管来支持复杂设计,为了性能着想,大量使用寄存器型的指令,原因在于寄存器离CPU最近,所以延时最短,取指最快,有利于主频提高

    那么基于栈与基于寄存器的架构,谁更快呢?intel的X86还保留有累加器指令和堆栈型指令,这是为了历史兼容。很多现今的处理器,除了load和store指令访存外,只支持对寄存器操作,不支持对堆栈以及内存的直接操作——这也从侧面反映出基于寄存器比基于栈的架构更与实际的处理器接近

    dvm 速度快!寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。JAVA虚拟机基于栈结构,程序在运行时虚拟机需要频繁的从栈上读取写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费很多CPU时间

    指令数小!dvm基于寄存器,所以它的指令是二地址和三地址混合,指令中指明了操作数的地址;jvm基于栈,它的指令是零地址,指令的操作数对象默认是操作数栈中的几个位置。这样带来的结果就是dvm的指令数相对于jvm的指令数会小很多,jvm需要多条指令而dvm可能只需要一条指令


    进程的区别

    Android 中的进程主要分为 native 进程和 Java 进程

    • native 进程 - 指的是采用C/C++实现的,不包括 Dalvik 实例的 Linux 进程
    • java进程 - 实例化了 Dalvik 虚拟机的 Linux 进程,我们开发的 APP 就是出于 java 进程中的

    我们使用 malloc、C++ new 和j ava new 所申请的空间都是heap空间,只不过 C/C++申请的内存空间在 native heap 中,而 java 申请的内存空间则在 dalvik heap 中。在平时的开发中,我们打交道的最多的就是 dalvik heap,我们的实例域、静态域、数组元素等都是在d alvik 的 heap 中,虚拟机的 GC 也发生在其中


    GC 原理

    GC 就是 JVM 的内存回收机制,GC 回收的是 堆内存,注意好啊是 堆内存

    目前有 4 种垃圾手机算法:

    当前的商业虚拟机的垃圾收集器都采用“分代收集”算法,这种算法并没有什么新的思想,只是根据对象的存活的周期不同将内存划分为几块,分代收集算法是复制算法和标记整理算法这两种算法情况的互补

    GC 把堆内存分为2部分:新生代 | 老年代 ,听起来有点像古生物研究呀 ~

    堆内存
    • 新生代

      • 作用对象:用于保存新建的对象及年轻的对象,过大的数组创建后会被保存至老年代
      • 回收算法:复制(coping)算法
      • 特点: 对象回收频繁,要求执行速度很快,在经过足够多次的 GC 回收之后,生存时间足够长的对象会被保存至老年代
    • 老年代

      • 作用对象:用于保存足够的对象及过大的数据,和新生代专一过来的稳定性强的数据
      • 回收算法:标记-清除(Mark-Sweep)算法
      • 特点: 对象回收频繁,要求执行速度很快,在经过足够多次的 GC 回收之后,生存时间足够长的对象会被保存至老年代

    GC 的触发时机对于 新生代 / 老年代 都是一样的,都是存满的就会强制执行一次 GC(新生代是Eden满了),GC 时会暂停除 GC 之外的所有线程,所以什么时候 GC 是个值得优化的地方,也是 JVM 优化的主要部分,在 android 里 GC 优化基本就是 JVM 优化的全部了

    GC 中内存回收部分是本文的重点,在 新生代 / 老年代 中有不同的回收机制和算法,搞懂这些原理部分就 OK 了。Eden 和2个 Survior 的大小比例为 8:1:1

    • 标记 - 对象可达
      GC 以一系列根节点 Roots 对象为起点,根节点对象基本采用静态属性和在栈内存中方法中的本地变量所引用的对象。然后向下搜索,搜索所走过的路径称为引用链 Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。

      标记 - 对象可达
    • 新生代回收
      通过标记我们知道哪些对象是无用的,可以回收的,这里 GC 把新生代分为 3个部分:Eden | survivor to | survivor from

      • Eden
        存放 new 出来的对象,在 GC 时通过 GC 自身的标记算法,把标记可达的对象复制到 survivor to 中去,然后整体删除自身 Eden 区域,这样效率最高不是

        标记 Eden 中的对象可达性
        转移可达的对象到 survivor to 里面
      • survivor to
        接收 Eden 传递过来的对象,在下次 GC 时依然通过 GC 自身的标记算法,把 Edensurvivor to 自身标记可达的对象全部复制到 survivor from 里面去,然后整体删除自身

        标记 Eden,survivor to 中的对象可达性
        转移可达的对象到 survivor from 里面
      • survivor from
        其实和 survivor to 相同,2者来回复制删除,整存整取,为的是不产生内存不连续,要不整理内存的消耗那是非常高的,也是非常影响性能的

    • 老生代回收
      老生代就没新生代这么复杂了,内部也不分区域,直接删除标记为不可达的对象

    新生代和老年带内存回收涉及到 3个算法: 复制算法 | 标记-清除算法 | 标记-清除-压缩算法

    • 复制算法
      这是新生代的算法,为啥呢,因为新生代里对象对,回收频繁,为了避免出现内存不连贯的问题和性能考虑,只有来回复制,整存整取,整块操作,整块删除才是最好的选择

    • 标记-清除算法
      这是最一般的算法了吧,这个算法的问题就是会产生内存不连续,较多的空间碎片会导致内存使用率过低,时间久了会大幅度提升 GC 所需的时间

    • 标记-清除-压缩算法
      这是老年带的算法,在标记完成后,扫描所有的对象,将所有被标记的有效对象压缩至划定的内存的顶部,然后根据有效对象和无效对象的边界,清除所有不可达的对象,不过整理压缩的速度比较慢。老年带对象较少,产生的碎片也较少,整理的代价也比较小


    什么样的对象会被移入老生带

    • 新生代中经历过15次GC的对象
      虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次GC后仍然存活,将被移动到Survivor空间中,并且对象的年龄设为1;对象在Survivor区中每“熬过”一个GC,年龄就增加1岁,当它年龄增加到一定程度(默认为15岁),就会晋升到老年带中
    • 大对象直接进入老年代
      所谓大对象是指,需要连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组,虚拟机提供了一个PretenureSizeThreshold参数,令大于这个这个值的对象直接在老生代中分配。这样做主要是为了避免在Eden区和两个Survivor区之间复制算法执行的时候产生大量的内存复制

    安卓分配与回收

    Android系统并不会对Heap中空闲内存区域做碎片整理。系统仅仅会在新的内存分配之前判断Heap的尾端剩余空间是否足够,如果空间不够会触发gc操作,从而腾出更多空闲的内存空间

    在Android的高级系统版本里面针对Heap空间有一个Generational Heap Memory的模型,这个思想和JVM的逐代回收法很类似,就是最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作

    每次 GC 都会把信息打印出来,学会查看 GC 日志对我们调优很有帮助

    GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。
    GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发GC操作来释放内存。
    GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。
    GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。
    

    这里有个支付宝的 GC 优化实战,不过是修改的虚拟机的回收机制,用自己的 libdvm.so 覆盖系统的


    Android 内存使用很少时也会 GC

    android 虽是基于Linux 的,但是在 GC 时机这块改了下,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]
    
    • heapgrowthlimit - 是一般状况下 vm 最大内存限制,多了就 OOM 了
    • heapmaxfree - 是在配置文件中设置 largeHeap="true" 后 vm 能获得的最大内存,超了一样 OOM
    • heapmaxfree - 最大内存空闲值,超了会触发 GC,同时调整内存上限
    • heapminfree - 最小内存空闲值,不够同样会触发 GC,GC 后再不够会调整内存上限
    • heapstartsize - vm 启动时申请的内存大小
    • heaptargetutilization - 内存期望利用率

    上面说的都是 java 堆内存。android 进程 vm 一上来是不会直接申请最大内存的,而是根据需求一点一点玩上调节的,这里就有讲头了,GC 也在这里触发

    理论上堆的大小应该 - LiveSize 实际使用量 / heaptargetutilization,但是这个值是有限制的,必须 >= LiveSize + MinFree,<= LiveSize + MaxFree,否则就要进行调整,调整的其实是 vm 内存上限 softLimit

    • 要申请的内存空间 > 当前空余内存,会先 GC,要是不够会动态调整内存上限,一次增加 heapmaxfree 的量
    • 当前空余内存 - 要申请的内存空间 < heapminfree,会先 GC,要是不够会动态调整内存上限
    • 当前空余内存 - 要申请的内存空间在 heapmaxfree - heapminfree 之间,不会触发 GC

    这频繁的触发 GC 就称为内存抖动

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


    扩展一下

    android 采用的是自己的虚拟机,Dalvik 和 ART,Dalvik VM 中堆结构相对于 JVM 堆结构有所区别,体现在 Dalvik 将堆分成了 Active 和 Zygote,Active 就是我们上面说的传统的堆内存,而 Zygote 存放的预加载类都是Android核心类和Java运行时库,这部分很少被修改,大多数情况下父进程和子进程共享这块区域

    为什么要把Dalvik堆分成Zygote堆和Active堆?这主要是因为Android通过fork方法创建一个新的zygote进程,为了尽可能的避免父进程和子进程之间的数据拷贝,fork方法使用写时拷贝技术,简单讲就是fork的时候不立即拷贝父进程的数据到子进程中,而是在子进程或者父进程对内存进行写操作时才对内容进行复制

    参考资料:

    相关文章

      网友评论

        本文标题:(过时、作废)android 多线程 — GC

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