我在一些可能导致稳定数量的垃圾收集的事情上实现了自己的通行证。完整的代码可以在这里找到:https://gist.github.com/dimo414/5243162
重点是这两个方法,它们在给定的实时时间内(而不是线程时间或 CPU 时间)构造和释放大量对象:
/**
* Loops over a map of lists, adding and removing elements rapidly
* in order to cause GC, for runFor seconds, or until the thread is
* terminated.
*/
@Override
public void run() {
HashMap<String,ArrayList<String>> map = new HashMap<>();
long stop = System.currentTimeMillis() + 1000l * runFor;
while(runFor == 0 || System.currentTimeMillis() < stop) {
churn(map);
}
}
/**
* Three steps to churn the garbage collector:
* 1. Remove churn% of keys from the map
* 2. Remove churn% of strings from the lists in the map
* Fill lists back up to size
* 3. Fill map back up to size
* @param map
*/
protected void churn(Map<String,ArrayList<String>> map) {
removeKeys(map);
churnValues(map);
addKeys(map);
}
类实现Runnable
这样你就可以在它自己的后台线程中启动它(或同时启动多个)。只要您指定,它就会运行,或者如果您愿意,可以将其作为守护线程启动(因此它不会阻止 JVM 终止)并指定它永远运行0
秒作为构造函数参数。
我对此类进行了一些基准测试,发现它花费了近三分之一的时间进行阻塞(大概是在 GC 上),并确定了大约 15-25% 的流失率和约 500 的大小的最佳值。每次运行完成 60 秒,下图绘制了线程时间,如java.lang.managment.ThreadMXBean.getThreadCpuTime()
以及线程分配的总字节数,如报告所示com.sun.management.ThreadMXBean.getThreadAllocatedBytes()
.
控制(0% 流失)本质上不应该引入任何 GC,我们可以看到它几乎不分配任何对象,并且几乎 100% 的时间都花在线程中。从 5% 到 95% 的流失率,我们相当一致地看到大约三分之二的时间花在线程上,大概另外三分之一花在 GC 上。我想说,这是一个合理的百分比。有趣的是,在流失百分比非常高的情况下,我们看到线程上花费了更多的时间,大概是因为 GC 进行了如此多的清理,所以它实际上能够更加高效。每个周期搅拌大约 20% 的对象似乎是一个不错的数量。
这张图描绘了线程如何在映射和列表的不同目标大小下运行,我们可以看到,随着大小的增加,必须在 GC 上花费更多的时间,有趣的是,我们实际上最终分配了更少的对象,因为较大的数据大小意味着它无法在同一时间段内进行尽可能多的循环。由于我们有兴趣优化 JVM 必须处理的 GC 搅动量,因此我们希望它需要处理尽可能多的对象,并在工作线程中花费尽可能少的时间。因此,4-500 左右似乎是一个不错的目标大小,因为它会生成大量对象并在 GC 上花费大量时间。
所有这些测试都是按照标准完成的java
设置,因此使用堆可能会导致不同的行为 - 特别是,~2000 是我在堆填满之前可以设置的最大大小,如果我们增加堆的大小,我们可能会在更大的大小上看到更好的结果堆。