Android Art 虚拟机 GC 机制之 java 部落的崛起

前言

  在正式研究 android art 虚拟机的GC机制之前,必须要先了解 linux 的内存管理,是的,只需要了解,不必深入,毕竟 android 系统是基于 linux 系统开发出来的移动操作系统,而GC机制当然也是基于 linux 系统的内存管理开发出来的用户态内存回收进制。除此之后,还要有一定的 linux 系统进程管理的基础,在深入探究之前可自行先复习一下 linux 系统的进程管理和内存管理,加固基础知识,在后期的研究会有事半功倍的效果,否则会不知所云,导致越到后面越没兴趣,最后放弃 android art 虚拟机的GC机制的学习。
  现在行业内也有不少相关的文章是分析 android art 虚拟机的内存回收机制的,有些也写得不错,比如老罗的ART运行时一系列的文章,结合源码分析其原理,不过从头看一次下来,能记住并且搞懂的内容还是比较少的。本文是只针对整个GC机制从浅到深系统地整理一次,包括GC机制所涉及到的虚拟机相关知识,对比 linux 系统的进行系统整理,使得读者更好理解。
  本文只做知识分享,给广大的想要深入理解 GC 机制的开发者提供前车之鉴,带大伙入个门,少走弯路,最后也感谢老罗的分享,在细节上分析得很好。
  给读者的个人建议:最好是在深入 art 虚拟机的源码之前先阅读完本篇文章,对他有个大概的理论了解,否则会非常痛苦。

介绍

  Android GC 的全称是 Android Gabage Collection,顾名思义是安卓系统的垃圾收集,为什么会有垃圾收集?众所周知,Android 应用程序大部分都是基于 java 语言开发的,在编写应用程序时不需要对对象进行内存释放的操作,通常在 new 一个对象后,当不需要使用时只需要把该对象的引用重新赋值为 Null 就可以了。这时 GC 就有存在的价值了,该对象的真正内存释放是在 GC 过程中释放掉了,不需要应用程序开发者操心。这就是 java 语言的核心之一,让开发者有更多的精力去关注应用逻辑,而并不需要与 C/C++ 那样考虑因内存没有释放导致内存泄漏的问题。
  但是当把对象的引用重新赋值为 Null 时,并不会立即触发 GC 释放该对象的内存,那么,是什么时候把对象释放掉的呢?这个问题会等阅读完整篇文章后,你自然就有答案了。请跟随我的脚步耐心阅读下去。

1、java 对象的创建

  要想清楚知道GC是如何工作的,则必须先知道对象是如何创建的?java 对象在 linux 系统中是以什么形状存在的?
  首先,请读者先以了解到的 linux 系统知识想像,或者猜测一下 Android 系统是如何在 new 一个对象时,为该对象分配内存的?如果你猜到最终是通过 malloc 函数分配一块内存的话,说明你的 linux 系统知识很扎实; 如果你猜到最终是通过 mmap 函数分配内存的话,说明你是 linux 高手。我们先不讨论哪种是正确的,我们先回顾一下 linux 应用程序的内存分布,毕竟 Android 应用程序也是跑在 linux 系统之上的,Android 应用程序的内存分布同样有 linux 应用程序的特性。如下图所示,是从教科书上截下来的。
  Android Art 虚拟机 GC 机制之 java 部落的崛起_第1张图片
  Android 应用程序在启动运行之后,其4G的内存分布空间与 linux 应用程序基本一样,有代码段、数据段、和堆栈区域。与 linux 应用程序的唯一区别是,Android 应用程序是运行在 java 虚拟机之上的,如果你把 Android 应用程序与 java 虚拟机看作整体,其本质就是一个 linux 应用程序。好了,在这里开始多出了一个 java 虚拟机。下面先介绍一下 java 虚拟机。

1.1、java 虚拟机

  目前 Android 系统中的 java 虚拟机有两个,一个是dalvik,另一个是 art,在 Android 4.4 之后的版本都是使用 art 作为 java 虚拟机。从网络上搜索 java 虚拟机,就会有大量的介绍,这里为了加深印象,我自己总结了下什么是 java 虚拟机,它是一个能够为 java 语言提供运行环境的程序,注意,这里说的是程序。主要的功能是把 java 语言解释成计算机可执行的机器码,管理 java 程序中的对象内存分配与释放等。也就是说,Android 系统的 java 程序都是运行在这个虚拟机(“程序”)之上的,以 Android 7.0 为例,当 linux 内核启动之后,首先通过 init 进程启动各种守护进程,这里面其中有个叫 zygote 的进程,其启动的可执行文件以及参数都在 init.rc 文件中指定,以下是 Android 7.0 的 zygote 进程启动参数

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks /sys/fs/cgroup/stune/foreground/tasks

  从启动参数可以看到, zygote 进程其实是一个名为 app_process64 的可执行程序,也就是说,zygote 进程的启动入口是在 app_process64 的 main 函数,至于整个启动过程这里不一一细说,有兴趣的可以阅读源码 (frameworks/base/cmds/app_process/app_main.cpp),这里先大概描述一下它在整个Android 系统中的作用以及设计思想,然后结合部分关键代码来了解 Android 系统中的 art 虚拟机。
  前面有提到, Android 系统的 Java 程序都是跑在虚拟机上的,那么 Android 的应用程序都需要一个 Java 虚拟机的环境,或者到这里有些读者会搞不懂了,没关系,先放下这个虚拟机环境的概念,我们先回顾一下 linux 应用程序的运行,如果我要写一个简单的能在arm平台上运行的 hello world 的 linux C 程序,那么首先我们先创建一个 C 文件 test.c,并且按照 C 语言的语法写一个打印 hello world 程序,如下

 #include 

 int main()  
 {  
     printf("Hello World\n");
     return 0;
 }

接着,使用交叉编译工具链把 test.c 编译成可在 arm 平台上可执行的机器码,也就是可执行程序。

arm-linux-gcc -o hello hello.c

这样直接放到 arm 平台设备上直接执行了,这里有一个交叉编译工具链 arm-linux-gcc,这个工具的作用就是把我们熟悉的 C 代码翻译成arm 平台可执行的机器码,当然,我们知道使用不同的交叉编译工具链最终会翻译成不同平台可执行的机器码。这是我们在学校里学习到的最熟悉的做法,那么 java 语言又是怎么被翻译成机器码的呢?这就需要 java 虚拟机了,它扮演的角色跟交叉编译工具链相似,区别在于,交叉编译工具链需要在 PC 机上预先把 C 语言翻译成机器码,然后再把可执行文件 hello 放在目标机器上,而 java 虚拟机则不需要,它是直接执行在目标机器上的,同时一边执行 java 虚拟机的相关逻辑,一边把待执行的 java 语言翻译成机器码,翻译成机器码后直接在目标机器上执行,所以在文章前面说 java 虚拟机是一个程序,是一个把可以明白 java 程序想要干嘛,并且告诉机器 java 代码的执行逻辑的程序,为了加深理解,下面讲个故事。
  现在有三个部落,为了对号入座,把这三个部落命名为 Java 部落,C 部落,和 Arm 部落,这三个部落讲的话不一样,谁都听不懂谁的,在这三个部落中,Arm 部落制造铁剑的环境很完善并且有充足的人力,不过缺少设计铁剑的人才,Java 部落和 C 部落都擅长剑的设计,但制造铁剑的环境很恶劣并且缺少人力,有一天,C 部落的人在讨论为什么他们制造出来的剑没有达到设计时的效果,其中有一个人提出了一个大胆的设想,干脆把设计方案给 Arm 部落的人制造,他们那有更好的制造环境,于是他们便跑到 Arm 部落里想让他们帮忙按照 C 部落的要求制造一把铁剑,由于他们语言不通,结果鸡和鸭讲,最后 C 部落的人回去后努力学习了 Arm 部落的语言,并且把制造步骤和设计图纸按照 Arm 部落的语言写下来,派了一个信使把这个设计方案送到了 Arm 部落,Arm 部落最后把 C 部落的铁剑制造了出来。
同样,Java 部落的国情与 C 部落是一样的,但与 C 部落的做法不一样,他们先派了个人去 Arm 部落学习语言和口语,后来这个人被两个部落的人称为“Java 虚拟机”,很受人尊敬,于是每当 java 部落的铁剑设计师需要找 Arm 部落的工匠制造铁剑时,都会找 java 虚拟机做翻译工作,就这样,越来越多的,各种各样的铁剑一把一把地制造出来了。故事先讲到这。
  Java 虚拟机到低是什么时候运行在 Arm 平台上的呢?前面有提到在 linux 内核启动之后,由 init 进程创建了一个名叫 zygote 的进程,就是这个 zygote 进程启动了 Java 虚拟机,以下是启动过程的函数调用,红色代表 java 代码。Java 虚拟机在 AndroidRuntime.cpp 里的 startVm 函数启动,如果此时 java 虚拟机指定为 art 虚拟机,则调用<android-root-dir>/art/runtime/runtime.cc 里的 Create 函数正式启动虚拟机,并且初始化所有的 java 运行时环境,这个过程非常复杂,想深入了解可参考《老罗的 Android 之旅》,本文只分析 art 虚拟机中的内存管理部分。
  Android Art 虚拟机 GC 机制之 java 部落的崛起_第2张图片
  虚拟机启动过程中,会初始化堆,GC,以及内存分配器等内存管理相关的内容,这里先跳过虚拟机的启动过程,先对整个 Android 运行状态有个大概的了解,在虚拟机启动之后,此时一切 java 语言的执行环境都准备就绪,接着 zygote 进程就开始运行 java 代码了。ZygoteInit.java 文件中的 main 方法是 zygote 进程的 java 代码入口,这里会先创建本地 socket 用来 ActivitySerivce 请求应用进程创建,接着加载 /etc/compiled-classes 文件所列的所有 java 类,然后启动 SystemServer 进程,最后进入等待循环,等待应用进程创建的请求。
  再对比一下 linux 进程,其实 zygote 进程与它没什么区别,其实质都是 linux 进程,只不过 zygote 进程在进入等待循环(“死循环”)之前运行了一段 java 虚拟机的相关代码(下文把这种运行一段 java 虚拟机的相关代码称之为“启动 java 虚拟机”),用来创建 java 语言的执行环境。到这里,有读者可能会想到,zygote 进程之所以能运行 java 代码是因为 zygote 进程在开始时启动了 java 虚拟机,那么如果一个应用程序的进程是不是又要像 zygote 进程一样,在启动之初都需要启动 java 虚拟机呢?
  如果你有这个问题抛出来,证明你已经对虚拟机有大致的了解了。至于这个问题我们先回顾一下 linux 进程创建子进程的做法。以下是一段C 语言创建子进程的代码

#include
static int value = 2;
static int showValue(int value)
{
        int ret = value * 2;
        return ret;
}
int main(void)
{
    printf("fork.\n");
    int pid = 0;
    value = 3;
    int child_value = 4;
    pid = fork();
    if (pid == 0) {
        printf("child=%d. child_value = %d, showValue = %d\n", getpid(), child_value, showValue(child_value));
        printf("child=%d. value = %d, showValue = %d\n", getpid(), value, showValue(value));
        return -1;
    }
    printf("father=%d. child_value = %d, showValue = %d\n", getpid(), child_value, showValue(child_value));
    printf("father=%d. value = %d, showValue = %d\n", getpid(), value, showValue(value));
    return 0;
}

   main 函数中创建了一个 child_value 的整形值,并初始化为 4,修改了全局变量 value 的值为 3,接着调用 fork 函数创建子进程,该函数返回的 pid 为 0 时,说明正在运行的进程为子进程,并把 fork 前创建的 child_value 的值和修改过的 value 的值打印出来,同时调用 showValue 对它乘以 2输出打印。如果 pid 返回不为 0 ,说明正在运行的进程为父进程,同样对 child_value 和 value 的值进行同样的处理并打印,如下为输出结果

fork.
father=6081. child_value = 4, showValue = 8
father=6081. value = 3, showValue = 6
child=6082. child_value = 4, showValue = 8
child=6082. value = 3, showValue = 6

  从结果可以看出,无论是子进程还是父进程,child_value 和 value 的值都是 4 和 3,这就是著名的 fork 函数,为什么会这样?是因为子进程在创建时会拷贝一份父进程地址空间给自己作为初始化地址空间,再深入的解释就不解释了,如果有 linux 基础的人早就知道上段代码的运行结果了,这里我想说明的是,android 应用程序的进程正是利用这一特点继承了 java 虚拟机的运行环境,这是如何实现的呢?上面提过到 zygote 进程在最后会进入等待循环,此时所有的 java 运行环境都创建好了,并保存在 zygote 进程的地址空间中,当有应用程序启动时,会向 zygote 进程发出进程创建的申请,zygote 进程收到后最终调用 fork 函数创建进程,并把子进程的入口函数指向 java 代码的 ActivityThread.java 中的main 函数,以下是伪代码。

ZygoteInit.Main(){
    startVM();
    while(1){
        waitForCreateApp();  // 等待应用程序创建请求
        pid = fork();
        if(pid == 0) {    // 子进程创建成功
            ActivityThread.main(); // Android 应用程序的入口函数
            ...
        }
    }
}

  这样 Android 应用进程就不需要重新启动一次 java 虚拟机就启动了一个 java 虚拟机,从此就可以执行 java 代码了,由于所有 android 应用程序都是通过这种方式进行创建的,这样就可以节省很多物理内存,因为 java 虚拟机中只读的那部分其实在物理内存中只保存一份,各应用程序对其共同享用。
  到这里总结一下,Android 系统中,每一个 Android java 进程都对应一个 java 虚拟机实例,即每一个 Android java 进程都有一个单独的 java 虚拟机,每一个 java 虚拟机都是 zygote 进程的 java 虚拟机的”副本”。为了加深印象,接着上面的故事继续类比。
  后来,java 部落派去 Arm 部落学习语言和口语的第一个人,即 java 虚拟机,一直生活在 Arm 部落里,并组建了自己的家庭,他的后代也继承了他的事业,并被外界称为 java 虚拟机实例,为每一个 java 部落的铁剑设计师做翻译的工作,而最开始那们 java 虚拟机被封为 zygote,从此 Java 部落的铁剑越来越好,越来越多样。
  读完 java 部落的崛起一文后,相信大家对 java 虚拟机有了一定的了解,下篇开始介绍 java 虚拟机中资源(内存)管理的部分,敬请期待!

你可能感兴趣的:(art-之-GC,java-虚拟机,android-虚拟)