项目中的代码大量充斥着一个返回列表的函数返回
new ArrayList<>()
, 我的概念中完全可以用Collections.emptyList()
来代替,可是这么到底有多少的内存节省呢? 今天来探究一下
先找一个工具类
一个对象占用多少字节?
新建 Maven 项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>objtest</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<finalName>test</finalName>
<archive>
<manifestEntries>
<Premain-class>com.demo.objtest.SizeOfObject</Premain-class>
<Boot-Class-Path></Boot-Class-Path>
<Can-Redefine-Classes>false</Can-Redefine-Classes>
</manifestEntries>
<addMavenDescriptor>false</addMavenDescriptor>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
SizeOfObejct.java
package com.demo.objtest;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
public class SizeOfObject {
static Instrumentation inst;
public static void premain(String args, Instrumentation instP) {
inst = instP;
}
/**
* 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、<br></br>
* 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;<br></br>
* 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小 <br></br>
*
* @param obj
* @return
*/
public static long sizeOf(Object obj) {
return inst.getObjectSize(obj);
}
/**
* 递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小
*
* @param objP
* @return
* @throws IllegalAccessException
*/
public static long fullSizeOf(Object objP) throws IllegalAccessException {
Set<Object> visited = new HashSet<>();
Deque<Object> toBeQueue = new ArrayDeque<>();
toBeQueue.add(objP);
long size = 0L;
while (toBeQueue.size() > 0) {
Object obj = toBeQueue.poll();
//sizeOf的时候已经计基本类型和引用的长度,包括数组
size += skipObject(visited, obj) ? 0L : sizeOf(obj);
Class<?> tmpObjClass = obj.getClass();
if (tmpObjClass.isArray()) {
//[I , [F 基本类型名字长度是2
if (tmpObjClass.getName().length() > 2) {
for (int i = 0, len = Array.getLength(obj); i < len; i++) {
Object tmp = Array.get(obj, i);
if (tmp != null) {
//非基本类型需要深度遍历其对象
toBeQueue.add(Array.get(obj, i));
}
}
}
} else {
while (tmpObjClass != null) {
Field[] fields = tmpObjClass.getDeclaredFields();
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers()) //静态不计
|| field.getType().isPrimitive()) { //基本类型不重复计
continue;
}
field.setAccessible(true);
Object fieldValue = field.get(obj);
if (fieldValue == null) {
continue;
}
toBeQueue.add(fieldValue);
}
tmpObjClass = tmpObjClass.getSuperclass();
}
}
}
return size;
}
/**
* String.intern的对象不计;计算过的不计,也避免死循环
*
* @param visited
* @param obj
* @return
*/
static boolean skipObject(Set<Object> visited, Object obj) {
if (obj instanceof String && obj == ((String) obj).intern()) {
return true;
}
return visited.contains(obj);
}
public static void main(String[] args) throws IllegalAccessException {
System.out.println("sizeOf(new ArrayList<>()): " + sizeOf(new ArrayList<>()));
System.out.println("fullSizeOf(new ArrayList<>()): " + fullSizeOf(new ArrayList<>()));
System.out.println("sizeOf(Collections.emptyList()): " + sizeOf(Collections.emptyList()));
System.out.println("fullSizeOf(Collections.emptyList()): " + fullSizeOf(Collections.emptyList()));
}
}
先打包
$mvn package
我在 Idea 里直接运行的,配置 vm 参数
运行结果
sizeOf(new ArrayList<>()): 24
fullSizeOf(new ArrayList<>()): 40
sizeOf(Collections.emptyList()): 16
fullSizeOf(Collections.emptyList()): 16
分析
想一下实际的场景,比如一个文章接口,文章本身有内容,拉评论列表,拉关联信息列表,拉标签列表,遍历节点拉节点关联表,这几个操作是肯定要执行的,其他的先不算,这就 4 个 List 返回,如果用 new ArrayList<>()
返回就会生成 4 个对象,占用 160 字节, 而 Collections.emptyList()
都指向同一个对象,只占用 16 字节。
看了下实际 QPS 是 181,那么每秒要生成 4*181=724
个 ArrayList
对象,内存 28K , 算到 5分钟, 内存就要消耗 8.3M,显然 GC 来清理这些的消耗就显得有点浪费, Collections.emptyList()
的话完全没有 GC 负担。不过实际情况应该不会存在那么多空列表。
最后一个问题就是,有人说返回的 List 万一要用呢?
我觉得吧,这是一个编码习惯和思想的问题,接受其他方法返回的列表并不应该直接使用,首先得检验可用性,然后再接受结果进行处理,处理也应该基于自己的列表,变量作用域应该尽量仅限于本函数,不然会带来很多不可预料的结果。函数式编程的方法论大抵如此。