在我的应用程序的某些部分,我将 17MB 日志文件解析为列表结构 - 每行一个 LogEntry。大约有 100K 行/日志条目,这意味着大约。每行 170 字节。令我惊讶的是,即使我指定 128MB(256MB 似乎就足够了),我还是用完了堆空间。 10MB 的文本变成对象列表如何导致空间增加十倍?
我知道 String 对象使用的空间量至少是 ANSI 文本(Unicode,一个字符 = 2 个字节)的两倍,但这至少消耗了四倍。
我正在寻找的是 n 个 LogEntries 的 ArrayList 将消耗多少的近似值,或者我的方法如何创建无关的对象来加剧情况(请参阅下面关于 String 的评论.trim()
)
这是我的 LogEntry 类的数据部分
public class LogEntry {
private Long id;
private String system, version, environment, hostName, userId, clientIP, wsdlName, methodName;
private Date timestamp;
private Long milliSeconds;
private Map<String, String> otherProperties;
这是进行读取的部分
public List<LogEntry> readLogEntriesFromFile(File f) throws LogImporterException {
CSVReader reader;
final String ISO_8601_DATE_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS";
List<LogEntry> logEntries = new ArrayList<LogEntry>();
String[] tmp;
try {
int lineNumber = 0;
final char DELIM = ';';
reader = new CSVReader(new InputStreamReader(new FileInputStream(f)), DELIM);
while ((tmp = reader.readNext()) != null) {
lineNumber++;
if (tmp.length < LogEntry.getRequiredNumberOfAttributes()) {
String tmpString = concat(tmp);
if (tmpString.trim().isEmpty()) {
logger.debug("Empty string");
} else {
logger.error(String.format(
"Invalid log format in %s:L%s. Not enough attributes (%d/%d). Was %s . Continuing ...",
f.getAbsolutePath(), lineNumber, tmp.length, LogEntry.getRequiredNumberOfAttributes(), tmpString)
);
}
continue;
}
List<String> values = new ArrayList<String>(Arrays.asList(tmp));
String system, version, environment, hostName, userId, wsdlName, methodName;
Date timestamp;
Long milliSeconds;
Map<String, String> otherProperties;
system = values.remove(0);
version = values.remove(0);
environment = values.remove(0);
hostName = values.remove(0);
userId = values.remove(0);
String clientIP = values.remove(0);
wsdlName = cleanLogString(values.remove(0));
methodName = cleanLogString(stripNormalPrefixes(values.remove(0)));
timestamp = new SimpleDateFormat(ISO_8601_DATE_PATTERN).parse(values.remove(0));
milliSeconds = Long.parseLong(values.remove(0));
/* remaining properties are the key-value pairs */
otherProperties = parseOtherProperties(values);
logEntries.add(new LogEntry(system, version, environment, hostName, userId, clientIP,
wsdlName, methodName, timestamp, milliSeconds, otherProperties));
}
reader.close();
} catch (IOException e) {
throw new LogImporterException("Error reading log file: " + e.getMessage());
} catch (ParseException e) {
throw new LogImporterException("Error parsing logfile: " + e.getMessage(), e);
}
return logEntries;
}
用于填充 map 的实用函数
private Map<String, String> parseOtherProperties(List<String> values) throws ParseException {
HashMap<String, String> map = new HashMap<String, String>();
String[] tmp;
for (String s : values) {
if (s.trim().isEmpty()) {
continue;
}
tmp = s.split(":");
if (tmp.length != 2) {
throw new ParseException("Could not split string into key:value :\"" + s + "\"", s.length());
}
map.put(tmp[0], tmp[1]);
}
return map;
}
最佳答案
那里还有一个 map ,您可以在其中存储其他属性。您的代码不会显示此映射是如何填充的,但请记住,与条目本身所需的内存相比,映射可能具有大量内存开销。
支持Map的数组的大小(至少16个条目* 4字节)+每个条目一个键/值对+数据本身的大小。两个映射条目,每个条目使用 10 个字符作为键,10 个字符作为值,将消耗 16*4 + 2*2*4 + 2*10*2 + 2*10*2 + 2*2*8= 64+16+ 40+40+24 = 184 个字节(1 个字符 = 2 个字节,一个 String 对象至少消耗 8 个字节)。仅此一项就几乎使整个日志字符串的空间需求增加了一倍。
此外,LogEntry 包含 12 个对象,即至少 96 个字节。因此,在没有 Map 和实际字符串数据的情况下,仅日志对象就需要大约 100 个字节(或多或少)。加上所有引用文献的指针(每个 4B)。我用 Map 数了一下,至少有 18 个,即 72 个字节。
添加数据(上一段中提到的对象引用和对象“ header ”):
2 个 long = 16B,1 个日期存储为 long = 8B,映射 = 184B。另外还有字符串内容,比如 90 个字符 = 180 个字节。当放入列表时,列表项的每一端可能有一两个字节,因此每个日志行总共大约有 100+72+16+8+184+180=560 ~ 600 字节。
因此,每条日志行大约 600 字节,这意味着 100K 行至少会消耗大约 60MB 的空间。这将使其至少与设置为 asize 的堆大小处于相同的数量级。此外,tmpString.trim() in a loop might be creating copies of string 。类似地,String.format() 也可能会创建副本。应用程序的其余部分也必须适合此堆空间,并且可以解释其余内存的去向。
关于java - 将 17MB 文本文件解析为 List 如何导致 128MB 堆内存不足?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14750202/