JVM垃圾回收--回收算法详解

2022-04-14 09:54:34 浏览数 (1)

一分钟的深入思考抵得过一小时的盲目寻找

根据对Java对象生命周期的统计,大部分对象只存活一小段时间,存活下来的对象能存活很长时间。Java虚拟机分代回收的思想,也就是从这个统计进行设计的。分代设计就是将堆划分为年轻代和老年代,对象存活时间很短就在年轻代,存活很长时间,就把这个对象移动到老年代。基于分代,就可以针对不同区域使用不同的算法了。年轻代使用耗时较短的回收算法也就是所说的Minor GC,大量的存活下来的对象占据老年代,到一定量级,那么根据算法就会触发全堆扫描--》FULL GC,这个时候就是我们所说的 Stop-the-world。

根据分代思想,我们可以看看Java 虚拟机的堆划分。

JVM将新生代划分为三个区,一个Eden区,两个大小相同的Survivor区。我们新创建的对象,new出来的会放到Eden区中,Eden区中的临时对象会在这里,如果Eden区中的对象进行一次Minor Gc,不能被回收的对象会放到 Survivor的一个区中,每一次进行Minor GC都会进行一次对象的搬运,从Eden区搬运到Survivor区中某一个区域,对象每一次进行搬运都会计数一次,当计数到达15次后,就会把这个合格的对象搬运到永久代中。

卡表--老年代引用新生代对象的解决方案:

堆空间被划分为年轻代和老年代,由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,

那么,需要跟踪从老年代到新生代的所有引用,从而避免每次Minor GC时扫描整个老年代,减少开销。

虽然做了分代,但是由于对象之间的相互调用,老对象又和新对象产生了引用关系,那么就得跑到老年代扫一遍

才能知道引用的新对象是否也该回收了,其实就是循着绳子找东西,本来院子里面的东西要搬走,但是屋子里面的有根绳子

连着院子里面的东西,那么就得到屋子里面寻摸一遍,看看绳子还连着根儿没有,能搬走不能。

对于HotSpot JVM,使用了卡标记(Card Marking)技术来解决老年代到新生代的引用问题。

具体是,使用卡表(Card Table)和写屏障(Write Barrier)来进行标记并加快对GC Roots的扫描。

这个操作在解释执行器中比较容易实现。但是在即时编译器生成的机器码中,则需要插入额外的逻辑。

这也就是所谓的写屏障(write barrier,注意不要和 volatile 字段的写屏障混淆)。

基于卡表(Card Table)的设计,通常将堆空间划分为一系列2次幂大小的卡页(Card Page)。

卡表(Card Table),用于标记卡页的状态,每个卡表项对应一个卡页。

HotSpot JVM的卡页(Card Page)大小为512字节,卡表(Card Table)被实现为一个简单的字节数组,即卡表的每个标记项为1个字节。

当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为dirty。

首先,计算对象引用所在卡页的卡表索引号。将地址右移9位,相当于用地址除以512(2的9次方)。

可以这么理解,假设卡表卡页的起始地址为0,那么卡表项0、1、2对应的卡页起始地址分别为0、512、1024(卡表项索引号乘以卡页512字节)。

其次,通过卡表索引号,设置对应卡标识为dirty。

1.无条件写屏障带来的性能开销#

每次对引用的更新,无论是否更新了老年代对新生代对象的引用,都会进行一次写屏障操作。

显然,这会增加一些额外的开销。但是,与YGC时扫描整个老年代相比较,这个开销就低得多了。

不过,在高并发环境下,写屏障又带来了虚共享(false sharing)问题。

2.高并发下虚共享带来的性能开销#

在高并发情况下,频繁的写屏障很容易发生虚共享(false sharing),从而带来性能开销。

假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行。

HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共64*512=32KB。

如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能。

一个简单的解决方案,就是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表项未被标记过才将其标记为dirty。

这就是JDK 7中引入的解决方法,引入了一个新的JVM参数-XX: UseCondCardMark,在执行写屏障之前,先简单的做一下判断。如果卡页已被标识过,则不再进行标识。

简单理解如下:

Copy

if (CARD_TABLE [this address >> 9] != 0)

CARD_TABLE [this address >> 9] = 0;

与原来的实现相比,只是简单的增加了一个判断操作。

虽然开启-XX: UseCondCardMark之后多了一些判断开销,但是却可以避免在高并发情况下可能发生的并发写卡表问题。通过减少并发写操作,进而避免出现虚共享问题(false sharing)。

垃圾回收算法还有很多,思路和思想都是提高回收效率,减少对系统的影响,另外还有一个空间利用率问题 。

备注:文中一部分是基于自己整理,一部分是对网络上的内容的摘录整合。

0 人点赞