Android虚拟机的几个问题探究

这里说的Android虚拟机指运行在Android平台上的虚拟机,即日常遇到的Dalvik和ART虚拟机。这篇文章记录了自己对Android虚拟机几个问题的理解。只是个人学习和理解过程的记录,如有不当之处万望指正,邮箱yangfan3687@163.com。也希望在面对下面这些所谓JVM常见问题时能给你带来不一样的思考。

  • 问题1:如何理解JVM内存模型?
  • 问题2:什么是GC ROOTS?
  • 问题3:Android虚拟机有没有分代回收?

0x02 堆转储HPROF协议

在看JVM内存模型时,不妨先了解一下堆转储文件的协议。通过HPROF的问题格式可以大致了解JVM在内存中的划分情况。

2.1 HPROF文件格式

总的来说,HPROF文件分为两个大的部分,分别是Hprof Header和Hprof Body。其中Header部分又包括以下几个部分:

  • fixed header:包含文件描述以’/0’结尾,id字段的长度信息等
  • string table:包含所有用到的字符串,包括类目、方法名、常量名等。
  • class table:包含所有的类信息
  • stack frame:包含所有线程的栈帧信息
  • stack trace:包含所有线程的虚拟机栈情况

Body部分就是完整的jvm堆信息,将堆上的对象引用状况表述到文件中。其中在表述类对象是静态方法和静态成员变量时稍复杂,详细格式请查看 3.2 ~ 3.4。以下是HPROF文件格式的详细说明:

||||||||||||
| :——– | :——–:| :——: |:——: |:——: |:——: |:——: |
| “JAVA PROFILE 1.0.3/0”(Magic Code) | 4byte mIdSize (储存id的字节长度) | 8byte (Time Stamp) |
| 0x01(string table) | 4 byte | 4 byte(字符串length) | mIdSize byte | strlen(length) byte|
| …… | …… | …… | …… | …… |
| 0x02(class table) | 4 byte | 4 byte (length) | 4byte(class serial number) | mIdSize byte| 4 byte(Stack trace serial number)| 4 byte(class name string id)
| …… | …… | …… | …… | …… |
| 0x04(stack frame) | 4 byte | 4 byte (length) | mIdSize byte | mIdSize byte (methodName string id) | mIdSize byte (methodSignature string id) | mIdSize byte (sourceFile string id ) | mIdSize byte (serial) | mIdSize byte (lineNumber )|
| …… | …… | …… | …… | …… |
| 0x05(stack trace) | 4 byte | 4 byte (length) | 4 byte serialNumber | 4 byte threadSerialNumber | 4 byte numFrames | numFrames * mIdSize byte stack frame id|
| …… | …… | …… | …… | …… |
| 0x0C(HEAP DUMP) | 4 byte | mIdSize byte (length) |
| 0xFF(ROOT_UNKNOWN) | mIdSize byte string id |
| 0x01(ROOT_JNI_GLOBAL) | mIdSize byte string id | mIdSize byte jni global id|
| 0x02(ROOT_JNI_LOCAL) | mIdSize byte string id | 4 byte thread serial number | 4 byte stack frame number |
| 0x03(ROOT_JAVA_FRAME) | mIdSize byte string id | 4 byte thread serial number | 4 byte stack frame number |
| 0x04(ROOT_NATIVE_STACK) | mIdSize byte string id | 4 thread serial number|
| 0x05(ROOT_STICKY_CLASS) | mIdSize byte string id |
| 0x06(ROOT_THREAD_BLOCK) | mIdSize byte string id | 4 thread serial number|
| 0x07(ROOT_MONITOR_USED) | mIdSize byte string id |
| 0x08(ROOT_THREAD_OBJECT) | mIdSize byte string id | 4 byte thread serial number | 4 byte stackSerialNumber |
| 0x20(ROOT_CLASS_DUMP) | mIdSize byte string id |
| 0x21(ROOT_INSTANCE_DUMP) | mIdSize byte string id | mIdSize byte stackId | mIdSize byte class id | 4 byte remaining |
| 0x22(ROOT_OBJECT_ARRAY_DUMP)| mIdSize byte string id | mIdSize byte stack id | num elements | mIdSize byte class id| mIdSize * num elements(skip) |
| 0x23(ROOT_PRIMITIVE_ARRAY_DUMP)| mIdSize byte string id | mIdSize byte stack id | 4 byte num elements | 4 byte primitive type | mIdSize * num elements(skip) |
| 0xC3(ROOT_PRIMITIVE_ARRAY_NODATA)| mIdSize byte string id | mIdSize byte stack id | 4 byte num elements | 4 byte primitive type | 4 * num elements(skip) |
| 0xfe(ROOT_HEAP_DUMP_INFO) | mIdSize byte heap id | mIdSize byte heap name id(string id)
| 0x89(ROOT_INTERNED_STRING)| mIdSize byte string id |
| 0x8a(ROOT_FINALIZING) | mIdSize byte string id |
| 0x8b(ROOT_DEBUGGER) | mIdSize byte string id |
| 0x8c(ROOT_REFERENCE_CLEANUP) | mIdSize byte string id |
| 0x8d(ROOT_VM_INTERNAL) | mIdSize byte string id |
| 0x8e(ROOT_JNI_MONITOR) | mIdSize byte string id | 4 byte thread serial number | 4 byte stack frame number |
| 0x90(ROOT_UNREACHABLE) | mIdSize byte string id|
| 0x1C(HEAP DUMP SEGMENT) | 4 byte | mIdSize byte (length) |

2.2 ROOT_CLASS_DUMP的格式

||||
| :——– | :——–:|
| 0x20(ROOT_CLASS_DUMP) | 1 |
| id| 4|
| stack serial number| 4|
| super class id| 4|
| class loader id| 4|
| signeres id| 4|
| protection domain id| 4|
| reserved| 4|
| reserved| 4|
| instance size | 4|
| const pool num entries| 2|
| 2 * num entries |
| static fields num entries | 2|
| static fields | static fields num entries * (static fields),下面会再单独列出来|
| instance fields num entries| 2|
| instance fields| instance fields num entries * (instance fields)

2.3 0x20(ROOT_CLASS_DUMP).Static Fields

|||||
| :——– | :——–:| :——: |
| 4 | 1 | 4|
| static fields id| static fields type| type size|

2.4 0x20(ROOT_CLASS_DUMP).Instance Fields

||||
| :——– | :——–:|
| 4 | 1|
| instance id| instance type|

2.5 HEAP DUMP和HEAP DUMP SEGMENT的区别

如果你仔细研究了上面的内容的话,可能你就会有这样一个问题,HEAP DUMP和HEAP DUMP SEGMENT有什么区别?为什么要有两个标记?
其实堆转储的hprof文件格式中,原本是使用4字节32位存储堆对象的 “HEAP DUMP” (0x0C)的区块长度,但同时也就限制了HEAP DUMP的大小必须在4GB以内。在出现这个问题的情况下,在HPROF文件中新增了”HEAP DUMP SEGMENT” (0x1C)的格式,用来将超过4GB的JVM堆对象信息分别存储到文件的多个区块中。

Be Careful with HPROF Heapdumps Bigger than 4GB @2013-04-16

0x03 理解JVM内存模型

jvm-memory-model.png

  1. 程序计数器
    在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

  2. Java虚拟机栈
    线程私有,每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

  3. 本地方法栈
    线程私有,本地方法栈的功能和特点类似于虚拟机栈,不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。我们常见的HotSpot虚拟机选择合并了虚拟机栈和本地方法栈。

  4. Java堆
    所有线程共享的内存区域,所有对象实例及数组都要在堆上分配内存。

  5. 方法区
    所有线程共享的内存区域,为了区分堆,又被称为非堆。用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

在谈到JVM内存结构时,从HPROF文件的header和body部分就有体现,从数据上划分结构就可以分为方法区(包括运行时常量池,也就是header中string table的部分)、java堆(body部分)、虚拟机栈、本地方法栈和程序计数器。其中方法区和java堆部分是所有线程共享的数据区,其他则为线程独有的数据区。为什么会有这样的结构划分和设计?
先看设计内存的目的,是因为随着CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存。但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存来缓解这种症状。基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存。回到JVM的内存模型中, Java中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域main memory,而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理。这样也就说的通了,为啥在JVM中有的区域是线程共享的,有的区域是线程独享的。

0x04 什么是GC和GC_ROOTS?

4.1 什么是GC

垃圾回收(Garbage Collection,简称GC),是垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。这种机制也不单单只有Java虚拟机才有,ObjectC和C#都有自己相应的垃圾回收机制,垃圾回收机制是帮助程序员自动管理对象内存空间的机制。

4.2 什么是GC ROOTS

常见的垃圾回收方式,引用计数算法和根搜索算法。
引用计数就像是每个对象有个账本,当一个对象被另一个对象引用时该对象的引用计数器+1,当引用失效时引用计数器-1,任何引用计数为0的对象可以被当作垃圾收集。引用计数有一个先天的缺陷,那就是多个对象相互持有引用形成一个引用环是,那么环中的所有对象引用计数都不为0,这时这些对象都不能被垃圾回收器回收。
根节点搜索指的是从根节点集合出发找到所有引用链可达对象,当一个对象到根节点集合(GC ROOTS)没有任何引用链存在时就证明此对象是不可用的。从对比JVM规范的垃圾回收根节点(来自:Garbage Collection Roots)、HPROF文件协议中的GC ROOTS tag类型和square haha库源码中对GC ROOTS的类型定义,参照下表。

|JVM规范名称| HPROF中的TAG | haha库中的RootType枚举类型 | 描述 |
| :——– | :——–:| :——: |:——: |:——: |:——: |:——: |
| System Class | 0x05 | RootType.SYSTEM_CLASS | 被bootstrap/system class加载器加载的类,例如所有rt.jar中包名为 java.util.*的类 |
| JNI Local | 0x02 | RootType.NATIVE_LOCAL | native代码中的本地变量,例如user defined JNI code or JVM internal code |
| JNI Global | 0x01 | RootType.NATIVE_STATIC | native中的全局变量,例如user defined JNI code or JVM internal code |
| Thread Block | 0x06 | RootType.THREAD_BLOCK | |
| Thread | | | |
| Busy Monitor | 0x07 | RootType.BUSY_MONITOR | 所有调用 wait()、 notify()方法的, 或者同步的。For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object. |
| Java Local | 0x03 | RootType.JAVA_LOCAL | 本地变量,例如线程栈帧中的参数和方法 |
| Native Stack | 0x04 | RootType.NATIVE_STACK | In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection |
| Finalizable | 0x8a | RootType.FINALIZING | 在finalizer等待队列里的对象 |
| Unfinalized | | | |
| Unreachable | 0x90 | RootType.UNREACHABLE | 从其他根节点都无法到达的对象 |
| Java Stack Frame | 0x03 | RootType.JAVA_LOCAL | A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects. |
| Unknown | 0xff | RootType.UNKNOWN | |
| | 0x89 | RootType.INTERNED_STRING | |
| | 0x8b | RootType.DEBUGGER | |
| | 0x8c | RootType.REFERENCE_CLEANUP | |
| | 0x8d | RootType.VM_INTERNAL | |
| | 0x8e | RootType.NATIVE_MONITOR | |

对照上面这个表总结起来,所谓JVM GC Roots是进行垃圾回收时根节点的集合。大致包含以下几个方面:

  • 所有Java线程当前活跃的栈帧所指向GC堆里的对象的引用,换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
  • VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
  • 所有当前被加载的Java类
  • JNI handles,包括global handles和local handles
  • Java类的引用类型静态变量
  • Java类的运行时常量池里的引用类型常量(String或Class类型)
  • String常量池(StringTable)里的引用

按照根搜索GC的思想,从根节点出发的找到的对象就被认定为存活的,其他的对象都是“无用的”,但是GC ROOTS的集合不应该是一成不变的,特别是面对分代GC时。为啥这样说呢?分代GC是一种部分收集(partial collection)的做法。在执行部分收集时,从GC堆的非收集部分指向收集部分的引用,也必须作为GC roots的一部分。具体到分两代的分代式GC来说,如果第0代叫做young gen,第1代叫做old gen,那么如果有minor GC / young GC只收集young gen里的垃圾,则young gen属于“收集部分”,而old gen属于“非收集部分”,那么从old gen指向young gen的引用就必须作为minor GC / young GC的GC ROOTS的一部分。所以针对一次GC来说,GC ROOTS的类型类型范围未必只有jvm规范定义中所列举的那几种情况。

0x05 Android虚拟机有分代GC吗?

Android Q开始google才为ART虚拟机添加分代收集机制。

参考文档

JVM内存模型与GC算法
Android Hprof 协议
java的gc为什么要分代?RednaxelaFX的回答
Android Q Beta 正式发布 | 精于形,安于内
https://www.cnblogs.com/dingyingsi/p/3760447.html