前置知识
- 有Android开发基础
- 了解 Java 语法和 JVM
- 已阅读 聊聊ART虚拟机_对象的分配问题
前言
在上一篇文章中,我们聊到了对象的分配问题,简单说明了何为 ART 虚拟机,以及对象中类的加载、内存布局等问题。本文继接上文,将会讲完对象的使用和销毁的问题,希望本文对你有所帮助。
在 ART 中,对于一个对象,我们可以从其分配、使用到销毁来看,其主要结构如下:
内存分配
分配器
在 Android 里面的内存分配是由内存分配器来进行分配的。其作用如下:
APP 的 java 对象内存分配是托管给 VM 来处理的,不会直接向操作系统申请。VM 就像一个连接 java 代码和内存的中介,VM 才是真正的内存管控者,其控制对内存的占用和内存布局。
而分配器则分为 3 类分配器,分别是:TLAB
、ROSallocator
、LOSallocator
。他们的原理和作用如下:
- 小内存/临时变量 -> TLAB:给每个线程开一些小缓存,一些小的对象,例如栈上的对象就在 TLAB 上面进行分配。
- 中等内存/数组、容器 -> ROSallocator:而较为大些的对象,则在 ROSallocator 中进行分配,这时候 ROSallocator 会直接去使用
VM mem pool
,此为 ART 托管的内存池。 - 大量内存/Bitmap存储图片 -> LOSallocator:更大的对象,交由 LOSallocator 去处理,就直接从 linux 上面取。例如 Bitmap 就是这样子。
内存碎片
由于内存中分配算法的问题,出现了多次分配后使得找不出一块连续的内存来放置当前需要的内存块,从而导致内存溢出。而 ART 分配原理就是找到一段在最优范围里面符合大小的连续内存。
如下图中所示,未被使用的内存很分散,但是却由于不能组成连续的内存块,无法被使用了。这些分散的内存块就是内存碎片。而这些内存碎片就是由内存分配过程中导致的,事实上,我们可以使用较好的内存回收策略来解决这些碎片问题。
内存回收
当出现了内存碎片问题可以用两种方法来解决。分别是 GC 和 RC。
GC:垃圾回收 (Garbage Collection),是一些语言管理内存的方式,如 Java 语言等;程序员不需要主动管理内存,程序运行时环境(虚拟机)会做垃圾回收的工作,就是在合适的时机 自动释放不再需要的内存。(需要系统去主动收集不使用的对象)
RC:引用计数(Reference Count),每当有一个新的强引用指针指向,对象的引用计数就会+1
,当减少一个强引用指针,引用计数就会-1
,当引用计数为0
时,对象就会被销毁。在 iOS 的 Swift 中,对象的内存是通过引用计数来管理的。(无需系统主动去收集不使用的对象)
RC的问题以及解决方案
事实上,虽然 RC 释放内存十分及时,但是单纯使用 RC 机制会出现一些问题的。这个问题就是环引用问题。
当两者出现环应用的话,就很难对其进行回收,因为他们之间会出现一个死结,导致其引用计数不会小于1。
为解决环引用的问题,IOS 的策略是使用弱引用和手动标记。手动标记中是将解环动作移交给了开发者,让其能小于1。而弱引用则是这样子,当对象有其他的对象引用的时候,弱引用就算作一种引用,可以找到被引用对象;若是没有其他人引用,只有相互引用的时候,弱引用就不算引用。
ART的引用
而在 ART 中,其使用的是 GC 机制,但是同时它也具有三种引用机制。
- 强引用:直接持有的,无法被 GC 回收
- 软引用:内存不足时候会被回收
- 弱引用:一旦触发 GC 必定被回收
我们需要注意的是:GC 并非在内存不够的时候才会被触发
触发GC的条件
如果 GC 只是在内存不够的时候才被触发,那么就会导致很多的问题。如果在内存不够才被触发,那么会使得性能下降且 GC 裂化。
而在 Android 中,触发 GC 的条件有如下两个
- 内存不够了
- 手机认为该 GC 了
- VM 堆占用达到水位。该水位是维持性能和内存的平衡点。
- 系统内存紧张。进程太多了,需要 GC 一下。
- 未知原因。可能是锁屏了,有些手机认为你在锁屏后不会再次使用,然后就趁机 GC 一下。
由此我们可以看出,IOS 的内存回收是要比 Android 要及时的,所以 Android 的内存中总是会存有垃圾在的。这也是为何 IOS 内存少,但是运行却未受太大影响的原因。
GC的方式
首先,我们需要了解一下 GC Root。GC Root 就是 GC 的起点,从这个起点出发的对象,都不能被释放。因为GC 认为 GC Root 以及它引用的对象是程序后面的可能会用到的,所以不会释放;没有被 GC Root 直接或间接引用的对象,后面一定不会被用到,可以被释放掉。
一般有如下四个起点:
- 栈/在栈内存的变量
- static 变量
- native 中的 JNI 引用的对象(native ref)
- VM 保留
如下图所示,处理 对象C ,其他的对象其引用起点都是 GC Root,所以只有 对象C 需要被 GC 掉。
而在基于 GC Root 来进行 GC 的方式则有两种,包括 Tracing GC
和 Copy GC
。
Tracing GC
Tracing GC 的方法是 从 root 遍历,然后将遍历过的都标记下来,被标记过的就是 GC Root 引用链上面的,最后将没有标记的 GC 掉即可。
Copy GC
Copy GC 的方法也是 从 root 出发,不过是将遍历过了对象 copy 到一个空闲的区域,然后将原有的区域的内存集中 GC 掉。这个方法的好处就是可以获得一块连续的内存空间。
ART中的应用场景
而在 ART 中,对于两种 GC 是的应用场景是如下这样子的:
前台GC | 后台GC | |
使用场景 | 应用在前台的进程 | 非前台应用进程、service/push进程 |
算法 | Tracing GC(mark-sweep) | Tracing GC(compacting) |
速度 | 快 | 慢 |
内存碎片 | 有 | 无 |
额外空间 | 不需要 | 需要 |
如何编写内存友好的代码
基于上文的讲述,我们知道了内存的分配和回收的机制,那么我们写代码的时候应该考虑我们的代码是否对内存是友好的。例如我们在考虑使用数组或者是链表的时候,除了其增删查改性能的考虑,我们仍需考虑到其是否对内存友好。例如数组是连续的,对内存友好,而链表是离散的,对内存不友好;那么在数量基本确定的时候,我们应该首要使用数组结构。
并且,我们需要了解到 Finalizer 机制和 Cleaner 机制的不同。Finalizer 的在生命周期中只会执行一次,再次被激活后就不会再次触发这个机制了,而如果我们对其二次激活做判断和保留的话,又容易导致内存泄露问题。所以说,我们需要尽量使用 Cleaner 机制。