我可以在java中连续分配对象吗?

时间:2012-03-09 10:12:54

标签: java performance caching memory garbage-collection

假设我有一大堆相对较小的对象,我需要经常迭代。
我想通过提高缓存性能来优化迭代,所以我想在内存上连续分配对象 [而不是引用],这样我就可以减少缓存未命中数,整体表现可能会明显好转。

在C ++中,我可以只分配一个对象数组,它会按照我的意愿分配它们,但在java中 - 在分配一个数组时,我只分配引用,并且分配是在一个对象上完成的。时间。

我知道如果我“一次”分配对象[一个接一个],jvm 很可能将对象分配为尽可能连续,但它可能不是如果内存碎片化就足够了。

我的问题:

  1. 有没有办法告诉jvm在我开始分配对象之前对内存进行碎片整理?是否足以确保[尽可能]连续分配对象?
  2. 这个问题有不同的解决方案吗?

3 个答案:

答案 0 :(得分:11)

新对象正在Eden空间中创建。伊甸园空间永远不会分散。 GC后它总是空的。

你遇到的问题是当执行GC时,对象可以随机排列在内存中,甚至可以按照相反的顺序排列。

解决方法是将字段存储为一系列数组。我将其称为基于列的表而不是基于行的表。

e.g。而不是写

class PointCount {
    double x, y;
    int count;
}

PointCount[] pc = new lots of small objects.

使用基于列的数据类型。

class PointCounts {
    double[] xs, ys;
    int[] counts;
}

class PointCounts {
    TDoubleArrayList xs, ys;
    TIntArrayList counts;
}

阵列本身最多可以在三个不同的位置,但数据总是连续的。如果您对字段子集执行操作,这甚至可以稍微提高效率。

public int totalCount() {
   int sum = 0;
   // counts are continuous without anything between the values.
   for(int i: counts) sum += i;
   return i;
}

我使用的解决方案是避免GC开销,因为大量数据是使用接口来访问直接或内存映射的ByteBuffer

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class MyCounters {
    public static void main(String... args) {
        Runtime rt = Runtime.getRuntime();
        long used1 = rt.totalMemory() - rt.freeMemory();
        long start = System.nanoTime();
        int length = 100 * 1000 * 1000;
        PointCount pc = new PointCountImpl(length);
        for (int i = 0; i < length; i++) {
            pc.index(i);
            pc.setX(i);
            pc.setY(-i);
            pc.setCount(1);
        }
        for (int i = 0; i < length; i++) {
            pc.index(i);
            if (pc.getX() != i) throw new AssertionError();
            if (pc.getY() != -i) throw new AssertionError();
            if (pc.getCount() != 1) throw new AssertionError();
        }
        long time = System.nanoTime() - start;
        long used2 = rt.totalMemory() - rt.freeMemory();
        System.out.printf("Creating an array of %,d used %,d bytes of heap and tool %.1f seconds to set and get%n",
                length, (used2 - used1), time / 1e9);
    }
}

interface PointCount {
    // set the index of the element referred to.
    public void index(int index);

    public double getX();

    public void setX(double x);

    public double getY();

    public void setY(double y);

    public int getCount();

    public void setCount(int count);

    public void incrementCount();
}

class PointCountImpl implements PointCount {
    static final int X_OFFSET = 0;
    static final int Y_OFFSET = X_OFFSET + 8;
    static final int COUNT_OFFSET = Y_OFFSET + 8;
    static final int LENGTH = COUNT_OFFSET + 4;

    final ByteBuffer buffer;
    int start = 0;

    PointCountImpl(int count) {
        this(ByteBuffer.allocateDirect(count * LENGTH).order(ByteOrder.nativeOrder()));
    }

    PointCountImpl(ByteBuffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void index(int index) {
        start = index * LENGTH;
    }

    @Override
    public double getX() {
        return buffer.getDouble(start + X_OFFSET);
    }

    @Override
    public void setX(double x) {
        buffer.putDouble(start + X_OFFSET, x);
    }

    @Override
    public double getY() {
        return buffer.getDouble(start + Y_OFFSET);
    }

    @Override
    public void setY(double y) {
        buffer.putDouble(start + Y_OFFSET, y);
    }

    @Override
    public int getCount() {
        return buffer.getInt(start + COUNT_OFFSET);
    }

    @Override
    public void setCount(int count) {
        buffer.putInt(start + COUNT_OFFSET, count);
    }

    @Override
    public void incrementCount() {
        setCount(getCount() + 1);
    }
}

使用-XX:-UseTLAB选项运行(以获得准确的内存分配大小)打印

  

创建一个100,000,000的数组,使用了12,512个字节的堆,并花了1.8秒来设置和获取

作为它的off堆,它几乎没有GC影响。

答案 1 :(得分:0)

遗憾的是,无法确保在Java中创建/停留在相邻内存位置的对象。

但是,按顺序创建的对象很可能最终彼此相邻(当然这取决于实际的VM实现)。我很确定虚拟机的编写者都知道地方性非常受欢迎,并且不会随意散布对象。

垃圾收集器在某些时候可能会移动对象 - 如果你的对象是短暂的,那应该不是问题。对于长寿命对象,它取决于GC如何实现移动幸存者对象。再一次,我认为编写GC的人已经在这个问题上花了一些心思,并且会以一种不会使局部性变得不可避免的方式执行副本,这是合理的。

显然没有任何上述假设的保证,但由于我们无论如何都无法做任何事情,所以不要担心:)

你可以在java源代码级别做的唯一事情是有时避免对象的组合 - 而是你可以“内联”你通常放在复合对象中的状态:

class MyThing {
    int myVar;
    // ... more members

    // composite object
    Rectangle bounds;
}

代替:

class MyThing {
    int myVar;
    // ... more members

    // "inlined" rectangle
    int x, y, width, height;
}

当然,这会使代码的可读性降低,并且可能会复制很多代码。

通过访问模式对类成员进行排序似乎会产生轻微的影响(我注意到在重新排序某些声明后基准代码片段略有改动),但我从未打扰过验证如果它是真的。但如果虚拟机不对成员进行重新排序,那将是有意义的。

在同一主题上,(从性能视图)能够将现有基本数组重新解释为另一种类型(例如,将int []转换为float [])也是很好的。虽然你在这里,为什么不为工会成员呢?我确定。 但是我们必须放弃许多平台和架构独立性来换取这些可能性。

答案 2 :(得分:-3)

在Java中不起作用。迭代不是增加指针的问题。根据对象物理存储在堆上的位置没有性能影响。

如果您仍希望以C / C ++方式处理此问题,请将Java数组视为指向结构的指针数组。循环遍历数组时,分配实际结构的位置并不重要,而是循环遍历指针数组。

我会放弃这种推理。这不是Java的工作方式,也是次优化。