缘起
继续从读书笔记的角度来系统的学习《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源码的时候,注定会碰到本书提到的内容。所以,这本书应该是一本集成汇总类的书籍(其参考的各种文献材料也是后续做深入研究的宝库)。
最后的最后
我期望的结果不是朋友们从我的书、文章、博客后学会了什么知识,干成了什么,而应该是说,神农,我可是踩在你的肩膀上的喔。
关于学习方面的问题,我已经讨论完了。后面这个公众号将对一些基础的技术,新技术做一些学习和分享。也欢迎你的投稿。不过,正如我在公众号“联系方式”里说的那样——郑渊洁在童话大王《智齿》里有一句话令我印象深刻,大意是“我有权保持沉默,但你说的每一句话都可能成为我灵感的源泉”。所以,影响不是单向的,很可能我从你那学到的东西更多。
神农和朋友们的杂文集
长按识别二维码关注我们