大数值使用BitSet存储导致的内存溢出

背景:

在日常的工作中,使用Redis的bitmap统计每天的登录用户数,使用java的BitSet进行统计总数或者与或非等操作时,我们可以看到BitSet/Redis的Bitmap操作的身影,他们也的确能减少内存的使用量以及操作的性能,但是我们不能神话这两个结构的作用,认为什么数据都适合转成用BitSet/Redis的Bitmap然后操作.

BitMap/BitSet不适用的场景:

举一个最简单的例子,保存一个Integer.max/2的正整数值,我们正常使用Int四个字节就够了,把这个整数放到Set或者List集合进行操作即可,但是有些人非要使用BitSet来存放,那么你知道仅仅存储这一个整数,BitSet需要多少字节吗?

    public static void bitSetTest() {BitSet bitSet = new BitSet();bitSet.set(Integer.MAX_VALUE / 2, true);System.out.println(bitSet.toByteArray().length);}

程序运行结果如下: 134217728, 看到没,BitSet需要将近134217728/1024/1024=130M的字节来存放这一个整数,惊讶不?同理这个结论可以推理到使用Redis的BitMap结构,那么为什么这些结构需要这么多的字节来存放仅仅一个整数?我们先大概看一下BitSet/Bitmap的存储数据结构:

可以看到所谓的bit操作只是说第一个字节可以存放8这个绝对值,第一个字节+第二个字节可以存放16这个绝对值,也就是说绝对值越大,所需要的字节数组越大,其实只有最后一个bit存放着1这个位,其他前面的字节数组都是存放0,完全被浪费掉了,不过还是需要分配这些字节数组,这就是存放Integer.MAX_VALUE /2 这样一个整数需要134M字节数组的原因。
其实从这里你可以发现类似BitSet和Redis的BitMap的结构,它占用的字节数只和要存放的数字的最大值有关,也就是说和这个BitSet存放进去多少数量没有关系,比如下面的代码:

    public static void bitSet1Test(){BitSet bitSet = new BitSet();bitSet.set(Integer.MAX_VALUE /2, true);for(int i=0;i<1000000;i++){bitSet.set(RandomUtils.nextInt(0, Integer.MAX_VALUE /2));}System.out.println(bitSet.toByteArray().length);}

程序运行结果如下: 134217728, 可以看到BitSet里面存放了10w个元素,不过元素的最大值是Integer.max/2,最终它占用的内存大小是134217728/1024/1024=130M,有没有发现存放1个Integer.max/2和存放10w个最大值不超过Integer.max/2的两个BitSet的内存占用是一样的?没错,就是一样的

所以我们可以得出结论: 想要高效使用BitSet和Redis的Bitmap数据结构,里面存放的数值要尽可能小,这样才能占用尽可能小的内存。

那么有人就问了,如果我的数值就是这么大呢?有办法使用BitSet和Redis的Bitmap数据结构吗?
答案是:你需要经过一层转换,也就是你需要把你这个大的整数值映射到某个小的整数值再放到BitSet/Redis的Bitmap中,等到从BitSet/Redis的Bitmap把这个小的数值取出时,再映射成原来的大的数值,这样就可以高效的使用BitSet/Redis的Bitmap结构进行操作了

彩蛋:Roaring64Bitmap/Roaring32Bitmap 和这里的BitSet/Redis的Bitmap的结论一样吗?

Roaring64Bitmap/Roaring32Bitmap和这里的BitSet/Redis的Bitmap的存储结构不一样,他们占用的内存虽然也是受到数值绝对值大小的影响,但是并没有像BitSet/Redis的Bitmap这样极端,所以使用Roaring64Bitmap和Roaring32Bitmap结构时最好首先进行测试,不过可以肯定的是,这些结构在保存大的数值时内存占用很大,和直接使用Long数组存放相差并不大,所以原则是一致的,首先把大的数值转成小的数值后再放到这些Bit数据结构中进行存储和运算.

本文链接:https://my.lmcjl.com/post/14292.html

展开阅读全文

4 评论

留下您的评论.