Android性能优化之虚拟机调优

众所周知,我们的Android App运行在Java虚拟机之上,而Java是一门带GC的语言。在虚拟机进行垃圾回收的时候,要做一件很形象的事叫做STW(stop the world);也就是说,为了回收那些不再使用的对象,虚拟机必须要停止所有的线程来进行必要的工作。虽说这一点在ART运行时上得到了很大的改善,但是GC的存在对App运行时的性能始终有着微妙的影响。如果你观察过手机输入的日志,一定会看到类似如下的内容:

12-23 18:46:07.300 28643-28658/? I/art: Background sticky concurrent mark sweep GC freed 15442(1400KB) AllocSpace objects, 8(128KB) LOS objects, 4% free, 32MB/33MB, paused 10.356ms total 53.023ms at GCDaemon thread CareAboutPauseTimes 1
12-23 18:46:12.250 28643-28658/? I/art: Background partial concurrent mark sweep GC freed 28723(1856KB) AllocSpace objects, 6(92KB) LOS objects, 11% free, 31MB/35MB, paused 2.380ms total 108.502ms at GCDaemon thread CareAboutPauseTimes 1

上面的日志反映一个事实:GC是有代价的。有很多有关性能优化的文章提到GC,会花长篇大论讲述垃圾回收的过程以及原理,但所做的策略无非就是「不要创建不必要的对象」,「避免内存泄漏」最终就提到MAT,LeakCanary等工具的使用上去了;我只能说这很苍白无力——写出这样的代码、学会使用工具应该是基本要求。

虽说Android也支持NDK开发,但是我们不可能把所有代码全用C++重写吧?那么,我们有没有办法能影响GC的策略,使得GC尽量减少呢?答案是肯定的。原理在于Android的进程机制——每一个App都有一个单独的虚拟机实例,在App自己的进程空间,我们有相当大的主动权。

我举个简单的例子。(下面的内容基于Android 5.1系统,所有的原理以及代码不保证能在其他系统版本甚至ROM上工作)

Android上所有的App进程都从Zygote进程fork而来,App子进程采用copy on write机制共享了Zygote进程的进程空间;其中Android虚拟机以及运行时的创建在Android系统启动,创建Zygote进程的时候已经完成了。垃圾回收机制是虚拟机的一部分,因此,我们先从Zygote进程的启动过程谈起。

我们知道,Android系统是基于Linux内核的,而在Linux系统中,所有的进程都是init进程的子孙进程,Zygote进程也不例外,它是在系统启动的过程,由init进程创建的。在系统启动脚本system/core/rootdir/init.rc文件中,我们可以看到启动Zygote进程的脚本命令:

service zygote /system/bin/app_process -Xzygote /system/bin –zygote –start-system-server

也就是说init进程通过执行 /system/bin/app_process 这个可执行文件来创建zygote进程;app_process的源码可见 这里;在main函数的最后有这么一句话:

123
if (zygote) {    runtime.start("com.android.internal.os.ZygoteInit", args);} else if (className) {

最终调用到了AndroidRuntime.cpp 的start函数,而这个函数中最重要的一步就是启动虚拟机:

1234
JNIEnv* env;if (startVm(&mJavaVM, &env) != 0) {    return;}

这个函数相当之长,不过都是解析虚拟机启动的参数,比如堆大小等等;探究largeHeap 这篇文章对一些重要的参数做了说明,这些参数对虚拟机非常重要,后面我们会见到。解析参数完毕之后,最终调用JNI_CreateJavaVM来真正创建Java虚拟机。这个接口是Android虚拟机定义的三个接口这一,dalvik能切换到art很大程度上与这个有关。它的具体是现在 jni_internal.cc;JNI_CreateJavaVM 这个函数在拿到虚拟机的相关参数之后,就直接创建了Android运行时:

123
if (!Runtime::Create(options, ignore_unrecognized)) {  return JNI_ERR;}

Runtime的创建非常复杂,其中,跟GC相关的是,App的堆空间被创建出来了;Heap的构造函数接受了一大堆参数,这些参数对于GC有着重大的影响,如果要调整GC的策略,从这里入手,是比较靠谱的。

12345678
heap_ = new gc::Heap(options->heap_initial_size_,                     options->heap_growth_limit_,                     options->heap_min_free_,                     options->heap_max_free_,                     options->heap_target_utilization_,                     options->foreground_heap_growth_multiplier_,                     options->heap_maximum_size_,// ...

其中 heap_initialsize 是堆的初始大小,heap_growthlimit是堆增长的最大限制,heap_minfree以及heap_maxfree 是什么呢?详细的用途见 Android ART GC之GrowForUtilization的分析 简单来说就是,Android系统为了保证堆的利用效率,减少堆中的内存碎片;每次执行GC回收到一些内存之后,会对堆大小进行调整。比如说你进入了一个图片非常多的页面,这时候申请了100M内存,当你退出这个页面的时候,这100M自然就被回收了,成为了空闲内存;但是系统为了防止浪费,并不会把这100M的空闲内存全部留给你,而是做一个调整。而具体调整到多大,则与heap_min_free_heap_max_free_ 以及 heap_target_utilization_ 相关。

说到这里,原理性的部分已经解释完了;除了流程稍微复杂,也没有什么难点。那么这个堆,跟我们的启动性能优化有什么关系呢?

在Android App的启动过程中,进程占用的内存在一段时间内是持续上涨的;假设堆的初始大小为8M,启动过程中的占用内存峰值30M;启动过程的进行中,伴随着大量临时对象的创建,它们朝生夕死,不久就被回收掉:

如上图,这是某次启动过程中某App的内存占用情况;我们看到了有很多小折线,专业术语叫做内存抖动;原因呢,也很明显——有大量的临时对象被创建。怎么解决?有人说,不要创建大量的临时对象。道理我都懂,可是做不到。对于很多大型App来说,启动的过程是相当复杂的,而很多操作也不能简单滴去掉。那么问题来了,30M并不是一个很大的数字,为什么系统如此恐慌,还需要不停滴回收内存呢?

有一种冷,叫做你妈妈觉得你冷。垃圾回收并不是说有垃圾了才去回收,而是只要系统觉得你需要回收垃圾就会进行。

那么,能不能在启动过程中让堆保持持续增长而不进行GC呢?毕竟,30M并不会造成什么OOM。是什么原因导致系统没有这么做?答案是空闲内存。比如说一开始堆有8M,随着启动过程的进行,堆增长到了24M;这时候执行了一次GC,回收掉了8M内存,也是堆回到了16M;我们还有8M的空闲内存。系统就会说,小伙子,你占这么多空闲内存干嘛呀?来妈妈帮你保管,于是你就只剩下2M的空闲内存了。但显然App使用的堆内存很快就会超过18M,于是又引发一系列GC以及堆大小调整,周而复始直至启动完成内存平稳。至此,我们的结论已经很明显:

如果我们能够调整 heap_minfree 以及 heap_maxfree,就能很大程度上影响GC的过程

如何调整这两个参数的大小呢?拿到Heap对象的指针,找到这两个参数的偏移量,直接修改内存即可 这里稍微需要一点C++内存布局的知识;至于如何拿到Heap对象的指针,只有去源码里面寻找答案了。这里我给出最终的实现代码:

1234567891011121314151617181920
void modifyHeap(unsigned size) {   // JavaVMExt指针 可以从JNI_OnLoad中拿到  JavaVMExt * vmExt = (JavaVMExt *)g_javaVM;   if (vmExt->runtime == NULL) {        return;   } char* runtime_ptr = (char*) vmExt->runtime;    void** heap_pp = (void**)(runtime_ptr + 188);    char* c_heap = (char*) (*heap_pp);

    char* min_free_offset = c_heap + 532;    char* max_free_offset = min_free_offset + 4;    char* target_utilization_offset = max_free_offset + 4;

    size_t* min_free_ = (size_t*) min_free_offset;    size_t* max_free_ = (size_t*) max_free_offset;

    *min_free_ = 1024 * 1024 * 2;    *max_free_ = 1024 * 1024 * 8;}

修改之后启动过程中内存占用如下,可以看到我们的目的已经达到:

顺便说明一下,上面的代码没有考虑任何的可移植性和适配性,只起演示作用。真正投入使用是一个体力活:其一,我们依赖了某特定Android版本某个类的内存布局,其中的成员变量的偏移量可能不同版本不同;其二,这个 minfree 以及 maxfree 具体调整为多大,跟手机的物理内存,App使用的内存,手机配置的初始堆大小等等因素密切相关;调整一个合适的参数需要花费一些时间,Android机型如此之多,这里需要一些小技巧。

不知道上面这个例子有木有让你感受到深入系统底层,那种呼风唤雨无所不能的快感?可能很多人觉得我们都是写写if else而已,调节面改动画写业务已经够了;但我想说明的是,深入学习系统原理是非常有好处的,它可以赋予你在应用层永远无法拥有的能力。

另外留个作业,我们上面提到观察GC的次数,除了使用debug模式下用工具观察,能不能用代码监听到呢?本文主要说明了虚拟机运行时等native层的重要性,而这个答案可以在Java Framework中找到 ^_^

Android性能优化(31)---虚拟机调优相关推荐

  1. Android性能优化之虚拟机调优

    介绍完 深入学习Android:虚拟机&运行时 之后,很多小伙伴问我,你描述的这些知识结构看起来艰深晦涩高大上,实际工作中能有多大用途呢?今天我就简单举个例子. 众所周知,我们的Android ...

  2. Spark性能优化:Shuffle调优篇

    Spark性能优化:Shuffle调优篇 一.调优概述 大多数Spark作业的性能主要就是消耗在了shuffle环节,因为该环节包含了大量的磁盘IO.序列化.网络数据传输等操作.因此,如果要让作业的性 ...

  3. GPU性能优化之CUDA调优指南

    GPU性能优化之CUDA调优指南 GPU性能优化之CUDA调优指南 1 整体性能优化策略 2 最大化利用率 2.1 应用程序层次 2.2 设备层次 2.3 多处理器层次 2.3.1 占用率计算 3 最 ...

  4. Spark性能优化:资源调优篇

    在开发完Spark作业之后,就该为作业配置合适的资源了.Spark的资源参数,基本都可以在spark-submit命令中作为参数设置.很多Spark初学者,通常不知道该设置哪些必要的参数,以及如何设置 ...

  5. 性能优化之MySQL调优篇

    MySQL对于很多Linux从业者而言,是一个非常棘手的问题,多数情况都是因为对数据库出现问题的情况和处理思路不清晰.在进行MySQL的优化之前必须要了解的就是MySQL的查询过程,很多的查询优化工作 ...

  6. Spark性能优化之-资源调优

    文章目录 概述 Spark作业基本运行原理 资源参数调优 num-executors executor-memory executor-cores driver-memory spark.defaul ...

  7. Spark性能优化之-开发调优

    文章目录 概述: 原则一:避免创建重复的RDD 原则二:尽可能复用同一个RDD 原则三:对多次使用的RDD进行持久化 Spark的持久化级别 如何选择一种最合适的持久化策略 原则四:尽量避免使用shu ...

  8. Spark性能优化之-shuffle调优

    文章目录 概述 ShuffleManager发展概述 HashShuffleManager运行原理 未经优化的HashShuffleManager 优化后的HashShuffleManager Sor ...

  9. 性能优化专题 - MySql 性能优化 - 04 - MySql调优

    目录导航 前言 Undo-log与Redo-log 案例 当前读.快照读 Redo Log的落盘配置 MySQL配置优化 MySQL服务器参数类型 快速定位MySql配置文件 MySQL内存参数配置 ...

  10. mysql 字符串索引 优化_MySQL性能优化之索引调优实战

    索引失效场景或使用注意事项 a.索引无法存储null值,所以建议都给默认值 b.如果条件中有or,即使使用了索引条件也不起作用,所以尽量少用or 如果想使用or,又让索引生效,只能将or的每个列上加上 ...

最新文章

  1. Java 8学习资料汇总
  2. [转载] 百科全说——何裕民:性格影响疾病(10-12-20)
  3. c语言的0变成奇怪的图形,Word打开后图片中的文字变成了奇怪的符号
  4. 如果和对方的意见或者事件冲突了怎么办?让步
  5. すぬけ君の地下鉄旅行 / Snuke's Subway Trip(AtCoder-2069)
  6. fscanf读出来的数字不正确_听了那么多数字英文儿歌,这位牛妈的做法却最能引导孩子输出!...
  7. Atitit nlp文本挖掘和自然语言处理方面,常用的算法总结 比如tf-idf 目录 1.1. tf:词频,是指某个词在某篇文章中出现的频率 2 1.2. 去停用词算法 2 1.3. idf。
  8. 深入浅出 Python Iterators 迭代器
  9. iOS Orientation 屏幕旋转
  10. sof与NIOS II的elf固件合并生成jic文件
  11. 《提问的智慧》 - 懒人的脑图
  12. 硅二极管温度传感器的特点
  13. vue各类轮播图大全
  14. Windows环境下配置Ceres(带suitesparse-metis)
  15. Apache Kafka API AdminClient Scram账户的操作(增删改查)
  16. 计算机软件工程考研考哪些专业,软件工程考研考什么科目?
  17. Mac系统显示已连接(可以打开qq)但是无法打开浏览器解决方案
  18. [BluehensCTF 2022] pwn11 crypto3
  19. 装了linux无法进入bios设置u盘启动,装了linux无法进入bios设置密码
  20. 功放板加开关音量_一种带音量开关功能的音响功放板的制作方法

热门文章

  1. 嵌入式Linux系统编程学习之二常用命令
  2. fastnest怎么一键排版_什么公众号排版编辑器可以换字体?公众号字体在哪里选择修改?...
  3. c语言ics什么意思,[转载]C网来话筛选(ICS)业务及实现
  4. 【JUC】第五章 JUC 阻塞队列、线程池
  5. 快速入门MyBatis-Plus,看这一篇就够了。
  6. Collection和Collections有什么区别?
  7. 扩展中国剩余定理 exCRT 学习笔记
  8. UVa 1225 Digit Counting 题解
  9. magic_quotes_gpc合magic_quotes_runtime的区别!
  10. 从内部重启python脚本