【问题标题】:Fixing GC Overhead Limit Exceeded Without Increasing Heap Size在不增加堆大小的情况下修复超过 GC 开销限制
【发布时间】:2017-05-26 15:08:30
【问题描述】:

我正在开发一个 Java 程序,该程序将从 Sybase 数据库中获取数据,并使用 UCanAccess 将其导入 Microsoft Access 数据库。但是,我目前遇到了一个问题,收到错误“java.lang.OutOfMemoryError:GC 开销限制超出”。

为了说明情况,我尝试将大约 130 万条记录导入 Access 数据库。该程序当前在导入大约 800,000 条这些记录后遇到错误,在运行时大约十分钟,并且在从 Sybase 数据库检索到 ResultSet 之后很长时间。

我试图修改堆大小,但这会导致程序显着变慢。请注意,这是一个临时程序,可以根据需要运行多次,因此运行时间应该在几分钟或几小时左右,而根据我的观察,增加堆大小会增加运行时间天。

作为参考,错误发生在 main 方法中,在称为 getRecords 的子例程期间(发生这种情况的确切代码行因运行而异)。我已将代码包含在下面的程序中,并对部分代码进行了一些小改动,例如我正在使用的确切查询以及访问数据库的用户名和密码,以免泄露敏感信息。

我可以在我的程序代码中更改什么以减轻垃圾收集器的负载,而不会将运行时间增加几个小时以上吗?

编辑:看来我误认为 Java 的默认最大堆大小。当我认为通过将堆大小设置为 512m 来增加堆大小时,我无意中将堆大小减半。当我将堆大小设置为 2048m 时,我收到了 java 堆空间错误。如果可能的话,我仍然想在不修改堆大小的情况下解决问题。

编辑 2:显然,我被误导了我需要处理的一些记录。它是我最初认为的大小的两倍,这表明我需要彻底改变我的方法。继续接受答案,因为该答案确实带来了很大的改进。

getRecords 方法:

   public static void getRecords(SybaseDatabase sdb, AccessDatabase adb)
    {
        ArrayList<Record> records = new ArrayList<Record>();
        StringBuffer sql = new StringBuffer();
        Record currentRecord = null;
        try{
            Statement sybStat = sdb.connection.createStatement();
            PreparedStatement resetADB = adb.connection.prepareStatement("DELETE FROM Table");
            PreparedStatement accStat = adb.connection.prepareStatement("INSERT INTO Table (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
sql.append(query);//query is a placeholder, as I cannot give out the actual query to the database. I have confirmed that the query itself gives the ResultSet that I am looking for
            ResultSet rs = sybStat.executeQuery(sql.toString());
            resetADB.executeUpdate();
            boolean nextWatch = true;
            Integer i = 1;
            Record r = new Record();
            while(nextWatch)
            {
                for (int j = 0; j < 1000 && nextWatch; j++)
                {
                    nextWatch = rs.next();

                    r.setColumn(i, 0);
                    r.setColumn(rs.getString("B"), 1);
                    r.setColumn(rs.getString("C"), 2);
                    r.setColumn(rs.getString("D"), 3);
                    r.setColumn(rs.getString("E"), 4);
                    r.setColumn(rs.getString("F"), 5);
                    r.setColumn(rs.getString("G"), 6);
                    r.setColumn(rs.getString("H"), 7);
                    r.setColumn(rs.getString("I"), 8);
                    r.setColumn(rs.getString("J"), 9);
                    r.setColumn(rs.getString("K"), 10);
                    r.setColumn(rs.getInt("L"), 11);
                    r.setColumn(rs.getString("M"), 12);
                    r.setColumn(rs.getString("N"), 13);
                    r.setColumn(rs.getString("O"), 14);
                    r.setColumn(rs.getString("P"), 15);

                    records.add(r);
                    i++;
                }

                for(int k = 0; k < records.size(); k++)
                {
                    currentRecord = records.get(k);

                    for(int m = 0; m < currentRecord.getNumOfColumns(); m++)
                    {
                        if (currentRecord.getColumn(m) instanceof String)
                        {
                            accStat.setString(m + 1, "\"" + currentRecord.getColumn(m) + "\"");
                        }
                        else
                        {
                            accStat.setInt(m + 1, Integer.parseInt(currentRecord.getColumn(m).toString()));
                        }
                    }
                    accStat.addBatch();
                }
                accStat.executeBatch();
                accStat.clearBatch();
                records.clear();
            }
            adb.connection.commit();
        }
        catch(Exception e){
            e.printStackTrace();
        }
        finally{

        }   
    }
}

完整代码:

import java.util.*;
import java.sql.*;
import com.sybase.jdbc2.jdbc.SybDriver;//This is an external file that is used to connect to the Sybase database. I will not include the full code here for the sake of space but will provide it upon request.

public class SybaseToAccess {
    public static void main(String[] args){
        String accessDBPath = "C:/Users/me/Desktop/Database21.accdb";//This is a placeholder, as I cannot give out the exact file path. However, I have confirmed that it points to the correct file on the system.
        String sybaseDBPath = "{sybServerName}:{sybServerPort}/{sybDatabase}";//See above comment
        try{
            AccessDatabase adb = new AccessDatabase(accessDBPath);
            SybaseDatabase sdb = new SybaseDatabase(sybaseDBPath, "user", "password");

            getRecords(sdb, adb);
        }
        catch(Exception e){
            e.printStackTrace();
        }
        finally{

        }       
    }
    public static void getRecords(SybaseDatabase sdb, AccessDatabase adb)
    {
        ArrayList<Record> records = new ArrayList<Record>();
        StringBuffer sql = new StringBuffer();
        Record currentRecord = null;
        try{
            Statement sybStat = sdb.connection.createStatement();
            PreparedStatement resetADB = adb.connection.prepareStatement("DELETE FROM Table");
            PreparedStatement accStat = adb.connection.prepareStatement("INSERT INTO Table (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
sql.append(query);//query is a placeholder, as I cannot give out the actual query to the database. I have confirmed that the query itself gives the ResultSet that I am looking for
            ResultSet rs = sybStat.executeQuery(sql.toString());
            resetADB.executeUpdate();
            boolean nextWatch = true;
            Integer i = 1;
            Record r = new Record();
            while(nextWatch)
            {
                for (int j = 0; j < 1000 && nextWatch; j++)
                {
                    nextWatch = rs.next();

                    r.setColumn(i, 0);
                    r.setColumn(rs.getString("B"), 1);
                    r.setColumn(rs.getString("C"), 2);
                    r.setColumn(rs.getString("D"), 3);
                    r.setColumn(rs.getString("E"), 4);
                    r.setColumn(rs.getString("F"), 5);
                    r.setColumn(rs.getString("G"), 6);
                    r.setColumn(rs.getString("H"), 7);
                    r.setColumn(rs.getString("I"), 8);
                    r.setColumn(rs.getString("J"), 9);
                    r.setColumn(rs.getString("K"), 10);
                    r.setColumn(rs.getInt("L"), 11);
                    r.setColumn(rs.getString("M"), 12);
                    r.setColumn(rs.getString("N"), 13);
                    r.setColumn(rs.getString("O"), 14);
                    r.setColumn(rs.getString("P"), 15);

                    records.add(r);
                    i++;
                }

                for(int k = 0; k < records.size(); k++)
                {
                    currentRecord = records.get(k);

                    for(int m = 0; m < currentRecord.getNumOfColumns(); m++)
                    {
                        if (currentRecord.getColumn(m) instanceof String)
                        {
                            accStat.setString(m + 1, "\"" + currentRecord.getColumn(m) + "\"");
                        }
                        else
                        {
                            accStat.setInt(m + 1, Integer.parseInt(currentRecord.getColumn(m).toString()));
                        }
                    }
                    accStat.addBatch();
                }
                accStat.executeBatch();
                accStat.clearBatch();
                records.clear();
            }
            adb.connection.commit();
        }
        catch(Exception e){
            e.printStackTrace();
        }
        finally{

        }   
    }
}

class AccessDatabase{
    public Connection connection = null;
    public AccessDatabase(String filePath)
            throws Exception
        {
            String dbString = null;
            dbString   = "jdbc:ucanaccess://" + filePath; 
            connection = DriverManager.getConnection(dbString);
            connection.setAutoCommit(false);
        }
}
class Record{
    ArrayList<Object> columns;
public
    Record(){
        columns = new ArrayList<Object>();
        columns.add("Placeholder1");
        columns.add("Placeholder2");
        columns.add("Placeholder3");
        columns.add("Placeholder4");
        columns.add("Placeholder5");
        columns.add("Placeholder6");
        columns.add("Placeholder7");
        columns.add("Placeholder8");
        columns.add("Placeholder9");
        columns.add("Placeholder10");
        columns.add("Placeholder11");
        columns.add("Placeholder12");
        columns.add("Placeholder13");
        columns.add("Placeholder14");
        columns.add("Placeholder15");
        columns.add("Placeholder16");
    }

    <T> void setColumn(T input, int colNum){
        columns.set(colNum, input);
    }

    Object getColumn(int colNum){
        return columns.get(colNum);
    }

    int getNumOfColumns()
    {
        return columns.size();
    }
}

class SybaseDatabase{
    public Connection connection;

    @SuppressWarnings("deprecation")
    public SybaseDatabase(String filePath, String Username, String Password)
        throws Exception
    {
        SybDriver driver;

        try 
        {
            driver = (SybDriver)Class.forName("com.sybase.jdbc2.jdbc.SybDriver").newInstance();
            driver.setVersion(SybDriver.VERSION_6);
            DriverManager.registerDriver(driver);
        } 
        catch (Exception e) 
        {
            e.printStackTrace(System.err);
        }   

        connection = DriverManager.getConnection("jdbc:sybase:Tds:" + filePath, Username, Password);
    }
}

【问题讨论】:

  • 最好的方法似乎不是同时处理 130 万行数据并且只处理一次事务。您不能通过在“少数”行中划分数据来处理这些记录(可能由 10000 或 100,000 行的数据包处理)?像这样,你不会有这么多的数据保存在内存中
  • @Prim 如果您在谈论提交,我最近刚刚尝试修改我的程序,使其每 1000 行提交一次,而不是一个大事务。有一个改进 - 大约 950000 行导入到 Access DB 中 - 但在那之后它仍然被炸毁了。
  • 您的主要问题更多是您的 ArrayList 仍然加载了 130 万行,尝试仅读取和加载 1000 行(可能通过更改您的 sql 请求来限制行数),处理这些行并提交,等等(不保留所有记录的引用)。
  • @Prim 周末,我继续尝试创建 1300 个不同的 ResultSet,每个 1000 行,在创建下一个 ResultSet 之前处理这些行并提交更改。我还在每 1000 行之后使用 clear 函数清除了 ArrayList 。再次,这取得了显着的改进 - 1150000 行,高于 950000 - 但后来我再次收到错误。我还能做些什么来减少正在使用的内存量吗?
  • 你能发布你当前代码的更新吗?

标签: java garbage-collection


【解决方案1】:

如果你想使用更少的内存,你应该同时处理更少的行,但重用所有你可以重用的对象(比如PreparedStatement

首先:您在记录中使用固定大小的ArrayList&lt;&gt;。您可以为此使用数组Record[]ArrayList 的原理是有一个动态大小的数组,这里不需要

第二:在处理之前不要从数据库中加载所有数据,加载一部分数据并处理它,然后继续。

您可以通过提取处理某些行的代码部分并通过limiting the number of returned rows 更改您的查询来做到这一点。

现在,您加载 1000 行(从索引 0 到 999),然后处理并提交它们。然后加载 1000 行(从索引 1000 到 1999),处理并提交它们。然后你继续。在每组行之间,不要对处理过的数据(如记录)保留任何引用,以免它们被保存在内存中(如必要时它们将被垃圾收集)。

如果你仍然没有足够的内存,我猜你保留了一些没有被垃圾收集的对象的引用,从而导致内存泄漏问题:你的程序在处理每个数据时需要越来越多的内存。您可以使用一些工具,如jvisualvm(在 java 中提供)来调查内存的使用情况

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-04
    • 1970-01-01
    • 2018-01-25
    • 1970-01-01
    相关资源
    最近更新 更多