在之前项目中,我的同事遇到了频繁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个字节的内存空间,并给出了一个计算公式公式:
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中取值