在之前项目中,我的同事遇到了频繁full gc的情况,帮他排查发现,他在内存缓存了一个Map,里面存了大量的Integer,大约7000W,这部分占了大量的内存。

后来通过改变数据结构,从原来的Hashmap改为Bitmap,解决了这部分内存问题,发布后,内存占用明显减少。

Bitmap介绍

Bitmap(jdk中实现叫BitSet)就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。

假设我们有一个需要存储大量数字的场景(比如10亿个int),下面是不同容器占用的内存大小:

数组:1000000000*4/1024/1024 = 3814M(一个int占4字节)
集合list:1000000000*16/1024/1024 = 15258M(一个Integer对象占16字节)
集合map、set:1000000000*32/1024/1024 = 30517M(粗略估计32字节:一个node节点+一个Integer对象占用32字节)
Bitmap :1000000000/8/1024/1024=119M

 

Bitmap的核心思想:一个byte占8个bit,如果用一个bit表示一个int数字的值,即0表示这个数不存在,1表示整个数存在,那么一个byte就能表示8个int数字,一个int空间就能表示32个int数字, 原本一个int数占32bit,现在只占1bit。如下图所示:

 

关键方法算法:

public class BitMap {

 private byte[] bits;

 public BitMap(int capacity) {
 bits = new byte[capacity / 8 + 1];
 }

 public BitMap(byte[] bits) {
 this.bits = bits;
 }

 /**
 * 除8得到在byte[]中index
 *
 * @param num
 * @return
 */
 private int getIndex(int num) {
 return num >> 3;
 }

 /**
 * 求余数得到在byte中的第几个比特位
 *
 * @param num
 * @return
 */
 private int getPosition(int num) {
 return num & 0x07;
 }

 /**
 * 标记指定数字(num)在bitmap中的值,标记其已经出现过
 *
 * @param num
 */
 public void set(int num) {
 this.bits[getIndex(num)] |= 1 << getPosition(num);
 }

 /**
 * 判断指定数字num是否存在
 *
 * @param num
 * @return
 */
 public boolean contains(int num) {
 return (this.bits[getIndex(num)] & 1 << getPosition(num)) != 0;
 }

 /**
 * 重置某一数字对应在bitmap中的值
 *
 * @param num
 */
 public void reset(int num) {
 this.bits[getIndex(num)] &= ~(1 << getPosition(num));
 }

}

Bitmap的常用应用场景

1、标签场景。比如用户标签,性别、是否90后这样的标签,可以每一个标签属性作为一个bitmap,里面存储用户的id。

2、统计场景。判断用户7天内是否登录过,每天有一个记录用户登录情况的bitmap,登录过的用户,在Bitmap对应用户id位置上置1,把N天的bitmap做按位与或者按位或,就可以得到N天都登陆的用户和N天没有登录的用户。

3、Bitmap配合redis使用,redis中存储的数字集合,一样可以用bitmap替换,以减少redis内存使用。同时,redis也提供了api来操作位(GETBIT、SETBIT)。

4、Bloomfilter:底层数据结构就是依靠BitMap。

 

注意事项

要注意的是,稀疏场景下,Bitmap可能会占用更多的内存。

 

还可以占用更少的空间吗?

Compressed Bitmap:

由于BitMap存在浪费空间的场景,http://roaringbitmap.org给出了一种压缩位图的实现——RoaringBitmap。 Github链接:https://github.com/RoaringBitmap/RoaringBitmap

在内存空间方面,RoaringBitmap官方表示,对于每个int,RoaringBitmap不会使用超过2个字节的内存空间,并给出了一个计算公式公式:

ImmutableMap 只能构造10个_BitMap

 

RoaringBitmap压缩算法相关论文:https://arxiv.org/pdf/1603.06549.pdf

可以根据你的业务实际需求,跟jdk原生bitmap做一次对比压测之后,再选择是否需要使用RoaringBitmap。

 

总结

1、少量的int——常规数组、集合皆可

2、大量的int,并且对有存取效率有严格要求(关注性能的场景)——Bitmap

3、大量的int,并且对内存大小有严格要求(关注内存空间场景)——RoaringBitmap

 

补充

同事提到了负数场景,解决方法:

假如要存-2147483648这个值,

long i=Integer.toUnsignedLong(-2147483648)//i=2147483648

只要把2147483648存到bitmap中就好了,取值的时候也一样,负数先做一遍Integer.toUnsignedLong,再根据转换后的结果再去bitmap中取值