0%

Presto源码解析 - Slice实现

2020-11-21日更:

x信金科,如果大数据部招人,如果你是业务出生/学历不好/公司背景不强,就不要浪费他们的时间了

前言

Presto的Slice并不在Presto包中,是在一个独立的包org.airlift.slice
org.airlift是个工具类,作者也是Presto的主要开发者,主要是服务于Presto的,但是我们也可以单独取出来用。

ClassLayout和Unsafe

在Java中一般是无法取得类的大小的,需要通过一些特殊的手段,例如Unsafe包中的方法。
org.openjdk.jol包封装了很多Unsafe的方法。
我们可以通过ClassLayout类来或者我们创建的Java对象在内存中的大小。

通过Unsafe我们可以对对象的内存直接进行操作。
假设我们创建一个int类型的数组,每个元素的值是他Index的位置

1
2
3
4
int[] nums = new int[20];
for (int i = 0; i < nums.length; i++) {
nums[i] = i;
}

上面这种写法是常规的写法。
我们也可以用Unsafe。

1
2
3
4
5
6
7
8
9
10
11
/**
* 调用反射获得Unsafe实例
*/
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

int[] nums = new int[20];
for (int i = 0; i < nums.length; i++) {
unsafe.putInt(nums, (long) i * Unsafe.ARRAY_INT_INDEX_SCALE + Unsafe.ARRAY_INT_BASE_OFFSET, i);
}

Unsafe实例我们需要通过反射获得,直接获得会抛出异常。
ARRAY_INT_BASE_OFFSET表示数组对象的第一个元素在内存中的位置。
ARRAY_INT_INDEX_SCALE表示每一个真正的元素的数据中的占据空间。
这些变量都在Unsafe中,还有许多,对应byte数组,long数组等。

同样的,只要能获得对象的地址,那么我们就可以对任意的对象进行写入。
我们尝试在Object对象中写入两个Long元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 调用反射获得Unsafe实例
*/
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

Object object = new Object();
System.out.println(object);
int size = ClassLayout.parseClass(Object.class).instanceSize();
System.out.println("size: " + size); // 16
/**
* 把Object写为两个long
*/
unsafe.putLong(object, 0L, 13L);
unsafe.putLong(object, (long) SizeOf.SIZE_OF_LONG, 19L);

System.out.println(unsafe.getLong(object, 0L)); // 13
System.out.println(unsafe.getLong(object, (long)SizeOf.SIZE_OF_LONG)); // 19

System.out.println(object); // upe

需要注意的是,这样Object对象就是不可用了。
如果我们强行输出的话,会抛出空指针异常。

同样的,我们可以对对象进行设值,这个时候我们可以借助ClassLayout封装好的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class Person {
private String name;
private int age;

//get set...
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Person{");
sb.append("name='").append(name).append('\'');
sb.append(", age=").append(age);
sb.append('}');
return sb.toString();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Person person = new Person();
ClassLayout personClassLayout = ClassLayout.parseClass(Person.class);
int size = personClassLayout.instanceSize();
System.out.println("size: " + size);
System.out.println("header size: " + personClassLayout.headerSize());

/**
* 得到所有的field的信息
*/
SortedSet<FieldLayout> fields = personClassLayout.fields();
for (FieldLayout layout : fields) {
switch (layout.name()) {
case "name":
unsafe.putObject(person, ((long) layout.offset()), "Zhu");
break;
case "age":
unsafe.putInt(person, ((long) layout.offset()), 20);
break;
default:
break;
}
}
System.out.println(person); // Person{name='Zhu', age=20}

总之,这就给了一个类似于利用大对象的内存配合Unsafe直接操作内存的方法做一个内存池的思路

Slices

我们无法直接创建Slice类,可以通过Slices类提供的很多的静态方法来进行创建。

1
2
3
4
Slice allocate(int capacity); 
//利用这个方法我们可以直接创建一个容量为capacity的Slice,底层就是创建了一个byte[capacity]的数组,不过这个对象是在堆内的
Slice allocateDirect(int capacity);
//利用这个方法我们可以在堆外创建一块内存,底层是使用的nio的ByteBuffer.allocateDirect

创建了Slice之后,就可以往里面添加元素了。
在Presto中最重要的两个用法就是FixedWidthBlock和VariableWidthBlock了。
创建者两种Block运用他的Builder类FixedWidthBlockBuilder和VariableWidthBlockBuilder类。

FixedWidthBlockBuilder

定长的Block,所以会固定一个FixedSize,然后底层就是一个byte数组。
不管我们往里面写什么,只要一个entry的长度是FixedSize就行。
同时这个不提供自动扩展内存的功能,当超出大小时,会抛出异常。

VariableWidthBlockBuilder


变长的Block,没有固定的大小,所以需要一个额外的数组记录指定entry的位置
同时在每次增加之前会确保内存空间足够,如果不够会进行自动扩容。