【问题标题】:Java : OutOfMemoryError even after using GSON Streaming APIJava:即使在使用 GSON Streaming API 之后也出现 OutOfMemoryError
【发布时间】:2021-09-24 07:08:00
【问题描述】:

我一直在研究一个问题陈述,其中我们收到了一个巨大的 JSON 响应,当我们使用传统的 gson 解析技术对其进行解析时,它曾经给出 OutOfMemoryException,因为此方法在处理数据之前将数据存储在内存中,所以作为对此的解决方案,我一直致力于流式传输 JSON 响应,它不会将所有内容都放入内存中,因此它工作正常,直到大约 160 万条记录,之后甚至中断。所以这是我们得到的例外。

线程“主”java.lang.OutOfMemoryError 中的异常:Java 堆空间

这是我为此使用的全部代码:

// Getting reponse into InputStream and casting it to JsonReader object for parsing
InputStream liInStream = luURLConn.getInputStream();
lCycleTimeReader = new JsonReader(new InputStreamReader(liInStream, "UTF-8"));

我们的 JSON 如下所示:

{
"Report_Entry": [
    {
       "key1": "value",
       "key2": "value",
       "key3": "value",
       "key4": "value",
       "key5": "value"
    },
    {
       "key1": "value",
       "key2": "value",
       "key3": "value",
       "key4": "value",
       "key5": "value"
    }
]}

在我们的解析方法中使用这个对象:

public HashMap<String, HashMap<String, String>> getcycleTimeMap(JsonReader poJSONReaderObj,
        CycleTimeConstant cycleTimeConstant, int processId) {

    Integer counter = 0;
    HashMap<String, HashMap<String, String>> cycleTimeMap = new HashMap<String, HashMap<String, String>>();
    HashMap<String, HashMap<String, String>> finalcycleTimeMap = new HashMap<String, HashMap<String, String>>();

    try {
        CycleTime cycleTime = new CycleTime();

        poJSONReaderObj.beginObject();
        while (poJSONReaderObj.hasNext()) {

            String name = poJSONReaderObj.nextName();
            if (name.equals("Report_Entry")) {
                poJSONReaderObj.beginArray();
                while (poJSONReaderObj.hasNext()) {

                    JsonToken nextToken2 = poJSONReaderObj.peek();
                    if (JsonToken.BEGIN_OBJECT.equals(nextToken2)) {
                        poJSONReaderObj.beginObject();
                    } else if (JsonToken.END_OBJECT.equals(nextToken2)) {
                        poJSONReaderObj.endObject();
                    } else {
                        String nextString = "";
                        if (JsonToken.STRING.equals(nextToken2)) {
                            nextString = poJSONReaderObj.nextString();
                        } else if (JsonToken.NAME.equals(nextToken2)) {
                            nextString = poJSONReaderObj.nextName();
                        }

                        switch (nextString) {
                        case "key1":
                            cycleTime.setKey1(poJSONReaderObj.nextString());
                            break;
                        case "key2":
                            cycleTime.setKey2(poJSONReaderObj.nextString());
                            break;
                        case "key3":
                            cycleTime.setKey3(poJSONReaderObj.nextString());
                            break;
                        case "key4":
                            cycleTime.setKey4(poJSONReaderObj.nextString());
                            break;
                        case "key5":
                            cycleTime.setKey5(poJSONReaderObj.nextString());
                            break;
                        }
                    }

                    poJSONReaderObj.endObject();

                    System.out
                            .println("Value of Map is : " + new Gson().toJson(cycleTime) + "counter  : " + counter);
                    counter++;
                    System.out.println("Counter : " + counter);
                    cycleTimeMap = (HashMap<String, HashMap<String, String>>) cycleTimeBpProcessIterator(
                        cycleTime, cycleTimeConstant, counter, processId);
                }
                finalcycleTimeMap.putAll(cycleTimeMap);
            }
        }
        JsonToken nextToken = poJSONReaderObj.peek();
        if (JsonToken.END_OBJECT.equals(nextToken)) {
            poJSONReaderObj.endObject();
        } else if (JsonToken.END_ARRAY.equals(nextToken)) {
            poJSONReaderObj.endArray();
        }
    } catch (IOException ioException) {
        ioException.printStackTrace();
    }

    System.out.println("FINAL MAP TO BE LOADED : " + new Gson().toJson(finalcycleTimeMap));

    return finalcycleTimeMap;

}

处理响应的POJO类:

 public class CycleTime {
    
    private String key1 = "";
    private String key2 = "";
    private String key3 = "";
    private String key4 = "";
    private String key5 = "";
    
    
    public String getKey1() {
        return key1;
    }
    public void setKey1(String key1) {
        this.key1 = key1;
    }
    public String getKey2() {
        return key2;
    }
    public void setKey2(String key2) {
        this.key2 = key2;
    }
    public String getKey3() {
        return key3;
    }
    public void setKey3(String key3) {
        this.key3 = key3;
    }
    public String getKey4() {
        return key4;
    }
    public void setKey4(String key4) {
        this.key4 = key4;
    }
    public String getKey5() {
        return key5;
    }
    public void setKey5(String key5) {
        this.key5 = key5;
    }
    
}

我不确定这里可能是罪魁祸首,但似乎它给出了相同的错误,我想知道下一个避免这种 OutOfMemoryException 的方法应该是什么。

【问题讨论】:

  • 您确定这是您的实际代码吗? cycleTimeMap 未使用,finalcycleTimeMap 从未修改。
  • @tgdavies,抱歉代码丢失了。现已添加,请查看
  • 大概finalcycleTimeMap 变得非常大。你检查过它使用了多少空间吗?
  • 我们尚未对此进行广泛检查,但这可能是因为数据非常庞大的原因:仅供参考,我们已经提取了 1 天的 json,文件大小为 480MB
  • 你的最大堆大小是多少?

标签: java gson out-of-memory streaming


【解决方案1】:

将整个文档读入单个对象并不意味着流式阅读会对您有所帮助。 此外,Gson 在后台使用流式传输,因为它只是一种可选的读写方式。 但是,您的方法远非良好:

  • Gson 事情:
    • 最主要的是:正确使用 Gson 并让它发挥作用。我无法为您提供的 JSON 文档运行您的代码:它既不适用于根 JSON 对象,也不适用于唯一的顶级对象条目(因此您的反序列化器由于不正确使用 hasNextbeginObject 而被破坏/endObject 对)。
  • 常见的 Java 事物:
    • 不要在返回部分组合对象的中间捕获异常(正确吗?);
    • 不要使用Throwable.printStackTrace(使用适当的日志记录工具);
    • 如果您不想使用记录器,请将其打印到System.err(这只是用于此类目的的适当标准流);
    • Integer 作为计数器是个坏主意,因为它会创建许多装箱值,尤其是对于大型文档(使用 int -- 很好);
    • enum 值可以(并且应该)检查与 == 是否相等(这是安全的,因为它们是单例);
    • 那么,您也可以使用switch 来处理枚举(两者都缩短了,编译时更安全);
    • 不要在循环中创建 Gson 实例,尤其是具有那么多迭代的实例(Gson 实例被认为是不可变的,因为它是线程安全的,但在构造它的对象时并不便宜);
    • 不要使用可以有静态类型纯对象的映射(最好);
    • 返回一个永远只有一个键值对映射的目的是什么? (返回值);
  • 常见的设计东西:
    • 对声明使用尽可能通用的类型:不是HashMap,而是Map(如果有一天您需要另一个带有有序键的映射呢?或者如果您根本不需要映射呢?);李>
    • 反向依赖(如果你不需要有五个键的CycleTime怎么办?);
  • 流媒体:
    • 如果它在 OOM 错误中运行,那么收集显然无法容纳您的应用程序 RAM 的巨大地图有什么意义? (使用回调或承诺(推送方法)来处理单个元素、迭代器或流(拉取方法)、反应流(推送方法)等等);
    • 仅针对较小的内存占用或使用聚合收集结果(否则您将面临 OOM 的风险)。

这是通过回调使用推送方法来减少内存占用的方法:

@UtilityClass
public final class StreamSupport {

    public static void acceptArrayElements(@WillNotClose final JsonReader jsonReader, final Consumer<? super JsonReader> acceptElement)
            throws IOException {
        jsonReader.beginArray();
        while ( jsonReader.hasNext() ) {
            acceptElement.accept(jsonReader);
        }
        jsonReader.endArray();
    }

}
@UtilityClass
public final class CycleDeserializer {

    public static void readCycles(final JsonReader jsonReader, final Consumer<? super JsonReader> acceptJsonReader)
            throws IOException {
        jsonReader.beginObject();
        while ( jsonReader.hasNext() ) {
            switch ( jsonReader.nextName() ) {
            case "Report_Entry":
                StreamSupport.acceptArrayElements(jsonReader, acceptJsonReader);
                break;
            default:
                jsonReader.skipValue();
                break;
            }
        }
        jsonReader.endObject();
    }

}
private static final Gson gson = new GsonBuilder()
        .disableHtmlEscaping()
        .disableInnerClassSerialization()
        .create();

@Test
public void test()
        throws IOException {
    try ( final JsonReader jsonReader = openTheHugeDocument() ) {
        CycleDeserializer.readCycles(jsonReader, jr -> {
            final CycleTime cycleTime = gson.fromJson(jr, CycleTime.class);
            System.out.println(cycleTime);
        });
    }
    // do the simplest aggregation operation: `COUNT`
    try ( final JsonReader jsonReader = openTheHugeDocument() ) {
        final AtomicInteger count = new AtomicInteger();
        CycleDeserializer.readCycles(jsonReader, jr -> {
            try {
                jr.skipValue();
                count.incrementAndGet();
            } catch ( final IOException ex ) {
                throw new RuntimeException(ex);
            }
        });
        System.out.println("Count = " + count);
    }
    // this will probably fail when the document is huge because it is collected into a single collection
    // (you need to let your JVM use as much RAM as possible if it is a must for you)
    try ( final JsonReader jsonReader = openTheHugeDocument() ) {
        final Collection<CycleTime> cycleTimes = new ArrayList<>();
        CycleDeserializer.readCycles(jsonReader, jr -> {
            final CycleTime cycleTime = gson.fromJson(jr, CycleTime.class);
            cycleTimes.add(cycleTime);
        });
        System.out.println("Count in list = " + cycleTimes.size());
    }
}

如您所见,在上面的运行器中,您可以选择自己喜欢的方式来处理您的条目:哑日志、简单计数或简单的收集操作。

有关通过Stream 方法的拉取方法,请参阅:https://stackoverflow.com/a/69282822/12232870

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-03
    • 2016-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多