1. JVM G1 设计的核心机制
1.1. 简介 RSet记忆集 Remember Set
JVM垃圾回收算法,包括标记算法,标记需要回收的对象、存活对象,然后进行垃圾回收。通过根对象gc roots
可达性分析算法来标记(即从根对象gc roots
出发,标记所有存活对象,然后遍历对象的每个字段,继续标记,直到所有的对象标记完毕)。要么是存活对象,要么是垃圾对象需要被回收。
分代模型的JVM GC中,老年代和新生代的回收,一般是不同的,新生代一般先触发,无法腾出足够的空间,才会进入老年代GC,或者full gc。在G1中,先新生代回收,没有多余空间,就mixed gc,最后才可能会full gc。
RSet记忆集,是针对分代模型垃圾回收设计的数据结构。由于新生代,老年代gc的阶段不同,如果只回收新生代,按照gc roots
可达性分析算法,就会把老年代全部都给标记一遍,此时不打算回收老年代,这样会非常浪费时间。
可能会有老年代的对象引用新生代的对象。在触发新生代gc时,在老年代的里面有一些对象也在引用链中。
RSet记忆集,就是为了解决跨代引用问题而设计的。RSet记忆集,用key - value结构,记录了跨代引用的引用关系,在gc的时候,可以快速的借助记忆集 + gc roots
解决同代引用及跨代引用的可达对象分析问题。
1.2. 简介 位图--bitMap
混合回收的并发标记阶段,使用位图来记录判定内存使用状态。
简单粗暴的方式,是直接遍历整个内存块,看各个内存块有没有数据存储,没有就认为是空闲的。有数据就认为是使用中的。很明显这样是不行的,效率太低了。
为了描述内存的使用状态,G1采取了位图的方式来做描述。在一个位图里面记录所有内存的使用状态。要看内存是否被使用,直接访问位图中这块内存对应的坐标的内容,就能知道内存是否已经使用。
举个例子:
位图,就是通过“位”来描述某一块内存的状态的图。
计算机是二进制,存储数据的最小单位,是字节。一个字节,占用了8位的二进制位。
位图是,每一个位的0、1来标识描述的内容的状态。如上图中,内存是否使用。G1中引入了位图来描述内存使用状态,主要在混合回收的并发标记阶段使用位图这种数据结构,提高内存是否使用的判定效率。
1.3. 简介 卡表 -- cardTable
卡表和位图比较类似,都是用一段数据描述一块内存的情况。跟位图不一样的地方是:位图只能用一位来描述使用情况(一个位只有0、1两种状态)。而卡表描述了更多的信息(比如内存是否使用,使用大小,内存的引用关系等),使用一个字节(8位)来描述一块内存的使用情况。
本质上卡表在数据结构层面和位图没有太大区别。只是描述比位图长,描述的内容比位图多。
在G1中,卡表用一个字节(8位)来描述512字节的空间的使用情况 及 内存被谁使用了。并且是全局卡表,即整个堆内存公用一个全局卡表来描述全局的内存使用情况及对象引用关系。
512字节的内存,可能会被引用多次,同一个对象,被多个对象引用。所以,卡表的描述,是大概的引用关系描述。
在G1中,JVM使用 RSet+卡表 来解决 分代模型中跨代引用 关系不好判定,不好追踪的问题。
1.4. 怎么来提升GC效率
JVM的垃圾回收效率主要是在下述方面体现的:
- 标记垃圾对象
- 清理垃圾,整理内存
在标记过程中,会非常非常耗时,标记过程,分成很多个步骤:初始标记、并发标记、重新标记、并发回收等。分成多个阶段就是为了部分阶段可以并行处理,提高效率。耗时主要是因为 引用关系不好判定,内存是否使用不好判定。
初始标记时,从gc roots
出发,标记所有直接被引用的关系。在并发标记阶段,追踪所有间接被引用的对象,如果出现跨代引用(新生代对象,被老年代引用),这样的对象是不能被回收的,Rset可以避免对老年代的遍历。
Rset不会直接记录哪个对象引用了哪个对象。cardTable里面可以用一个字节来描述512字节内存的引用关系。Rset里面记录cardTable相关的内容,这样结合使用判断可以提升效率。
例如,老年代 一块512B的空间里,有对象引用了新生代的对象,Rset会记录这个512B的空间在卡表里面的位置。判定引用关系时,会通过 Rset 找到对应卡表,然后通过卡表详细信息判断引用状态。
位图和并发标记是息息相关的,在并发标记阶段,可以借助位图描述内存使用情况,避免内存使用冲突的问题,也避免GC线程无效遍历未使用的内存。