上个月接到一个任务,是要对某工厂中的所有流程表的内容进行Excel导出,领导估计这个任务应该是个比较大的活儿,所以先让我做一下开发这个功能的整体评估。
我照惯例百度了半天,在看了大半天的广告之后几乎没有现成的案例,不禁感叹百度和谷歌在商业模式和格局之间的巨大差异,这里只属于个人吐槽,所以就不记录了。
百度查不到,谷歌也用不了(没有翻墙),怎么办呢?
想偷懒都不行,这下只能亲自动手整理了。
评估报告嘛早就提交了,鉴于今后这样类似的评估可能会时常出现,所以我觉得还是有必要记录一下评估中比较重要的思路。
我的评估大致从这5个维度触发来预估计算:开发周期、执行效率、内存占用、数据库占用和潜在风险。
当然分析的基础条件还是已经想好的具体方案,这5个维度都是来计算和衡量具体方案的。
今天主要是记录关于之前整理的此需求的功能可能会涉及到的内存占用情况,因为一不小心就可能搞出个大家伙(内存溢出)O(∩_∩)O哈哈~!
今天选择其中的一个方案来分享,如下:
方案一:沿用原业务逻辑
方案说明:此方案需要沿用原来的查询逻辑,循环执行以查询出所有流程的信息。
开发周期:预估3个工作日
执行效率:预估20分钟以上的执行时间。
内存占用:预估占用内存空间366MB以上。
总数据量:10W+
潜在风险:1、用户在导出过程中不能切换到其他页面。
2、需要控制同时导出的请求次数,否者将可能引起内存溢出,从而影响程序运行。
3、此导出执行期间,将会连带导致其他操作的时间拉长。
开发周期,潜在风险什么的就先不解释了。至于执行效率为啥这么慢,因为需要循环调用别人写好的业务逻辑来查询需要的所有数据,所以执行效率的瓶颈在于别人写好的存储过程☺。
具体流程为:将数据库中的10W条数据全部查询出来,然后拼接组装好后存放到内存中,所有数据处理完后,导出成Excel文档。
虽然好像可以使用响应式处理流的方式来提高性能,但是小弟我暂时不会 ^_^!
所以本次评估的重点在于这10W条数据将会占用多少内存空间。
还有个基础条件是:这10W条数据中的每一条数据都包含17个字段,其中有 long 类型,也有 int 类型,但是最多的还是 String 类型至少12个以上。
为了方面数据的读取,我将组装拼接好的每一条数据都用一个对象来保存。这样的话,简化下来,我就只需要按照最大数据来计算出一个对象在内存中的占用量,然后在×10W就行了。
先简单介绍下,
Java对象的内存布局:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
另外:不同的环境结果可能有差异,我所在的环境是HotSpot虚拟机,64位Windwos。
对象头
对象头在32位系统上占用8bytes,64位系统上占用16bytes。
实例数据
原生类型(primitive type)的内存占用如下:
reference类型在32位系统上每个占用4bytes, 在64位系统上每个占用8bytes。
对齐填充
HotSpot的对齐方式为8字节对齐:
(对象头 + 实例数据 + padding) % 8等于0且0 <= padding < 8
指针压缩
对象占用的内存大小收到VM参数UseCompressedOops的影响。32G内存以下的,默认开启对象指针压缩。
我们做评估的话,可以将这它排除掉,按照最大的数据的占用量来计算嘛罒ω罒!
数组对象
64位机器上,数组对象的对象头占用24个字节(8字节MarkWord+8字节类型指针+8字节数组长度)。之所以比普通对象占用内存多是因为需要额外的对象头空间存储数组的长度。
那么现在先还原一下之前我用来存储数据的对象:
public class StringObject { private int i1; //4bytes private int i2; //4bytes private long l1; //8bytes private long l2; //8bytes private long l3; //8bytes private String s1; private String s2; private String s3; private String s4; private String s5; private String s6; private String s7; private String s8; private String s9; private String s10; private String s11; private String s12; }
目前除了不知道String类型的占用大小外,其他属性的大小占用量可以确认= 16(对象头)+ 4*2(int类型2个) + 8*3(long类型3个) +…
接下来确认String类型的占用量。
String也是一个对象,所以也会有它的属性,所以根据它的属性就可以计算它的占用量了,
String的属性如下:
private final char value[]; //char数组,数组也是对象 /** Cache the hash code for the string */ private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L;
在 Java 里数组也是对象,因而数组也有对象头,故一个数组所占的空间为对象头所占的空间加上数组长度加上数组的引用,即 16 + 4 + 4= 24 字节 。
那么一个空 String 所占空间为:
对象头(16 字节)+ 引用 (8 字节 ) + char 数组(24 字节)+ 1个 int(4字节)+ 1个long(8字节)= 60 字节。
String占用内存计算公式:60 + 2*n,n为字符串长度。
那么这个 n 怎么来确定呢?
这就要看数据库中对应的字段存储的长度是多少了。
我的项目中用的数据库是Oracle,并且发现,之前的表设计中,String对应的每个字段都是用的清一色的 varchar(128)。正好便于计算O(∩_∩)O~。
按照这样来计算的话,
我这里的String对象将会达到:60 + 2 * 128 的长度,
因此我所需要的复合对象 StringObject 将会占用:16(对象头)+ 4*2(int类型2个) + 8*3(long类型3个) + (60 + 2 *128)*12(String类型12个) + 0(对齐填充) = 3840 bytes
根据以上的分析,得出结论:在内存中存储所有对象需要消耗内存约为 3840 × 100000 ≈ 366M。
至此对于这次需求对应功能涉及的内存占用评估结论已经得出,当然这也只是理论上最大的内存占用量,实际的情况应该会比这个结果小一些。
以上只是一个计算思路,也适用于其他复合对象的计算。
最后还有几个需要注意的地方:
1、所有数据都放在同一个集合中,这样好吗?
对于数据量比较大的时候来说,这样当然不好,集合最终是由数组来存储数据的,而数组在内存中需要开辟连续的空间,像上述功能中10W+长度的数组对于JVM来说肯定是“搞事情”。
建议将一个大集合拆分成多个小集合,最好是一个大的对象中包含几个小的对象,几个小的对象中包含了小的集合。
举个栗子:人体有108块骨头,如果用一个集合来装的话,也可以。
但是如果将108块骨头拆分成:上肢对象和下肢对象这2个对象的话,就可以将一个大集合拆分成2中集合。如果再把上肢拆分成:头部、双臂、躯体 的话,又可以进一步拆分,这样就可以将108块骨头拆分成很多个包含小集合的对象了。
2、对于集合的声明,有必要初始化容量吗?
对于声明集合时,已知或者大概知晓容量时,先声明出容量当然是有必要的。
如果我需要声明一个装10W+数据的集合
List<StringObject> list = new ArrayList<>(); //不声明容量
List<StringObject> list = new ArrayList<>(100000); //声明容量
比如以上代码中,在声明了容量的情况下,至少可以节省 13 次的扩容操作(集合的每次扩容,都增加原来的一倍长度)。
目前就暂时想到了这么多,如果以后想起什么其他方面的内容话,再来补充!(#^.^#)