
彭成寒
G1是目前最成熟的垃圾回收器,已经广泛应用在众多公司的生产环境中。我们知道,CMS作为使用最为广泛的垃圾回收器,也有令人头疼的问题,即如何对其众多的参数进行正确的设置。G1的目标就是替代CMS,所以在设计之初就希望降低程序员的负担,减少人工的介入。但这并不意味着我们完全不需要了解G1的原理和参数调优。笔者在实际工作中遇到过一些因参数设置不正确而导致GC停顿时间过长的问题。但要正确设置参数并不容易,这里涉及两个方面:第一,需要对G1的原理熟悉,只有熟悉G1的原理才知道调优的方向;第二,能分析和解读G1运行的日志信息,根据日志信息找到G1运行过程中的异常信息,并推断哪些参数可以解决这些异常。本书尝试从G1的原理出发,系统地介绍新生代回收、混合回收、Full GC、并发标记、Refine线程等内容;同时依托于jdk8u的源代码介绍Hotspot如何实现G1,通过对源代码的分析来了解G1提供了哪些参数、这些参数的具体意义;最后本书还设计了一些示例代码,给出了G1在运行这些示例代码时的日志,通过日志分析来尝试调整参数并达到性能优化,还分析了参数调整可能带来的负面影响。
1、垃圾标记的算法: (1)垃圾回收分成了垃圾标记和垃圾清理 垃圾标记分成了 【可达性分析】、【引用计数】 两种方式 (2)python还在使用引用计数,这里有判断逻辑,如果出现循环引用,使用可达性分析兜底。 补充:python这个还是别想了,引用技术和跨代引用之间有很多问题并不能解释清楚。 2、垃圾清理算法: (1)【标记清除】、【标记整理】、【复制算法】;G1使用的完全是复制算法。 (2)G1的内存组成,先分Young和Old,Young里面再分Eden和Survivor区分Young和Old的原因,是对象的生命周期差距很大,区分开之后可以减少无效扫描;Eden和Survivor是复制算法本来就需要至少两块,Survivor搞小一点是避免浪费,所以有下面的8:1:1。 (3)数据大小占比 复制算法涉及到Eden和Survivor来回倒,默认比例8:1:1,这个会动态计算。 新生代Young开局占总体内存的1/3,也是动态的,老年代占据的地盘更大;老年代默认给新生代保留10%的晋升空间。 补充:Young、Old比例这个完全没准,common-api的yong占了30%,api-service就能占50%,就看新对象垃圾多少了。 (4)CMS的标记整理算法 并发线程数通常会吃掉25%的CPU资源,它没有记忆集,全是CPU现场计算的。 3、记忆集RSet的维护 (1)【分区】,region,默认是2048个,用堆大小除以2048,每个region大小往2的幂次上去靠。按照8G内存来算,就是2M。 这个大小唯一的影响是超过region一半的对象,会直接进入老年代;region如果比较小,可以对内存进行精细化管理,但也会增加RSet管理的开销。 (2)【卡表】,card,默认是512字节。这个卡表只用来标识哪个区域脏了,每当有引用变更,G1会通过写屏障把卡表标记为脏卡。 后台会有一个Refine线程持续在刷这个脏卡,把它们扫一遍更新到RSet里。 【卡表这里终于看懂了】卡表的card是对region的极细粒度的细分,这个大小基本就是对象级别。当某个对象引用发生变化的时候,把card放到脏队列里面,基本等同于把对象放到脏队列里面!而记忆集不使用card也是因为card粒度过细,存储浪费成本太多了。 (3)【Dirty Card Queue队列】的维护,每次变更的时候,线程都先把这个变更消息存放在线程本地,超过一定数值之后再上报,当然也有超时时间兜底。 (4)【记忆集RSet】,分成了三种类型,按照Region为单位的 a.第一种类型是当前Region被外部引用的太少了,每次多一个外部引用都能精确记录到卡表指针,稀疏表。 b.第二种类型是数量多一点之后,每个card维护一个位图,把一个region分成512份,来表示对面对自己card的引用。 c.第三种类型是再多了,只能维护region对region的位图。 除了新生代对新生代自己之外,其他跨代引用和老年代对老年代引用都要记录。 4、【Young年轻代空间大小的推算】--G1的核心 年轻代空间大小的推算,受控于【两个时间】:G1的目的就是停顿时间可控 (1)【Eden的STW停顿时间】,这有一个上限 据不完全统计,每有1G的Eden,停顿时间就会上涨100ms,所以停顿时间和内存大小基本是相互转化的 (2)【G1线程总体占用的时间】,并发时间也算在里面,都是吃CPU时间片的 如果Eden太小了,GC就频繁,总体G1线程的时间就多,这个默认不能超过10%。 如果Eden大了,STW时间就多了,这个默认200ms。Eden每有1G通常就需要100ms。 (3)Young占总内存有一个上限的,这个上限默认是60%,老年代总得留点(其实根本到不了这个上限) 5、Eden清理的过程: (1)【STW】,使用CSet作为根集合进行扫描 (2)【Eden扫描和复制】是一体的,一旦发现对象存活,直接拷贝,内存完全copy一份不需要解析。 复制完之后老对象的对象头要做标记,指向新对象。 (3)【ReDirty】以及【重构卡表RSet】复制完之后,把Eden和指向Eden的老年代再扫描一遍,这一遍要把持有引用的那些对象,持有的引用变更一遍; 这时候Eden的内容已经都被复制到Survivor里去了,所以需要扫描的就是指向Survivor里面的这些对象,把引用变更到新对象上去。需要扫描的是之前的老年代RSet以及当前的Survivor。扫完一遍之后,引用变更了,RSet也重构出来了。(4)释放刚才回收的页面到【FreeList】,供下次申请内存用 6、快速分配和慢速分配 (1)TLAB占Eden的1%。每个线程创建的时候分配两段TLAB,本地内存,分配的时候先从这里出。 分配两段是因为有GC,GC的时候原来那段就不能用了,得分配到新空间里。 (2)【慢速分配就是尝试申请一段新的TLAB】;JVM分配光了就触发GC。 7、完整垃圾回收过程: (1)新生代回收 STW,深度递归遍历有存活对象就拷贝,拷贝完之后再遍历重构RSet和持有引用对象的引用值 (2)混合回收或者老年代回收 a. 混合回收默认在YGC之后内存还超过45%的时候开启 b. 初始标记,并发标记,再标记,并发清除 (3)三色标记详细过程 三色标记是并发标记过程中使用的算法,初始标记、并发标记、再标记三个阶段就是为了生产出一张【标记位图】来,用0101标识出哪个对象是垃圾,哪个是存活对象。 三色标记里有一类新对象和两类变更对象: a. 【新分配的对象利用TLAB的开始位置start做快照】,所有的对象都是在TLAB上分配的,TLAB不够会再申请。 b. 变更对象是在并发标记过程中,如果变化了就把对象扔到SATB队列中等待重新处理。
最好的GC就是让大家不需要关心GC,希望未来的Java不需要再读此类的书籍了。
代码的篇幅过大,非常不利于手机阅读。多一点概念和原理上的阐述比较好,书中不适合穿插大量代码,,毕竟书本中的代码不能像在IDE中一样有语法高亮,阅读不效率,太多了,看的累,极度影响阅读体验!
前面描述的看到还可以,也帮我解决了Java线程与OS线程如何关联起来的,后面的实在有很多东西吗接触到,完全不知所云[大哭],就很快速的看完,希望以后再看时,能好点[捂脸]
有实际优化经验来看这本书要好得多。
对读者的水平要求还是比较高。 作者的表达有时候确实有点问题,一些生僻的、易混淆的概念本可以归纳下的,但是文章整体结构的问题,读起来就让人觉得很费劲。比如在介绍mixed gc的各个流程的时候,并发标记、并发标记子流程这两个概念,在行文中有好几处混淆。 但总的说来,这是一本功底深厚的书。从基本原理(不仅仅只是G1中涉及的原理),到源码,以至于基本的优化方向、优化方法,作者都广泛涉猎、融汇贯通。 受益匪浅。
现在大部分公司都已经是jdk8了,并且在jdk11默认已经是G1,在互联网,业务熟练扩张的情况下需要高响应,低延迟的场景也越来越多,并且每一代gc回收器的目标都是为了减少stw.这个书对G1还是很全面的。
不知道作者心中的目标读者是什么样的。原理讲的不清楚,只是在堆砌细节,让人不知所云。
文字功底较差,文章组织讲解条理性不是很清晰
行文逻辑散乱,更像是作者自己的笔记