0

在我的应用程序的某些部分,我将一个 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;
}

用于填充地图的实用函数

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;
}
4

2 回答 2

2

您还在那里有一个地图,您可以在其中存储其他属性。您的代码没有显示此 Map 是如何填充的,但请记住,与条目本身所需的内存相比,Maps 可能具有大量内存开销。

支持 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 char = 2 字节,String 对象占用最少 8 字节)。仅此一项就会使整个日志字符串的空间需求几乎翻倍。

此外,LogEntry 包含 12 个对象,即至少 96 个字节。因此,单独的日志对象将需要大约 100 个字节,给予或获取一些,没有 Map 和没有实际的字符串数据。加上引用的所有指针(每个 4B)。我用 Map 至少数了 18 个,这意味着 72 个字节。

添加数据(上一段中提到的-object 引用和对象“标题”):
2 个 longs = 16B,1 个存储为 long = 8B 的日期,map = 184B。此外还有字符串内容,比如 90 个字符 = 180 个字节。当放入列表时,列表项的每一端可能有一两个字节,因此每个日志行总共大约 100+72+16+8+184+180=560 ~ 600 字节。

所以每个日志行大约 600 字节,这意味着 100K 行至少会消耗大约 60MB。这将使其至少处于与设置为大小的堆大小相同的数量级。此外,循环中的 tmpString.trim() 可能会创建 string 的副本。同样 String.format() 也可能会创建副本。应用程序的其余部分也必须适合这个堆空间,并且可以解释其余内存的去向。

于 2013-02-07T11:47:49.693 回答
0

不要忘记每个String对象都会消耗实际定义的空间(24 字节?)Object,加上对 char 数组的引用、偏移量(用于substring()使用)等。因此,将一行表示为“n”个字符串将增加额外的存储要求. LogEntry你能在课堂上懒洋洋地评估这些吗?

(关于字符串偏移量的使用 - 在 Java 7b6 之前,它String.substring()充当现有 char 数组的窗口,因此您需要一个偏移量。这最近发生了变化,可能值得确定以后的 JDK 构建是否更节省内存)

于 2013-02-07T11:40:25.223 回答