什么是堆写入流量以及为什么在ArrayList中需要它?

时间:2018-03-02 18:27:40

标签: java

我很想知道heap write traffic的含义以及为什么在ArrayList实现中需要它?

ArrayList实施的摘录,请参阅注释

的行
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
    Objects.requireNonNull(consumer);
    final int size = ArrayList.this.size;
    int i = cursor;
    if (i >= size) {
        return;
    }
    final Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length) {
        throw new ConcurrentModificationException();
    }
    while (i != size && modCount == expectedModCount) {
        consumer.accept((E) elementData[i++]);
    }
    // update once at end of iteration to reduce heap write traffic
    cursor = i;
    lastRet = i - 1;
    checkForComodification();
}

4 个答案:

答案 0 :(得分:2)

可能是作者希望使用局部变量i,因为在escape analysis and stack allocation启动时可能会分配堆栈。与cursor不同,i变量已更改多次由于i++循环内的while语句。在堆栈上增加它并且跳过所有Java内存模型含义应该更便宜。 Iterator.cursor是一个成员字段,它可能总是在堆上,尤其是Iterator个对象在用户代码中传递。

答案 1 :(得分:2)

通常cursor变量指向Iterator要返回的下一个元素。因此,在迭代时,您需要每次都更新cursor变量,以便它始终指向正确的元素。

但是,forEachRemaining方法自行完成迭代。这并不意味着暂停。因此,您可以忽略更新游标变量,直到方法完成。当方法迭代时,cursor将指向错误的元素。但是因为你不能暂停这个方法,所以没有任何区别。

通过这种方式,您可以将分配量减少到cursor,并减少堆流量。所以他们引用更正确的实现,如

while (i != size && modCount == expectedModCount) {
    consumer.accept((E) elementData[i++]);
    // Update cursor while iterating
    cursor = i;
}

或直接使用光标而不是额外的i

while (cursor != size && modCount == expectedModCount) {
    consumer.accept((E) elementData[cursor++]);
}

但是你要处理成员变量而不是局部变量i。与i合作更便宜,有关详细信息,请参阅@kdowbecki的答案。

答案 2 :(得分:1)

如果您忽略所有保护条件,next()会执行以下操作:

public E next() {
    Object[] elementData = ArrayList.this.elementData;

    int i = cursor;
    cursor = i + 1;
    lastRet = i;
    return (E) elementData[i];
}

forEachRemaining()基本上会继续调用next()并在每个元素上调用使用者,所以如果我们这样做,则内联next()逻辑,我们得到:

public void forEachRemaining(Consumer<? super E> consumer) {
    final int size = ArrayList.this.size;
    final Object[] elementData = ArrayList.this.elementData;

    int i = cursor;
    while (i != size) { // same as hasNext()
        // begin: consumer.accept(next())
        cursor = i + 1;
        lastRet = i;
        consumer.accept((E) elementData[i]);
        // end: consumer.accept(next())
        i++;
    }
}

由于cursorlastRet都是字段,因此它们存在于堆上,而堆栈中存在i

为减少内存写入次数,cursorlastRet的更新可以移出循环,因为它们实际上并未在循环中使用。

当然,您现在在一次额外i++之后再这样做了,因此您需要从1中减去i

public void forEachRemaining(Consumer<? super E> consumer) {
    final int size = ArrayList.this.size;
    final Object[] elementData = ArrayList.this.elementData;

    int i = cursor;
    while (i != size) {
        consumer.accept((E) elementData[i++]);
    }
    cursor = i;
    lastRet = i - 1;
}

效果是在迭代期间只更新堆栈变量i,并且两个堆值保持不变。

一旦JIT启动,如果内联accept()调用,堆栈变量i甚至可能被删除并变成一个寄存器值,大大减少了更新的次数。 #34;存储器中。

答案 3 :(得分:1)

感谢您提供答案。为了更好地解释,我将添加内部Java内存模型的外观,如下图所示。正如@kdowbecki,@ Zabuza和@Andreas指出的那样,使用Thread Stack内存进行本地执行然后在每次迭代中使用Heap内存是有效的。它可能属于amortized analysis类别。

Figure 1

在操作系统中检查memory model of a process(JVM是一个进程)也很有趣。

Process memory