如果有这么一种场景,需要将客户端机器上所有文件的MD5值,都放到服务端的数据库中,服务端会定期的对客户端的文件进行检查,看是否有非法文件(注:这里用MD5做非法文件的检查,并不是说每个文件的MD5是唯一的,请查看这篇BLOG:不同文件也可以有相同的MD5校验值)。但是为了增加检查的速度,于是就准备把服务端数据库中所有的MD5都加载到CACHE中,可是这里有一个问题,服务端的CACHE机器的内存不够大,不能够完全存放下所有的MD5串,大约可以存放75%左右的数据,并且限于其它的原因也不可能加大CACHE机的内存,也不可能增加机器,只有这么一台机器。
鉴于这种情况,通常的处理方式是将部份数据加载到CACHE中,然后对客户端发过来不存在于CACHE中的MD5串再通过查数据比较,这种方式也是一解方案,至少75%的数据可以通过CACHE获取到,其它的25%再从数据库中查询了。但是这里还有一个问题就是服务端保存的MD5文件有很多,如有数千万条,即使只有25%的数据到数据库中查询,但是每次都是从数千万条数据中去查询一条,虽然存在索引,但是也是要费不少时间的。这个时候,可能有些童鞋会想到分库分表,多加CACHE机什么的,但是前面已经说了只有这么一台CACHE机,就不能够解决这个问题了吗?
解决方案肯定是有的,首先我们需要对MD5值的构成有所了解,那就是MD5结果是由0-F这些字符{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }组成的,0-F也是16进制的组成元素。 我们知道16进制的每个字符需要用4位二进制位来表示,Java中byte用二进制表示占用8位,也就是说一个byte可以存放两位16进制数,进一步说32位的MD5值,就可以存到16个字节中,这样一下就节约了一半的存储空间(实际节约不到一半,不能够简单比较,但是可以这样通俗理解,后面会有比较说明),因为如果是直接将32位的MD5存放字符串,就是占32个字节。以下是将16进制转为字节数组,以及将字节数据转为16进制字符串的JAVA代码实现
* Convert byte[] to hex string.这里我们可以将byte转换成int,然后利用Integer.toHexString(int)来转换成16进制字符串。
* @param src byte[] data
* @return hex string
*/
public static String bytesToHexString(byte[] src){
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
/**
* Convert hex string to byte[]
* @param hexString the hex string
* @return byte[]
*/
public static byte[] hexStringToBytes(String hexString) {
if (hexString == null || hexString.equals("")) {
return null;
}
hexString = hexString.toUpperCase();
int length = hexString.length() / 2;
char[] hexChars = hexString.toCharArray();
byte[] d = new byte[length];
for (int i = 0; i < length; i++) {
int pos = i * 2;
d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
}
return d;
}
/**
* Convert char to byte
* @param c char
* @return byte
*/
private byte charToByte(char c) {
return (byte) "0123456789ABCDEF".indexOf(c);
}
好了,现在容量的问题是解决了,可以将数据库中全部的数据都读入到内存当中了,鼓掌!为了保证对客户查询的快速响应,我们会将从数据库中读出来的MD5字符串放到HashSet或者是HashMap中,以提供快速查询。可是我们发现下面的代码却达不到我们想要的效果:
Set<byte[]> hasSet = new HashSet<byte[]>();
byte[] k1 = new byte[] { 1, 2, 3 };
byte[] k2 = new byte[] { 1, 2, 3 };
String val = "value";
hasSet.add(k1);
Assert.assertTrue(hasSet.contains(k2));
在做assertTrue()的时候,抛出了异常,这就表示HashSet中没有k2这么一个key,这是因为数组的比较不是按值进行比较,而按引用进入比较的,而这里k2和k1是两个不相同的对象,这也就是HashSet中找不到k2的原因,因为它里面只存了k1。有人在stackOverFlow上面也提到了这样的问题,
我们要使用字节数组做为Key,这个时候我们就需要对其进行包装才可以使用,这里有两种包装方案,第一种是使用Java为我们提供了java.nio.ByteBuffer这个类来包装,第二种方案是使用自己的封装类来包装,下面分别介绍两种实现方式,后面会有两种方式实现比较,究竟哪种更省内存。
1、使用java.nio.ByteBuffer这个类来包装
以下是一个封装了add,remove及contains方法的HashSet实现类:
public class ByteKeyHashSet extends HashSet<ByteBuffer> {
private static final long serialVersionUID = -2702041216392736060L;
public boolean add(byte[] key) {
return super.add(ByteBuffer.wrap(key));
}
public boolean add(String key) {
return super.add(ByteBuffer.wrap(key.getBytes()));
}
public boolean remove(byte[] key) {
return super.remove(ByteBuffer.wrap(key));
}
public boolean remove(String key) {
return super.remove(ByteBuffer.wrap(key.getBytes()));
}
public boolean contains(byte[] key) {
return super.contains(ByteBuffer.wrap(key));
}
public boolean contains(String key) {
return super.contains(ByteBuffer.wrap(key.getBytes()));
}
}
我们用下面这个单元测试跑一下:
public class ByteKeyHashMapTest extends TestCase{
@Test
public void test() {
ByteKeyHashSet byteKeyHashSet = new ByteKeyHashSet();
byte[] k1 = new byte[] { 1, 2, 3 };
byte[] k2 = new byte[] { 1, 2, 3 };
byteKeyHashSet.add(k1);
Assert.assertTrue(byteKeyHashSet.contains(k2));
byteKeyHashSet.remove(k1);
Assert.assertFalse(byteKeyHashSet.contains(k2));
}
}
OK,没有问题,可以用字节数组做hash key存放了。
2、使用自己的包装类
下面是一个包装类ByteArrayWrapper,对equals及hashcode方法进行重写,以满足对数组进行值比较:
public final class ByteArrayWrapper {
private final byte[] data;
public ByteArrayWrapper(byte[] data) {
if (data == null) {
throw new NullPointerException();
}
this.data = data;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof ByteArrayWrapper)) {
return false;
}
return Arrays.equals(data, ((ByteArrayWrapper) other).data);
}
@Override
public int hashCode() {
return Arrays.hashCode(data);
}
}
下面是一个根据包装类ByteArrayWrapper,实现的ByteKeyHashSet2:
public class ByteKeyHashSet2 extends HashSet<ByteArrayWrapper> {
private static final long serialVersionUID = 4103739084726741502L;
public boolean add(byte[] key) {
return super.add(new ByteArrayWrapper(key));
}
public boolean add(String key) {
return super.add(new ByteArrayWrapper(key.getBytes()));
}
public boolean remove(byte[] key) {
return super.remove(new ByteArrayWrapper(key));
}
public boolean remove(String key) {
return super.remove(new ByteArrayWrapper(key.getBytes()));
}
public boolean contains(byte[] key) {
return super.contains(new ByteArrayWrapper(key));
}
public boolean contains(String key) {
return super.contains(new ByteArrayWrapper(key.getBytes()));
}
}
下面是单元测试的代码:
public class ByteKeyHashSetTest2 extends TestCase {
@Test
public void test() {
ByteKeyHashSet2 byteKeyHashSet = new ByteKeyHashSet2();
byte[] k1 = new byte[] { 1, 2, 3 };
byte[] k2 = new byte[] { 1, 2, 3 };
byteKeyHashSet.add(k1);
Assert.assertTrue(byteKeyHashSet.contains(k2));
byteKeyHashSet.remove(k1);
Assert.assertFalse(byteKeyHashSet.contains(k2));
}
}
执行单元测试,OK,也是成功的。
如何选择?
既然我们的目标是减少内存的占用,那么标准就是哪种占用的内存更少,就采取哪种实现了。
如何判定哪种实现占用的内存更少了,这个时候我们就需要知道如何计算对象的Deep Sizes(有的地方称为Retained Sizes,)了,指的是当前对象包括其引用的所有对象所占用的内存大小。下面是一个公式:
当前对象的Deep Sizes = 当前对象的引用 + 当前对象中最后一个字段的偏移量 + 当前对象中最后一个字段类型占用的字节数 + 所有数组的大小 + 当前对象中所有子对象的deep size
一个对象当中的字段数包括其本身的非静态字段以及其所有超类中的非静态字段,如果上面的公式看不明白,没有关系,待会儿后面再看这篇文章就会明白了:使用sun.misc.Unsafe及反射对内存进行内省(introspection)。
下面我们来看看不同的对象所占用的Deep Sizes情况(我当前的环境,每个对象引用占4个字节,不同的环境结果会不一样,不过差别不会太大):
String对象本身的Deep Sizes是48,每多一个字符Deep Sizes增加2个字节,如32位的MD5字符串,Deep Sizes是112;
ByteBuffer对象本身的Deep Sizes是73,每增加一个字符Deep Sizes增加1个字节,如32位的MD5字符串,通过其包装后,占用105个字节;
ByteArrayWrapper对象本身的Deep Sizes是36,每增加一个字符Deep Sizes增加1个字节,如32位的MD5字符串,通过其包装后,占用68个字节。
通过上面的结果我们可以得出,一个对象中的字段数越少,那么当前对象的Deep Sizes就会相对越小,也就是本身内存占用越小,反之亦然,我相信我们都应该知道选择哪种方式了吧!