Java 使用G1时,大量活动实例上的分配性能是否会降低?

Java 使用G1时,大量活动实例上的分配性能是否会降低?,java,performance,garbage-collection,g1gc,Java,Performance,Garbage Collection,G1gc,当我们的一些应用程序从CMS迁移到G1时,我注意到其中一个应用程序的启动时间延长了4倍。GC周期导致的应用程序停止时间不是原因。在比较应用程序行为时,我发现这个应用程序在启动后携带了高达2.5亿个活动对象(在12G的堆中)。进一步的调查显示,应用程序在最初的500万次分配期间速度正常,但随着活动对象池的增大,性能会越来越差 进一步的实验表明,一旦达到活动对象的某个阈值,当使用G1时,新对象的分配确实会减慢。我发现,将活动对象的数量增加一倍似乎会使分配所需的时间增加大约2.5倍。对于其他GC引擎,

当我们的一些应用程序从CMS迁移到G1时,我注意到其中一个应用程序的启动时间延长了4倍。GC周期导致的应用程序停止时间不是原因。在比较应用程序行为时,我发现这个应用程序在启动后携带了高达2.5亿个活动对象(在12G的堆中)。进一步的调查显示,应用程序在最初的500万次分配期间速度正常,但随着活动对象池的增大,性能会越来越差

进一步的实验表明,一旦达到活动对象的某个阈值,当使用G1时,新对象的分配确实会减慢。我发现,将活动对象的数量增加一倍似乎会使分配所需的时间增加大约2.5倍。对于其他GC引擎,该系数仅为2。这确实可以解释经济放缓的原因

不过,有两个问题使我对这一结论表示怀疑:

  • 大约500万个活动实例的阈值似乎与整个堆有关。有了G1,我会期望任何这样的降级阈值都与一个区域相关,而不是与整个堆相关
  • 我在网上到处寻找解释(或至少说明)这种行为的文件,但没有找到。我甚至没有找到类似“拥有超过xxx个活动对象是邪恶的”这样的建议
所以:如果有人能告诉我我的观察结果是正确的,也许能给我一些解释性的文件,或者是关于这个领域的一些建议,那就太好了。或者,有人告诉我我做错了什么

下面是一个简短的测试用例(运行多次,取平均值,扣除显示的垃圾收集时间):

import java.util.HashMap;
/**
*分配器演示活动对象数之间的依赖关系
*和分配速度,使用各种GC算法。
*使用,例如:
*java分配器-Xmx12g-Xms12g-XX:+printgapplicationstoppedtime-XX:+UseG1GC
*java分配器-Xmx12g-Xms12g-XX:+printgapplicationstoppedtime
*从执行时间中扣除停止时间。
*/
公共类分配器{
公共静态void main(字符串[]args){
定时器(2000000,真);
对于(int i=1000000;i=1000000;i/=2){
定时器(i,假);
}
}
专用静态无效计时器(int num,布尔预热){
很久以前=System.currentTimeMillis();
分配器a=新分配器();
int size=a.allocate(num);
long after=System.currentTimeMillis();
如果(!预热){
System.out.println(“分配“+num+”所需的时间:”
+(之后-之前)+“毫秒贴图大小=”+大小);
}
}
专用整数分配(整数){
HashMap=新的HashMap(2*numElements);
对于(int i=0;i
如上评论所述:

您的测试用例确实预先分配了非常大的引用数组,这些数组寿命很长,并且基本上占据了它们自己的一个区域(它们可能最终位于旧的gen或巨大的区域),然后用可能位于不同区域的数百万个额外对象填充它们

这会创建大量跨区域引用,G1可以以中等数量处理这些引用,但每个区域不能处理数百万个引用

G1的启发式算法也认为高度互联的区域收集起来很昂贵,因此即使它们完全由垃圾组成,也不太可能被收集

一起分配对象以减少跨区域参照

没有人为地延长它们的生命周期(例如,通过将它们放入某种缓存)也会导致它们在年轻一代GC中死亡,这比旧区域更容易收集,旧区域本质上积累了从不同区域引用的对象


因此,总的来说,您的测试用例对G1的基于区域的特性相当不友好。

GC日志(通过
PrintGCDetails
)并提及您正在使用的java版本会有帮助。实际应用程序的完整GC日志太大了。:)但无论如何,上面的测试用例展示了这种行为。请注意,这里的问题不是GC暂停。-我使用Java8更新45。行为与Windows和Linux相同。问题是测试用例是否会导致与实际工作负载相同的问题。拥有一个容纳数百万个对象的hashmap将在hashmap内创建一个非常大的引用数组,这可能需要一个区域,从而使从hashmap表到其节点的大多数引用跨区域,这需要更多的簿记。好主意,但单个大型阵列似乎也不是问题所在。原始应用程序将所有这些实例保存在ConcurrentHashMap中。我的第一个版本的测试用例也是这样做的。使用ConcurrentHashMap会创建大量(小的)附加实例,因此问题会更加突出。将测试用例中的单个HashMap替换为200个HashMap,所有这些HashMap都使用map[i%200].put()填充,这也会增强效果。请参阅我之前的评论:如果我不一次性创建所有HashMap,而是在创建后立即填充每个HashMap(以便在靠近其存储空间的位置构建元素),与单个HashMap相比,它似乎对G1有所帮助。但是,效果仍然非常明显。是的,测试用例是恶意的,但实际应用程序也是以完全相同的方式恶意的,因此测试用例完成了它的工作。:)我了解到,对于G1,必须确保跨区域引用的数量不会超过一百万。实现这一点的最简单方法通常是确保活动对象的总数不会增长到数百万个范围
import java.util.HashMap;

/**
  * Allocator demonstrates the dependency between number of live objects
  * and allocation speed, using various GC algorithms.
  * Call it using, e.g.:
  *   java Allocator -Xmx12g -Xms12g -XX:+PrintGCApplicationStoppedTime -XX:+UseG1GC
  *   java Allocator -Xmx12g -Xms12g -XX:+PrintGCApplicationStoppedTime
  * Deduct stopped times from execution time.
  */
public class Allocator {

public static void main(String[] args) {
    timer(2000000, true);
    for (int i = 1000000; i <= 32000000; i*=2) {
        timer(i, false);
    }
    for (int i = 32000000; i >= 1000000; i/=2) {
        timer(i, false);
    }
}

private static void timer(int num, boolean warmup) {
    long before = System.currentTimeMillis();
    Allocator a = new Allocator();
    int size = a.allocate(num);
    long after = System.currentTimeMillis();
    if (!warmup) {
        System.out.println("Time needed for " + num + " allocations: "
           + (after - before) + " millis. Map size = " + size);
    }
}

private int allocate(int numElements) {
    HashMap<Integer, String> map = new HashMap<>(2*numElements);
    for (int i = 0; i < numElements; i++) {
        map.put(i, Integer.toString(i));
    }
    return map.size();
}

}