用数百万个对象填充HashMap
会很快导致诸如内存使用效率低下,性能低下和垃圾回收等问题。 了解如何使用堆外CronicleMap
,其中可以包含数十亿个对象,而对堆的影响很小或没有。
当我们要使用中小型数据集时,内置的Map
实现(例如HashMap
和ConcurrentHashMap
是出色的工具。 但是,随着数据量的增长,这些
Map
的实现正在恶化,并开始表现出如约开放sourceed的系列文章的第一篇文章中一些令人不快的缺点CronicleMap
。
堆分配
在下面的示例中,我们将使用Point
对象。 Point
是一个POJO,具有公共默认构造函数以及X和Y属性(int)的getter和setter。 以下代码段将一百万个Point
对象添加到HashMap
:
final Map<Long, Point> m = LongStream.range( 0 , 1_000_000)
.boxed()
.collect(
toMap(
Function.identity(),
FillMaps::pointFrom,
(u,v) -> { throw new IllegalStateException(); }, IllegalStateException(); },
HashMap:: new
)
);
// Conveniency method that creates a Point from
// a long by applying modulo prime number operations
private static Point pointFrom( long seed) {
final Point point = new Point();
point.setX(( int ) seed % 4517 );
point.setY(( int ) seed % 5011 );
return point;
}
我们可以很容易地看到堆上分配的对象数量以及这些对象消耗多少堆内存:
Pers-MacBook-Pro:chronicle-test pemi$ jmap -histo 34366 | head | head
num #instances #bytes class name (module) -------------------------------------------------------
1 : 1002429 32077728 java.util.HashMap$Node (java.base @10 )
2 : 1000128 24003072 java.lang.Long (java.base @10 )
3 : 1000000 24000000 com.speedment.chronicle.test.map.Point
4 : 454 8434256 [Ljava.util.HashMap$Node; (java.base [Ljava.util.HashMap$Node; (java.base @10 )
5 : 3427 870104 [B (java.base @10 )
6 : 185 746312 [I (java.base @10 )
7 : 839 102696 java.lang.Class (java.base @10 )
8 : 1164 89088 [Ljava.lang.Object; (java.base [Ljava.lang.Object; (java.base @10 )
对于每个Map
条目,都需要在堆上创建Long
, HashMap$Node
和Point
对象。 还有许多创建了HashMap$Node
对象的数组。 这些对象和数组总共消耗了88,515,056字节的堆内存。 因此,每个条目平均消耗88.5个字节。
注意:额外的2429 HashMap$Node
对象来自Java内部使用的其他HashMap
对象。
堆外分配
与此相反, CronicleMap
使用很少的堆内存,运行以下代码时可以看到:
final Map<Long, Point> m2 = LongStream.range( 0 , 1_000_000)
.boxed()
.collect(
toMap(
Function.identity(),
FillMaps::pointFrom,
(u,v) -> { throw new IllegalStateException(); }, IllegalStateException(); },
() -> ChronicleMap
.of(Long. class , Point. class )
.averageValueSize( 8 )
.valueMarshaller(PointSerializer.getInstance())
.entries(1_000_000)
.create()
)
);
Pers-MacBook-Pro:chronicle-test pemi$ jmap -histo 34413 | head | head
num #instances #bytes class name (module) -------------------------------------------------------
1 : 6537 1017768 [B (java.base @10 )
2 : 448 563936 [I (java.base @10 )
3 : 1899 227480 java.lang.Class (java.base @10 )
4 : 6294 151056 java.lang.String (java.base @10 )
5 : 2456 145992 [Ljava.lang.Object; (java.base [Ljava.lang.Object; (java.base @10 )
6 : 3351 107232 java.util.concurrent.ConcurrentHashMap$Node (java.base @10 )
7 : 2537 81184 java.util.HashMap$Node (java.base @10 )
8 : 512 49360 [Ljava.util.HashMap$Node; (java.base [Ljava.util.HashMap$Node; (java.base @10 )
可以看出,没有为Java分配对象
CronicleMap
条目,因此也没有堆内存。
CronicleMap
不会分配堆内存,而是分配堆外内存,而不是分配堆内存。 假设我们使用标志-XX:NativeMemoryTracking=summary
启动JVM,则可以通过发出以下命令来检索正在使用的堆外内存量:
Pers-MacBook-Pro:chronicle-test pemi$ jcmd 34413 VM.native_memory | grep Internal VM.native_memory | grep Internal - Internal (reserved=30229KB, committed=30229KB)
显然,我们的一百万个对象使用略多于30 MB的堆外RAM放置在堆外内存中。 这意味着,
CronicleMap
使用的CronicleMap
平均需要30个字节。
这比需要88.5字节的HashMap
内存有效得多。 实际上,我们节省了66%的RAM内存和近100%的堆内存。 后者很重要,因为Java垃圾收集器只能看到堆上的对象。
注意,我们必须在创建时决定CronicleMap
最多可以容纳多少个条目。 相比于
HashMap
可以随着我们添加新的关联而动态增长。 我们还必须提供一个序列化程序 (即PointSerializer.getInstance()
),本文稍后将对此进行详细讨论。
垃圾收集
许多垃圾回收(GC)算法在与堆中存在的对象的平方成比例的时间内完成。 因此,例如,如果我们将堆上的对象数量加倍,则可以预期GC将花费四倍的时间才能完成。
另一方面,如果我们创建的对象增加了64倍,则预期的预期GC时间将增加1,024倍。 这有效地阻止了我们创造出巨大的
HashMap
对象。
使用ChronicleMap
我们可以放置新的关联,而无需担心垃圾收集时间。
序列化器
堆内存与堆外内存之间的介体通常称为
序列化器 。 ChronicleMap
带有许多针对大多数内置Java类型(例如Integer
, Long
, String
等)的预配置序列化器。
在上面的示例中,我们使用了一个自定义的序列化程序,该序列化程序用于在堆内存与堆外内存之间来回转换Point
。 序列化器类如下所示:
public final class PointSerializer implements
SizedReader<Point>,
SizedWriter<Point> {
private static PointSerializer INSTANCE = new PointSerializer();
public static PointSerializer getInstance() { return INSTANCE; } INSTANCE; }
private PointSerializer() {}
@Override
public long size( @NotNull Point toWrite) {
return Integer.BYTES * 2 ;
}
@Override
public void write(Bytes out, long size, @NotNull Point point) {
out.writeInt(point.getX());
out.writeInt(point.getY());
}
@NotNull
@Override
public Point read(Bytes in, long size, Point using) { @Nullable Point using) {
if (using == null ) {
using = new Point();
}
using.setX(in.readInt());
using.setY(in.readInt());
return using;
} }
上面的序列化器实现为无状态单例,并且write()
和read()
方法中的实际序列化非常简单。 唯一棘手的部分是,我们需要在
如果“ using”变量未引用实例化/重用的对象,则为read()
方法。
如何安装?
当我们想在项目中使用ChronicleMap
时,只需在pom.xml文件中添加以下Maven依赖项,就可以访问该库。
< dependency >
< groupId >net.openhft</ groupId >
< artifactId >chronicle-map</ artifactId >
< version >3.17.3</ version > </ dependency >
如果您使用的是另一种构建工具(例如Gradle),则可以通过单击此链接来了解如何依赖ChronicleMap
。
短篇小说
以下是ChronicleMap的一些属性:
堆外存储数据
几乎总是比HashMap
更高的内存效率
实现ConcurrentMap
不影响垃圾收集时间 有时需要一个序列化器 具有固定的最大条目大小 可以容纳数十亿个协会 是免费和开源的
翻译自: https://www.javacodegeeks.com/2019/08/java-chroniclemap-part-1-go-off-heap.html