加快Java的文件序列化速度
自从第一个Java版本开始,很多开发人员一直都在尝试让Java获得最少和C/C++一样的表现。JVM提供商尽他们最大的努力去实现一些新的JIT算法,但是还是有很多需要做的,特别是在我们使用Java的方法上。
例如,在对象<->文件序列化上就差距很大--尤其在读写内存对象上。我将就这个主题做一些解释和分享。
所有的测试都是在下面这个对象上执行的:
1
public
class
TestObject
implements
Serializable {
2
?3
??
private
long
longVariable;
4
??
private
long
[] longArray;
5
??
private
String stringObject;
6
??
private
String secondStringObject;
//just for testing nulls
7
?8
??
/* getters and setters */
9
}
?
为了简单起见,我将只贴出写入方法(尽管读取类似),完整的源码在我的GitHub上可以找到(http://github.com/jkubrynski/serialization-tests)
最标准的java序列化(我们都是从这里学起的)是这样的:
01
public
void
testWriteBuffered(TestObject test, String fileName)
throws
IOException {
02
??
ObjectOutputStream objectOutputStream =
null
;
03
??
try
{
04
????
FileOutputStream fos =
new
FileOutputStream(fileName);
05
????
BufferedOutputStream bos =
new
BufferedOutputStream(fos);
06
????
objectOutputStream =
new
ObjectOutputStream(bos);
07
????
objectOutputStream.writeObject(test);
08
??
}
finally
{
09
????
if
(objectOutputStream !=
null
) {
10
??????
objectOutputStream.close();
11
????
}
12
??
}
13
}
?
提升标准序列化速度的最简单方法时使用RandomAccessFile对象:
01
public
void
testWriteBuffered(TestObject test, String fileName)
throws
IOException {
02
??
ObjectOutputStream objectOutputStream =
null
;
03
??
try
{
04
????
RandomAccessFile raf =
new
RandomAccessFile(fileName,
"rw"
);
05
????
FileOutputStream fos =
new
FileOutputStream(raf.getFD());
06
????
objectOutputStream =
new
ObjectOutputStream(fos);
07
????
objectOutputStream.writeObject(test);
08
??
}
finally
{
09
????
if
(objectOutputStream !=
null
) {
10
??????
objectOutputStream.close();
11
????
}?????
12
}
?
更高深点的技术是使用Kryo框架,新旧版本的差距是很大的,我做过测试。因为性能比较上并没有体现出特别引人注意的差异,所以我将使用2.x版本,因为它对用户更友好而且更快些。
01
private
static
Kryo kryo =
new
Kryo();
// version 2.x
02
?03
public
void
testWriteBuffered(TestObject test, String fileName)
throws
IOException {
04
??
Output output =
null
;
05
??
try
{
06
????
RandomAccessFile raf =
new
RandomAccessFile(fileName,
"rw"
);
07
????
output =
new
Output(
new
FileOutputStream(raf.getFD()), MAX_BUFFER_SIZE);
08
????
kryo.writeObject(output, test);
09
??
}
finally
{
10
????
if
(output !=
null
) {
11
??????
output.close();
12
????
}
13
??
}
14
}
?
最后一个方案是在Martin Thompson的文章中提到的(Native C/C++ Like Performance For Java Object Serialisation),介绍了怎样在Java中像C++那样和内存打交道。
01
public
void
testWriteBuffered(TestObject test, String fileName)
throws
IOException {
02
??
RandomAccessFile raf =
null
;
03
??
try
{
04
????
MemoryBuffer memoryBuffer =
new
MemoryBuffer(MAX_BUFFER_SIZE);
05
????
raf =
new
RandomAccessFile(fileName,
"rw"
);
06
????
test.write(memoryBuffer);
07
????
raf.write(memoryBuffer.getBuffer());
08
??
}
catch
(IOException e) {
09
????
if
(raf !=
null
) {
10
??????
raf.close();
11
????
}
12
??
}
13
}
?
TestObject写入方法如下:
01
public
void
write(MemoryBuffer unsafeBuffer) {
02
??
unsafeBuffer.putLong(longVariable);
03
??
unsafeBuffer.putLongArray(longArray);
04
??
// we support nulls
05
??
boolean
objectExists = stringObject !=
null
;
06
??
unsafeBuffer.putBoolean(objectExists);
07
??
if
(objectExists) {
08
????
unsafeBuffer.putCharArray(stringObject.toCharArray());
09
??
}
10
??
objectExists = secondStringObject !=
null
;
11
??
unsafeBuffer.putBoolean(objectExists);
12
??
if
(objectExists) {
13
????
unsafeBuffer.putCharArray(secondStringObject.toCharArray());
14
??
}
15
}
?
直接内存缓冲区类(已简化了的,仅仅为了展示这个思想)
01
public
class
MemoryBuffer {
02
??
// getting Unsafe by reflection
03
??
public
static
final
Unsafe unsafe = UnsafeUtil.getUnsafe();
04
?05
??
private
final
byte
[] buffer;
06
?07
??
private
static
final
long
byteArrayOffset = unsafe.arrayBaseOffset(
byte
[].
class
);
08
??
private
static
final
long
longArrayOffset = unsafe.arrayBaseOffset(
long
[].
class
);
09
??
/* other offsets */
10
?11
??
private static final int SIZE_OF_LONG = 8;
12
??
/* other sizes */
13
?14
??
private long pos = 0;
15
?16
??
public MemoryBuffer(int bufferSize) {
17
????
this.buffer = new byte[bufferSize];
18
??
}
19
?20
??
public final byte[] getBuffer() {
21
????
return buffer;
22
??
}
23
?24
??
public final void putLong(long value) {
25
????
unsafe.putLong(buffer, byteArrayOffset + pos, value);
26
????
pos += SIZE_OF_LONG;
27
??
}
28
?29
??
public final long getLong() {
30
????
long result = unsafe.getLong(buffer, byteArrayOffset + pos);
31
????
pos += SIZE_OF_LONG;
32
????
return result;
33
??
}
34
?35
??
public final void putLongArray(final long[] values) {
36
????
putInt(values.length);
37
????
long bytesToCopy = values.length << 3;
38
????
unsafe.copyMemory(values, longArrayOffset, buffer, byteArrayOffset + pos, bytesToCopy);
39
????
pos += bytesToCopy;
40
??
}
41
?42
?43
??
public final long[] getLongArray() {
44
????
int arraySize = getInt();
45
????
long[] values = new long[arraySize];
46
????
long bytesToCopy = values.length << 3;
47
????
unsafe.copyMemory(buffer, byteArrayOffset + pos, values, longArrayOffset, bytesToCopy);
48
????
pos += bytesToCopy;
49
????
return values;
50
??
}
51
?52
??
/* other methods */
53
}
?
几个小时的Caliper测试结果如下:
?Full trip [ns]?Standard deviation [ns]?Standard??207307?2362Standard on RAF?42661?733KRYO 1.x??12027?112KRYO 2.x?11479?259Unsafe?8554?91在最后我们可以得出一些结论:
Unsafe序列化比标准的java.io.Serizlizable快了23倍使用RandomAccessFile可以使标准的有缓冲序列化加速将近4倍Kryo-dynamic序列化大约比手写实现的直接缓冲满了35%最后,就像我们看到的那样,还是没有绝对的答案。对于我们中的大多数人来说,获得3000ns(0.003ms)的速度提升是不值得为每个需要序列化的对象来写单独实现的。在标准的方案中,我们大多数选择Kryo?。然而,在惜时如金的低延时系统中,这个选择将会是完全不同的。