【问题标题】:Java pure existance of large array causes massive memory spikes大型数组的 Java 纯存在导致大量内存峰值
【发布时间】:2020-08-14 17:01:31
【问题描述】:

我目前正在编写一个 UCI 国际象棋引擎。在我的引擎内部,我有一个大的转置表,它将键映射到值。

在实现这一点时,我已经记住了内存,并且不想在表格中放置东西时创建新对象,所以我开始用空对象填充整个表格。

代码如下所示:

private TranspositionEntry[] entries;
private int size;

private int maxSize;
private long hashMask;

public TranspositionTable(int keyBits){
    this.maxSize = (int)(Math.pow(2, keyBits));
    this.hashMask =  maxSize - 1;
    this.entries =  new TranspositionEntry[maxSize];
    for(int i = 0; i < maxSize; i++){
        this.entries[i] = new TranspositionEntry(0,0,0,0,0, new Move(0, 0, 0, 0));
    }
}

private int index(long zobrist){
    return (int)(zobrist & hashMask);
}

public boolean contains(long key){
    int index = index(key);
    return entries[index].getZobrist() != 0;
}

public void clear(){
    for(int i = 0; i < maxSize; i++){
        this.entries[i].setZobrist(0);
    }
}

public int size(){
    return size;
}

public TranspositionEntry get(long key){
    System.out.println("I was called");
    int index = index(key);
    if(entries[index].getZobrist() == 0) return null;
    return entries[index];
}

public void put(long key, double eval, int depthLeft, int node_tpe, int color, Move move){
    int index = index(key);

    System.out.println("I was called");

    TranspositionEntry en = entries[index];

    if(en.getZobrist() == 0) size++;

    en.setVal(eval);
    en.setDepthLeft(depthLeft);
    en.setNode_type(node_tpe);
    en.setColor(color);

    en.getBestMove().setType(move.getType());
    en.getBestMove().setFrom(move.getFrom());
    en.getBestMove().setTo(move.getTo());
    en.getBestMove().setPieceFrom(move.getPieceFrom());
    en.getBestMove().setPieceTo(move.getPieceTo());
}

这是一个非常非常基本的哈希算法,但这与这个问题无关。 我通过运行这段代码测试了这个对象消耗了多少内存:

InstrumentationAgent.printMemoryOverview();
TranspositionTable<TranspositionEntry> entryTranspositionTable = new TranspositionTable<>(20);
InstrumentationAgent.printMemoryOverview();
System.out.println(entryTranspositionTable);

这给了我以下输出:

Used Memory   : 7 MB
Free Memory   : 483 MB
Total Memory  : 491 MB
Max Memory    : 7268 MB

Used Memory   : 100 MB
Free Memory   : 390 MB
Total Memory  : 491 MB
Max Memory    : 7268 MB

ai.tools.transpositions.TranspositionTable@1b6d3586

所以你可以看到这个表本身确实消耗了大约100MB 的内存。


现在有趣的部分来了:

当我运行我的代码来寻找给定位置的最佳移动时,我 通常首先创建转置表(或清除它,如果它 已经存在)。但我禁用了对转置的 ALL 访问 size() 除外。如上所示,表中有get/put 方法 带有print-statement 并且它们在任何时候都不会被调用。

VisualVM 在搜索位置时给我这个内存输出 转置表已在开始时创建但未使用 随时。

如您所见,内存使用量一开始就很高,然后收敛到相当低的水平。 我最初认为搜索本身在开始时会消耗这么多内存(这没有意义)。所以我运行了完全相同代码,除了this._transpositionTable = new TranspositionTable(20);

输出如下:

如您所见,搜索本身不会导致这些内存峰值。

所以我的问题是:

  1. 为什么纯粹存在未使用的数组会导致这些内存峰值,尤其是仅在代码开头
  2. 这个问题有解决办法吗?

解决这个问题对我来说非常重要,因为在测试时,我需要同时运行多个引擎,所以内存是个问题。我很高兴得到任何帮助或建议!

您好, 芬兰人

编辑 1:

添加转置入口代码:

private double val;

private long zobrist;

private int depthLeft;

private int node_type;
private int color;
private Move bestMove;


public TranspositionEntry(long zobrist, double val, int depthLeft, int node_type, int color, Move bestMove) {
    this.val = val;
    this.zobrist = zobrist;
    this.depthLeft = depthLeft;
    this.node_type = node_type;
    this.color = color;
    this.bestMove = bestMove;
}

编辑 2

我找到了解决这个问题的方法。 如果从一开始就限制了最大堆空间,则似乎不会出现尖峰。 我添加了xmx1024M,这似乎解决了问题。

【问题讨论】:

  • 表项的创建是在表的构造函数中完成的
  • 另外,为什么这会使用超过 1G 的 RAM?
  • ``` public TranspositionEntry(long zobrist, double val, int depthLeft, int node_type, int color, Move bestMove) { this.val = val; this.zobrist = zobrist; this.depthLeft = depthLeft; this.node_type = node_type; this.color = 颜色; this.bestMove = 最佳移动; } ```
  • 实际上只是复制这些值
  • 不要将代码放在 cmets 中,它不可读。只需编辑问题。

标签: java arrays memory heap-memory


【解决方案1】:

除非表的很大一部分已填满(例如 80%),否则您应该按需创建(和删除)条目以节省内存。 Java 中的对象存在,无论它们是否在数组(或其他对象)中被引用,直到垃圾收集器清理它们,在最后一个非弱引用被清除并且最后一个线程失去对该对象的访问权之后。即使这样,垃圾收集器也可能需要很长时间才能清理对象,具体取决于它的设置。最重要的是,一旦分配了内存,JVM 就不愿意返回内存,因为重新分配内存往往代价高昂。但是,以上所有内容都取决于实现。

填写表格的具体建议,编写ensureEntry 方法,按需在表格中创建对象,因此您不必在启动时对其进行初始化:

public TranspositionEntry ensureEntry(int index) {
    TranspositionEntry entry = this.entries[index];
    if (entry == null) {
        entry = new TranspositionEntry(0, 0, 0, 0, 0, new Move(0, 0, 0, 0));
        this.entries[index] = entry;
    }
    return entry;
}

这样的方法在运行时是非常轻量级的,因为编译器很可能会内联它,它对解决你的内存问题有很大帮助。只需使用它来访问您的表格。

如果您需要再次从表中删除条目,则编写类似的方法来写入您的表,如果“零大小写”为真,则将数组索引设置为null。不要害怕null!如果做得好,无效的引用是你的朋友。只是不要忘记,他们就在那里。

并解决“为什么空数组占用空间?”的问题

对象数组(与原始类型数组相反)分配的 vm 内存刚好足以存储它可能包含的所有引用,但不分配实际对象的空间。因此,如果您创建一个数组来保存 100 个Strings,那么您的数组将采用其自身开销的内存大小 + 100 个对象引用(无论它们是否存在)。但是,引用的大小是特定于操作系统和 VM 的,并且可能会有所不同,您需要为每个对象引用分配至少 32 位和(通常)最多 64 位的属性。所以一个大小为 100 的字符串数组分配了 100 * 64 + 开销位的空间。

【讨论】:

  • 谢谢!我这样做并限制了最大堆大小,并且效果很好:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多