问题
非移动GC一定比移动GC好吗?
基础知识
移动GC和非移动GC
移动GC
在进行垃圾回收时,为了减少碎片而移动对象来顺利完成垃圾回收的GC。
Serial GC
在单线程环境下,它在标记-清除(Mark-Sweep)算法的基础上进行,并且在执行垃圾收集时,会暂停所有应用线程。虽然它不是移动对象,但在某些情况下,它会移动对象以减少碎片。
Parallel GC
在多线程环境下,它通过多个线程并行执行大多数垃圾收集工作,以提高性能。在某些实现中,它可能包括对象移动以减少碎片。
非移动GC
在进行垃圾回收时,不移动对象,通过复制对象或者采用不同的策略来管理和回收内存。
G1 GC
G1将堆内存分割成多个区域,并优先处理垃圾最多的区域。G1不移动对象,而是通过复制存活对象到未使用的区域来处理内存回收。
CMS GC
在需要快速响应的应用场景下,CMS在应用程序运行的同时进行标记和清除工作,但不移动对象。
ZGC
采用多重映射和读屏障,指针染色,并发重映射,转发表,NUMA支持等技术来管理和回收内存。
Shenandoah
采用并发执行,大堆等技术支持来管理和回收内存。
GC的局部性
GC进行垃圾回收时,会保持内存的局部性,从而保证GC运行的效率。
时间局部性
如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。程序循环、堆栈等是产生时间局部性的原因。
空间局部性
在最近的将来将用到的信息很可能与正在使用的信息在空间地址上是临近的。
顺序局部性
在典型程序中,除转移类指令外,大部分指令是顺序进行的。指令的顺序执行、数组的连续存放等是产生顺序局部性的原因。
GC局部性原理
调整数据的访问顺序
优化循环逻辑和数组访问顺序,减少缓存冲突,提高缓存命中率。
二进制文件中代码段对齐
优化代码段的排列,使得部分代码段能集中在一个缓存中,提高代码段的缓存命中率。
实验
测试用例源码
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.*;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 1, jvmArgsAppend = {"-Xms8g", "-Xmx8g", "-Xmn7g" })
public class ArrayWalkBench {
@Param({"16", "256", "4096", "65536", "1048576", "16777216"})
int size;
@Param({"false", "true"})
boolean shuffle;
@Param({"false", "true"})
boolean gc;
String[] arr;
@Setup
public void setup() throws IOException, InterruptedException {
arr = new String[size];
for (int c = 0; c < size; c++) {
arr[c] = "Value" + c;
}
if (shuffle) {
Collections.shuffle(Arrays.asList(arr), new Random(0xBAD_BEE));
}
if (gc) {
for (int c = 0; c < 5; c++) {
System.gc();
TimeUnit.SECONDS.sleep(1);
}
}
}
@Benchmark
public void walk(Blackhole bh) {
for (String val : arr) {
bh.consume(val.hashCode());
}
}
}
- 数组的大小。不同规模的数组大小更能凸显差异。
- 打乱顺序。表示我们是否在遍历数组之前对其进行打乱。启用打乱可模拟插入不按顺序和/或随时间推移发生在不同索引上的情况。
- 垃圾回收。表示在准备好数据集后强制执行 GC。由于工作负载未在有效负载代码中分配,因此我们需要明确强制执行 GC,否则它将永远不会运行。
运行结果
Benchmark (gc) (shuffle) (size) Mode Cnt Score Error Units
ArrayWalkBench.walk false false 16 avgt 5 0.051 ± 0.001 us/op
ArrayWalkBench.walk false false 256 avgt 5 0.821 ± 0.001 us/op
ArrayWalkBench.walk false false 4096 avgt 5 14.516 ± 0.026 us/op
ArrayWalkBench.walk false false 65536 avgt 5 307.210 ± 4.789 us/op
ArrayWalkBench.walk false false 1048576 avgt 5 4306.660 ± 7.950 us/op
ArrayWalkBench.walk false false 16777216 avgt 5 60561.476 ± 925.685 us/op
ArrayWalkBench.walk false true 16 avgt 5 0.051 ± 0.001 us/op
ArrayWalkBench.walk false true 256 avgt 5 0.823 ± 0.003 us/op
ArrayWalkBench.walk false true 4096 avgt 5 18.646 ± 0.044 us/op
ArrayWalkBench.walk false true 65536 avgt 5 461.187 ± 31.183 us/op
ArrayWalkBench.walk false true 1048576 avgt 5 16350.706 ± 75.757 us/op
ArrayWalkBench.walk false true 16777216 avgt 5 312296.960 ± 632.552 us/op
ArrayWalkBench.walk true false 16 avgt 5 0.051 ± 0.001 us/op
ArrayWalkBench.walk true false 256 avgt 5 0.820 ± 0.004 us/op
ArrayWalkBench.walk true false 4096 avgt 5 13.639 ± 0.063 us/op
ArrayWalkBench.walk true false 65536 avgt 5 174.475 ± 0.771 us/op
ArrayWalkBench.walk true false 1048576 avgt 5 4345.980 ± 15.230 us/op
ArrayWalkBench.walk true false 16777216 avgt 5 68687.192 ± 1375.171 us/op
ArrayWalkBench.walk true true 16 avgt 5 0.051 ± 0.001 us/op
ArrayWalkBench.walk true true 256 avgt 5 0.828 ± 0.010 us/op
ArrayWalkBench.walk true true 4096 avgt 5 13.749 ± 0.088 us/op
ArrayWalkBench.walk true true 65536 avgt 5 174.230 ± 0.655 us/op
ArrayWalkBench.walk true true 1048576 avgt 5 4365.162 ± 88.927 us/op
ArrayWalkBench.walk true true 16777216 avgt 5 70631.288 ± 1144.980 us/op
如上述运行结果可知,遍历打乱顺序的数组确实比初始的有序数组慢得多,这主要是有对象的内存布局决定的!并且发现在使用ParallelGC后,将内存的数组在内存进行重新排列,从而有利于提升局部性执行的效率。
Benchmark (gc) (shuffle) (size) Mode Cnt Score Error Units
ArrayWalkBench.walk false false 16 avgt 5 0.051 ± 0.001 us/op
ArrayWalkBench.walk false false 256 avgt 5 0.826 ± 0.006 us/op
ArrayWalkBench.walk false false 4096 avgt 5 14.556 ± 0.049 us/op
ArrayWalkBench.walk false false 65536 avgt 5 274.073 ± 37.163 us/op
ArrayWalkBench.walk false false 1048576 avgt 5 4383.223 ± 997.953 us/op
ArrayWalkBench.walk false false 16777216 avgt 5 60322.171 ± 266.683 us/op
ArrayWalkBench.walk false true 16 avgt 5 0.051 ± 0.001 us/op
ArrayWalkBench.walk false true 256 avgt 5 0.826 ± 0.007 us/op
ArrayWalkBench.walk false true 4096 avgt 5 18.169 ± 0.165 us/op
ArrayWalkBench.walk false true 65536 avgt 5 312.345 ± 26.149 us/op
ArrayWalkBench.walk false true 1048576 avgt 5 16445.739 ± 54.241 us/op
ArrayWalkBench.walk false true 16777216 avgt 5 311573.643 ± 3650.280 us/op
如上述运行结果可知,如果使用非移动的GC可能就没有这样的优势,从而导致非移动GC在这种情况下可能比移动GC进行垃圾回收的效率要低!
那出现这种结果的原因是什么呢?我们可以通过-prof perfnorm来进一步分析,执行结果如下:
Benchmark (gc) (shuffle) (size) Mode Cnt Score Error Units
walk true true 1048576 avgt 25 4.247 ± 0.090 ns/op
walk:CPI true true 1048576 avgt 5 0.498 ± 0.050 #/op
walk:L1-dcache-load-misses true true 1048576 avgt 5 0.955 ± 0.025 #/op
walk:L1-dcache-loads true true 1048576 avgt 5 12.149 ± 0.432 #/op
walk:L1-dcache-stores true true 1048576 avgt 5 7.027 ± 0.176 #/op
walk:LLC-load-misses true true 1048576 avgt 5 0.156 ± 0.029 #/op
walk:LLC-loads true true 1048576 avgt 5 0.514 ± 0.014 #/op
walk:cycles true true 1048576 avgt 5 17.056 ± 1.673 #/op
walk:instructions true true 1048576 avgt 5 34.223 ± 0.860 #/op
walk false true 1048576 avgt 25 16.155 ± 0.101 ns/op
walk:CPI false true 1048576 avgt 5 1.885 ± 0.069 #/op
walk:L1-dcache-load-misses false true 1048576 avgt 5 1.922 ± 0.076 #/op
walk:L1-dcache-loads false true 1048576 avgt 5 12.128 ± 0.326 #/op
walk:L1-dcache-stores false true 1048576 avgt 5 7.032 ± 0.212 #/op
walk:LLC-load-misses false true 1048576 avgt 5 1.031 ± 0.032 #/op
walk:LLC-loads false true 1048576 avgt 5 1.365 ± 0.101 #/op
walk:cycles false true 1048576 avgt 5 64.700 ± 2.613 #/op
walk:instructions false true 1048576 avgt 5 34.335 ± 1.564 #/op
如上述运行结果所示,由于使用的是随机版本,内存的随机游走会消耗大量的时间成本,所以导致每条指令大约需要 2 个时钟,并且几乎每个最后一级缓存加载都会失败。
总结
垃圾回收的局部性与对象图的布局有关,而对象图的布局伴随着程序的运行是动态变化的,因此GC需要一直动态的适应这种变化的对象图的布局,从而保证局部性的优势,这在现实应用场景下是很难实现的,而进一步讲,如果考虑NUMA特性,其对于垃圾回收的局部性更是灾难,所以所以不考虑垃圾回收局部性就一味的推崇非移动垃圾回收器的说法是错误的。