估算数据库数据在java中内存占用
首先我们需要了解java中Class的内存结构
第一个Class头的8个字节:这个字节存储了比如这个实例目前的锁信息、目前属于的堆类型,初始化进度等等。第二个区域,oop指针,这个字段存储的是这个类的定义,就比如Java反射可以拿到字段名称,方法名称这些值都是存储在这个指针所指向的定义中。第三个区域,数据区域,存放数据的区域,这里的结构区分主要是两种:数组和非数组。如果是数组,数据区域中还会包含这个数组的大小。
我们大概说明了Class内存结构模型,这个时候对计算大小有个基本的方案了。我们可以根据Class定义的结构进行计算此实例占用的大小了。
以上是一个通用数据测试结论,估大家参考。
数据库记录占用的空间大小比较好算,比如一个int占用4字节,bigint占用8字节,date占用3字节,datetime占用8字节,varchar是变长字节等。如果不想精确计算,在数据库中通过统计信息也可以比较轻松的知道表总共占用的空间及每条记录平均行长。
当我们用JDBC访问数据库时,经常会被问到内存溢出的问题,由于java是面向对象的语言,用JVM来自动内存回收,不能按普通方法计算内存,本文给出一个估算内存的思路和参考答案
先给出普通JDBC中数据库对象与内存的映射关系
MySQL | Oracle | JDBC |
Int | | Integer |
Int unsigned | | Long |
BigInt | | Long |
BigInt unsigned | | BigInteger |
Decimal | Number | BigDecimal |
Varchar | Varchar2 | String |
Date | | Date |
Datetime | Date | Timestamp |
Timestamp | Timestamp | Timestamp |
Clob | Clob | String |
Blob | blob | Byte[] |
Text | Clob | String |
float | binary_float | float |
double | binary_double | double |
上面这个比较好理解,接下来我们需要JAVA常用对象的内存占用空间,这个可以通过JDK 5 开始提供的Instrumentation 接口来完成,也可以通过开源的sizeOf.jar 来测试,笔者是通过sizeOf.jar验证的。测试结果数据如下:
对象 | 64位 JVM 压缩指针 | 64位 JVM 非压缩指针 |
Integer | 16 | 24 |
Long | 24 | 24 |
Object | 16 | 16 |
Date | 24 | 32 |
Timestamp | 32 | 40 |
String_0 | 48 | 64 |
String_1 | 56 | 72 |
String_10 | 72 | 88 |
String_100 | 248 | 264 |
StringBuilder | 24 | 32 |
BigDecimal | 40 | 48 |
BigInteger | 64 | 80 |
HashMap | 128 | 216 |
HashMap_0 | 72 | 96 |
HashMap_100 | 576 | 1112 |
HashMap_10000 | 65600 | 131160 |
ArrayList | 80 | 144 |
ArrayList_0 | 40 | 64 |
ArrayList_100 | 440 | 864 |
ArrayList_10000 | 40040 | 80064 |
LinkedList | 48 | 80 |
LinkedHashMap | 96 | 144 |
ClassA | 32 | 40 |
ClassB | 40 | 48 |
ClassC | 40 | 56 |
由于现在主机一般都是64位, 64位JVM从JDK1.6.45开始,当JVM最大内存小于32GB时,自动打开压缩指针特性,这样对象的内存占用空间少很多,由上表可以看出,至少减少1/3的空间。
下面我们结合数据库数据来测试
假如mysql数据库有一张emp表,结构如下:
CREATE TABLE `emp` (
`id` int(11) NOT NULL,
`setup_time` datetime DEFAULT NULL,
`setup_date` dateDEFAULT NULL,
`name` varchar(16) DEFAULT NULL,
‘unit_price’ decimal(18,2) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
样本数据如下:
hm.put("id", 32466);
hm.put("name", "鸿运汽车");
hm.put("setupDate", new Date());
hm.put("setupTime", new Timestamp(System.currentTimeMillis()));
hm.put("unitPrice", new BigDecimal(100.00)
);
按上面样本数据计算,有效数据约50字节
在java里转换为HashMap和Emp对象测试空间如下
对象 | 64位 JVM 压缩指针 | 64位 JVM 非压缩指针 |
HashMap_empty | 48 | 96 |
HashMap_full | 800 | 1040 |
Emp_empty | 56 | 87 |
Emp_full | 280 | 362 |
从上面测试结果看,数据到JAVA里占用的空间增加了许多,在64位压缩指针下,如果存到HashMap,需要800字节,空间是数据库约16.6倍,如果存为Emp普通对象,需要280字节,是数据库的5倍。
如果我们是一个分页从数据库读取emp信息,每页显示50条记录,用List保存,HashMap需要39KB,emp对象需要13KB。
以上是一个通用数据测试结论,估大家参考。
下面是测试代码:
package src.test.java;
import net.sourceforge.sizeof.SizeOf;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.*;
public class SizeOfTest {
static {
SizeOf.skipStaticField(true); //java.sizeOf will not compute static fields
//SizeOf.skipFinalField(true); //java.sizeOf will not compute final fields
//SizeOf.skipFlyweightObject(true); //java.sizeOf will not compute well-known flyweight objects
}
public static void main(String[] args) throws SQLException, IOException, IllegalAccessException {
SizeOfTest ts = new SizeOfTest();
ts.objectSize();
ts.dataSize();
System.out.println("ok");
}
public void objectSize() {
System.out.println("Integer:" + SizeOf.deepSizeOf(new Integer(22)));
System.out.println("Long:" + SizeOf.sizeOf(new Long(33L)));
System.out.println("Object:" + SizeOf.sizeOf(new Object()));
System.out.println("Date:" + SizeOf.sizeOf(new Date()));
System.out.println("Timestamp:" + SizeOf.sizeOf(new Timestamp(System.currentTimeMillis())));
System.out.println("String_0:" + SizeOf.deepSizeOf(new String()));
System.out.println("String_1:" + SizeOf.deepSizeOf(new String("1")));
System.out.println("String_10:" + SizeOf.deepSizeOf(new String("213214555")));
System.out.println("String_100:" + SizeOf.deepSizeOf("6743222211"));
System.out.println("StringBuilder:" + SizeOf.deepSizeOf(new StringBuilder()));
System.out.println("BigDecimal:" + SizeOf.deepSizeOf(new BigDecimal("21313")));
System.out.println("BigInteger:" + SizeOf.deepSizeOf(new BigInteger("34535643")));
System.out.println("HashMap:" + SizeOf.deepSizeOf(new HashMap()));
System.out.println("HashMap_0:" + SizeOf.deepSizeOf(new HashMap(0)));
System.out.println("HashMap_100:" + SizeOf.deepSizeOf(new HashMap(100)));
System.out.println("HashMap_10000:" + SizeOf.deepSizeOf(new HashMap(10000)));
System.out.println("ArrayList:" + SizeOf.deepSizeOf(new ArrayList()));
System.out.println("ArrayList_0:" + SizeOf.deepSizeOf(new ArrayList(0)));
System.out.println("ArrayList_100:" + SizeOf.deepSizeOf(new ArrayList(100)));
System.out.println("ArrayList_10000:" + SizeOf.deepSizeOf(new ArrayList(10000)));
System.out.println("LinkedList:" + SizeOf.deepSizeOf(new LinkedList<Object>()));
System.out.println("LinkedHashMap:" + SizeOf.deepSizeOf(new LinkedHashMap<Object, Object>()));
System.out.println("Class1:" + SizeOf.deepSizeOf(new Text1()));
System.out.println("Class2:" + SizeOf.deepSizeOf(new Text2()));
System.out.println("Class3:" + SizeOf.deepSizeOf(new Tlass3()));
}
public void dataSize() throws IOException, IllegalAccessException {
HashMap hm = new HashMap();
System.out.println("HashMap_empty:" + SizeOf.deepSizeOf(hm));
hm.put("id", 32466);
hm.put("name", "鸿运汽车");
hm.put("setupDate", new Date());
hm.put("setupTime", new Timestamp(System.currentTimeMillis()));
hm.put("unitPrice", new BigDecimal(100.00));
System.out.println("HashMap_full:" + SizeOf.deepSizeOf(hm));
Emp emp = new Emp();
System.out.println("Emp_empty:" + SizeOf.deepSizeOf(emp));
emp.setId(32466);
emp.setName("鸿运汽车");
emp.setSetupDate(new Date());
emp.setSetupTime(new Timestamp(System.currentTimeMillis()));
emp.setUnitPrice(new BigDecimal(100.00));
System.out.println("Emp_full_deepSizeOf:" + SizeOf.deepSizeOf(emp));
System.out.println("Emp_full_sizeOf:" + SizeOf.sizeOf(emp));
}
class Text1 {
}
class Text2 extends Text1 {
}
class Tlass3 extends Text2 {
}
class Emp {
private Integer id;
private Timestamp setupTime;
private Date setupDate;
private String name;
private BigDecimal UnitPrice;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Timestamp getSetupTime() {
return setupTime;
}
public void setSetupTime(Timestamp setupTime) {
this.setupTime = setupTime;
}
public Date getSetupDate() {
return setupDate;
}
public void setSetupDate(Date setupDate) {
this.setupDate = setupDate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public BigDecimal getUnitPrice() {
return UnitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
UnitPrice = unitPrice;
}
}
}
在上述代码测试中,还需要添加RUN启动JVM options配置,不加配置会报错:
这是因为 SizeOf 用到了 JDK1.5 后新加入的 java.lang.instrument.Instrumentation 接口,所以需要为TestSize 设置 VM arguments
我的SizeOf.jar在F盘*路径下,参数配置为:-javaagent:F:\2019java\java-sizeOf\SizeOf.jar
添加完成后,重新运行TestSize类
可以看到,数据在java中占用内存都打印出来,默认为Byte字节,可自由转换
KB=(Byte)*1024
MB=(Byte)*1024*1024
在上述打印中,细心的小伙伴已经关注到了最后两行:
Emp_full_deepSizeOf:280
Emp_full_sizeOf:40
这块为什么都是接收,使用不同函数后,得到的字节却不同呢,原因如下:
这3行的使用方法基本包含了这个项目的所有内容,1.计算一层内存占用大小,2.计算所有引用关系包含的内存占用。
newInstance的初始化里面初始化了3种不同的计算Class实例大小的方法:
按照这个顺序进行优先初始化,如果失败了才会使用后面的方法。从前到后也是建议的使用顺序,反射的效率是最低的。
根据这个简单测试,我们可以总结一个结论:
1.数据库记录放在JAVA里,用对象(ORM一般的处理方式)需要3-4倍左右的内存空间,用HashMap这种KV保存需要10倍空间;
2.如果你主要数据是text大文本,那空间一般可以按2倍估算。