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

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

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

我知道如果我“一次”分配对象[一个接一个],jvm 最有可能将对象分配为尽可能连续,但如果内存碎片化则可能还不够。

我的问题:

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

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

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

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

而不是写作

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; } 

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

 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运行-XX:-UseTLAB选项(以获得准确的内存分配大小)打印

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

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

遗憾的是,无法确保在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; } 

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

通过访问模式对类成员进行排序似乎有轻微的影响(我注意到在重新排序一些声明之后对基准代码片段进行了轻微更改),但我从未打扰过validation它是否真实。 但是,如果VM不对成员进行重新排序,那将是有意义的。

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

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

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

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