Android进阶

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

2019-03-17  本文已影响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 进程

我们使用 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部分:新生代 | 老年代 ,听起来有点像古生物研究呀 ~

堆内存

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

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

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


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


安卓分配与回收

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]

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

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

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

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


扩展一下

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

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

参考资料:

上一篇下一篇

猜你喜欢

热点阅读