了解一下,Android 10中的ART虚拟机(9)-完结

缘起

继续从读书笔记的角度来系统的学习《Advanced Design and Implementation of Virtual Machines》。这本书是英文编写,全部、认真、深入的读下来非常考验人。我之前也只是读了60%,而且越到后面越没有耐心。想了想,JVM在系统层面上要达到一定水准,可能还是得精读一到两本这样的书籍。记读书笔记是我学习知识和技能的一种比较好的方式,伴随我至少20多年了。暂且给这次的读书笔记取名“关于VM的理论”。

本篇介绍最后两个部分:

本文是我们对ADIVM一书阅读笔记的结束。今天这篇文章会讲得稍显粗糙,毕竟,这属于高级部分——优化嘛....有些地方我只能整理个大概,让你知道作者在说什么(如果不整理的话,基本属于不知所云,完全搞不清状况的那种)。

GC基础知识回顾

先回顾下GC基础知识,也是我在《深入理解Android JVM ART》一书第十四章里的摘抄。

从大道理上说,一共有四种基础GC方法。

  • Mark Sweep

  • Copying Collection

  • Mark Compact

  • Reference Counting

下面是除Reference Counting之外的三种GC方法的示意图。

Mark Sweep的原理如上。大致是找到存活对象,然后把垃圾对象就地清理掉。这种方法理解起来相对简单,也没有什么附加操作。但实际使用时,会造成内存碎片。

Copying collection就是把存活对象拷贝到一个空闲区域。拷贝的时候可以重新排位置,让大家挤在一起。这样,内存碎片的问题就解决了。但这个拷贝肯定有开销,而且还存在拷贝前/后对象被引用的问题。另外,内存空间还要事先留一块做空闲区域,有点浪费。

Mark Compact其实和Copying差不多。只不过它不留单独一块空闲区域。而是移动....所谓的Compact(压缩,其实就是移动)

以上是三种基础GC的示意。接下来我们看看书中是怎么优化GC的。

GC优化之提高吞吐量(Throughput)

作者首先谈如何提高GC的吞吐量。这一章一上来就是一堆公式。我觉得主要难在如何定义和计算吞吐量上。我们先看看作者关心的是什么。

在讨论GC吞吐量时,作者的第一个优化设计考虑点是想讨论Partial Heap和Full Heap回收的时机选择问题。也就是minor/major GC如何搭配。注意,这基于了一个大前提,就是对Heap区域进行了划分。上面图中左下角是Heap的划分。右边是分类。

基于这么一个目的,整出了几个数学公式。下面是对原文内容的高度提炼。我感觉看不看都可以...BTW,我翻了下美亚上对这本书的评价,其中有一条说到,本书后面有一大堆引用材料。但是在正文里却一丁点没提到某个内容来自哪个文献。说实话,我对下面的吞吐量计算感觉模模糊糊的,但是又不知道是作者自己想出来的还是参考了哪些文献....

作者首先定义了吞吐量的计算公式,即APP运行期内总的回收内存大小/总的回收耗时。但这个太难计算,所以又定义了一个周期,即从一次major gc结束开始,到下一次major gc结束时结束。这个周期内包含一次major gc和若干个minor gc。假设每次minor gc的耗时差不多(都是Tminor),一次major gc的耗时差不多(都是Tmajor)。巴拉巴拉....

下面就到头晕的地方了....

上面的公式里其实做了不少假设。比如,minor gc后,应用大概都会分配dS大小的非垃圾内存.....但我感觉还不止这些假设,所以这一章其实难度挺大(现在理解美亚那个评论所体现的无奈了吧)。

最后,作者得到一个吞吐量计算公式。在Fmax/dS/Tmin/Tmax固定的情况下,解了一个微分方程。然后,下面又假设Fmin=16MB,做了一组吞吐量测试。这个逻辑确实跳跃太大(实在没明白这个图是怎么画出来的).....

上图左下角的优化前提条件以及最终的结论(早一点开始做Major GC)。

接着,对提高吞吐量的第二个设计考虑是使用分代GC还是不分代GC

以上是一些关键知识提炼。最终的结论如下:

按作者的测试结果,分代GC的吞吐量一开始比不分代GC低,但后面又高了。所以,作者提供的优化手段就是开始做不分代GC,吞吐量高。到某个点后,改做分代GC.....优化的思路就是这么简单粗暴。当然,具体怎么做到还是挺考验的。但目标确实就是这么直接..

提高吞吐量的最后一个设计考虑如下,这个主要从提高运行速度来看的。

GC优化之提高Scalability

GC的另一个优化是如何利用多核优势,提高吞吐量。这一章更枯燥,建议了解要做什么先..

上图中解释了concurrent gc和parallel gc的区别。concurrent gc强调的是mutator和collector的关系,并行工作。而parallel gc强调的是有多个collector的并行工作。

这一章讨论的内容见1、2、3的介绍。再次强调,本书的一个非常大的特点是很多同级的内容并不是并列关系。比如上图中所说的1和2。Object Traversal包含了Object Marking。另外,请注意Mark-Stack这个数据结构。这也是ART一书中GC部分常见的词。还有,书中说,根对象枚举由于需要暂停mutator的运行,Scalability对这个场景没有什么意义…

Traversal其实就是为了mark。但单独讲mark,只是mark这块还可以做一些别的优化设计…

针对多核,优化设计考虑点之一就是Parallel object traversal。由于被扫到的对象都要加到MarkStack里,这就变成一个典型的多写/多读的生产者/消费者模型。见下图解释:

当然,如果要加上负载均衡等处理,情况又会复杂多了。总之,作者在这一章里提到的三种优化方法都是更好解决多写/多读问题的。具体细节不讨论。有人会觉咱这篇文章里可能什么也没说。其实,你要是没有机会看到这篇文章的话,我有90%的几率预测你可能看不懂原文(或者是不知道原文在讨论什么)

接下来讨论的Parallel Object Marking。

正如我前面所说,上面两页图的内容在原文中是并列的标题。但显然Object Marking讨论的只是Traversal的一个小点....

最后,这一章还讨论一个优化设计点——Parallel Compaction。

先留着吧。等你看到原文这部分的时候再来回顾....

GC优化之提高Responsiveness

提高响应速度,这恐怕是很多键盘侠都能出来说道几句的地方。这个也是成熟技术了,我感觉主要手段还是concurrent....

优化设计考虑点之一——Concurrent Tracing。关键内容如下:

三个基本保证很重要。作者讨论相关GC方法时,最终要归结到满足这三个条件。

  • 不能漏掉一个存活对象

  • 可以保留一些垃圾对象,但最终要能回收它们

  • Tracing要能结束,不能陷入死胡同出不来。

Concurrent Tracing的整体情况如上图所示。首先,要明确Root根对象枚举完之后,才有concurrent之说。针对图中3中最后提到的问题,设计了下面三种办法。

本文只结束Snapshot-At-The-Beginning法。借助Write Barrier,当对象的成员变量被修改的时候,我们记住。注意看上面的示意图。

  • A.f1,f2,f3分别指向B、C、D三个对象。

  • 此时,A.f1赋值给了a,随后A.f1指向X。那么,A.f1原来指向的B对象就没有被记住。也不可能被找到。因为A被标记过了,不会再次被标记。

所以,借助Write-Barrier,我们主动记住B。相当于B也是被记住的对象。B需要被记住的原因是它可能是一个垃圾对象,也可能不是一个垃圾对象。不能现在就把它看做是垃圾对象(否则一旦被回收就会出问题。要注意的点是,mutator和collector此时是同时工作的)。SATB(Snapshot-At-The-Beginning)法就是这么个意思。里边还有一些变种方法。

接下来的优化设计考虑点是concurrent root-set enumeration。这个是有点难度。对这个优化点的真实含义需要仔细阅读原文。即它讨论的是多个mutator之间如何concurrent进行root-set枚举。

ART里,貌似没有使用“这么高级”(也可能是没有成熟实现)的办法。ART里线程栈中根对象的Visit属于NonCurrentRoot,是要暂停所有mutator后才能访问的....

Concurrent Moving Colleciton

这一章的安排也是中了我上面说内容并排的问题。前面三章从Throughput、Scalability、Responsiveness三个方面讲如何优化GC。这一章突然就是某个具体的GC方法...

ART里也有地方用到了concurrent moving gc法。注意下面的关键步骤。

CMC(Concurrent Moving Collection的缩写)优化设计考虑是一个出发点。就是mutator是否看到两个Obj A(一个是原Obj A,在From-Space里,一个是拷贝过去的Obj A',在To-Space里)。如果只看到To-Space里的Obj A,这个设计就叫To-Space Invariant。本篇主要介绍这个设计

注意2.a的处理。Rootset枚举完后,需要先把Root Set里的根对象移动到To-Space里。然后才恢复mutator的运行。

而关于2.b的处理,则借助了Read Barrier。见下图。

ART代码里会经常出现kUseBakerReadBarrier的常量。这个就是上图示意的Read-Barrier,其实严格来说是Load Barrier。因为不光Read,Write的时候也要做一些工作。这个方法最早是Baker提出来的.....

以上,就是和GC有关的优化部分。难度相当大。而且,原文也没有和参考文献做一个比较明确的关联,所以读起来会比较痛苦。我建议还是了解目的,具体怎么弄的,倒是可以结合ART的源码进行细致研究。

Lock实现和基于硬件的内存事务

本书最后两章分别讨论了对Lock的优化以及对基于硬件的内存事务技术在JVM上可能带来的帮助。

先看对Lock的介绍

说实话,我觉得不如直接看ART一书的第12.3节。ART的Lock综合了上图中提到的几种技术。上面的讨论其实更多像是一个回顾(当然,你要是不了解ART做法的话,不会有这种感觉)。这几项技术倒也不复杂,建议直接看我书(结合代码)算了。比这里讨论的强(我认为作者是在回顾这些优化技术,而不是提出什么新的东西)

本书最后一章介绍了基于硬件的事务内存技术在JVM实现里可能的用处。我不想讲太多,这个看下图的左下角一部分概要介绍即可。

JVM读书笔记总结

这本书总体上来说内容相当丰富。JVM本身是一门工程。工程的话,经验、某些领域优化的积累会相对较多,没有太多条条框框的理论(GC其实更多是一种优化)。在阅读ART源码的时候,注定会碰到本书提到的内容。所以,这本书应该是一本集成汇总类的书籍(其参考的各种文献材料也是后续做深入研究的宝库)。

最后的最后

  • 我期望的结果不是朋友们从我的书、文章、博客后学会了什么知识,干成了什么,而应该是说,神农,我可是踩在你的肩膀上的喔。

  • 关于学习方面的问题,我已经讨论完了。后面这个公众号将对一些基础的技术,新技术做一些学习和分享。也欢迎你的投稿。不过,正如我在公众号“联系方式”里说的那样——郑渊洁在童话大王《智齿》里有一句话令我印象深刻,大意是“我有权保持沉默,但你说的每一句话都可能成为我灵感的源泉”。所以,影响不是单向的,很可能我从你那学到的东西更多。

神农和朋友们的杂文集

长按识别二维码关注我们

猜你喜欢

转载自blog.csdn.net/Innost/article/details/107602892