个人处理java.lang.OutOfMemoryError: Java heap space的一次记录

一、问题背景

根据业务需求做了一个接口,主要功能是根据查询规则在数据库中查出数据,然后生成文件,校验文件,压缩文件,之后再把这三个文件上传到指定服务器。但由于数据量大,平时1kw+峰值则4kw+,虽然处理性能要求不高,但也不能跑太慢。按常规方法实现后跑1500w数据大概需要半个小时,还算符合业务需求。这两天数据量大,跑到2200w的时候出现了OOM。
在这里插入图片描述

二、问题处理

OOM的出现主要有两点,1.是内存真的不够了,执行的时候需要扩大堆内存。2.程序中出现了死循环有实例持有大量new对象得不到释放。针对这两点进行排查。

首先看下出问题的代码块

//生成文件
    public boolean loadData(EtlRule etlRule) throws IOException, SQLException {
    
    
        if (etlRule.getCreate_sql().trim().equals(""))
            logger.error("\"error: 配置中包括查询语句为空[create_sq;]\"");

        long FILE_UNIT_SIZE = 1024 * 1024 * 1024;
        String selSql;
        Connection destCon = null;//	源数据库连接对象
        Statement stmt = null;
        BufferedWriter output = null;

        long rowCount = 0L;
        int colCounts = 0;
        long flen = 0L;
        ResultSet res=null;
        int closeNum = 5000000;   //每五百万条记录关闭一次输入流,目的是清空堆内存。

        try {
    
    

            //初始化数据库连接
            EtlDBConnect etlDBConnect = new EtlDBConnect();
            destCon = etlDBConnect.initConfig2(destCon);

            if (etlRule.getCreate_sql().indexOf("${SRC_TABLE}") > 0) {
    
    
                selSql = etlRule.getCreate_sql().replace("${SRC_TABLE}", etlRule.table_name);
            }else {
    
    
                selSql = etlRule.getCreate_sql();
            }
            logger.info("数据查询:sql=[" + selSql + "]");

            //生成的文件个数
            int fileCount = 1;

            //向dataPath中添加路文件径数据
            String dataPathArr = etlRule.getLocal_path() + "/" + etlRule.getFile_name().replace("${NUM}",
                    "001");
            etlRule.getDataPath().add(dataPathArr);

            File file = new File(etlRule.getDataPath().get(fileCount-1));
            if (!file.exists()) {
    
    
                file.createNewFile();
                //System.out.println("ORA-000 数据文件创建失败: " + etlRule.getDataPath().get(0));
                //return false;
            }
            logger.info("[*输出数据文件*]:[" + etlRule.getDataPath().get(0) + "]");

            assert destCon != null;
            //stmt = destCon.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
            stmt = destCon.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            logger.info("[查询主表] sql==========" + selSql);

            res = stmt.executeQuery(selSql);
            //preFetchRowNum 为配置文件中的值
            res.setFetchSize(5000);    //设置预处理行数
            int fetchSize = res.getFetchSize();
            System.out.println("------默认fetchSize预处理行数:"+fetchSize);

            if (res == null || !res.next()) {
    
    

                logger.error("数据文件sql无数据!");
                return false;
            }
            res.previous();

            output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true),
                    StandardCharsets.UTF_8),1024);

            //StringBuilder line = new StringBuilder();
            String str = "";
            // 获取字段元信息
            ResultSetMetaData rsmd1 = res.getMetaData();
            colCounts = rsmd1.getColumnCount();
            //Date date = new Date();

            while (res.next()) {
    
    
                // 打印进度
                rowCount++;
                if (rowCount % printUnit == 0) {
    
    
                    //20w条数据的时候打印一条提示信息
                    //logger.info(rowCount + " ----rows proceed");
                    System.out.println(rowCount +" ----rows proceed");
                }

                //获取所有列对应的数据
                for (int i = 1; i <= colCounts; i++) {
    
    
                    //line.append(res.getString(i)).append("\n");
                    str = res.getString(i)+"\n";
                }

                //读取一条查询结果,写入一次
                output.write(str);
                output.flush();
                str = "";

                //如果文件长度大于2G,建立新文件,最多不会超过9个-------c中40亿的定值超int范围了
                if (file.length() >= (FILE_UNIT_SIZE * 2)) {
    
    
                    //如果存在${NUM},则生成新的文件,不存在就继续向原文件中写入
                    if (etlRule.getFile_name().contains("${NUM}")){
    
    
                        output.close();
                        fileCount++;
                        flen = file.length();

                        //添加记录数
                        etlRule.getDataCount().add(rowCount);
                        etlRule.getDataSize().add(flen);

                        String newFile = etlRule.getLocal_path() + "/" + etlRule.getFile_name().replace(
                                "${NUM}", "00" + fileCount);
                        etlRule.getDataPath().add(newFile);
                        file = new File(etlRule.getDataPath().get(etlRule.getDataPath().size() - 1));
                        if (!file.exists()) {
    
    
                            file.createNewFile();
                        }
                        output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false), StandardCharsets.UTF_8));
                    }
                }
                //logger.info("\t"+ String.valueOf(rowCount) +"\t\t\trows processed");
            }//while

            //数据写入字段
            flen = file.length();
            etlRule.getDataCount().add(rowCount);
            etlRule.getDataSize().add(flen);

        } catch (Exception | Error throwables) {
    
    
            System.out.println("捕获到错误:"+throwables.getMessage());
            System.out.println("规则运行标识复位。");
            ruleConfigReset(etlRule);
            //throwables.printStackTrace();
        }finally {
    
    
            if (output != null) {
    
    
                try {
    
    
                    //刷新缓存区
                    output.flush();
                    output.close();
                } catch (IOException e) {
    
    
                    logger.error("关闭输出流异常:{}", e);
                }
            }
            //关闭数据库连接
            assert res != null;
            res.close();
            stmt.close();
            destCon.commit();
            destCon.close();
        }

        return true;
    }

通过代码块的分析,把问题锁定在了while()循环中,因为只有这里可能出现死循环,或者持有new对象过多的情况。通过进一步查找锁定在了

output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true),
                    StandardCharsets.UTF_8),1024);

//读取一条查询结果,写入一次
                output.write(str);
                output.flush();

那么继续追踪BufferedWriter的write()方法

public void write(String str) throws IOException {
    
    
        write(str, 0, str.length());
    }

writre(String str)中又调用了重载的另一个write(String str, int off, int len)方法

public void write(String str, int off, int len) throws IOException {
    
    
        synchronized (lock) {
    
    
            char cbuf[];
            if (len <= writeBufferSize) {
    
    
                if (writeBuffer == null) {
    
    
                    writeBuffer = new char[writeBufferSize];
                }
                cbuf = writeBuffer;
            } else {
    
        // Don't permanently allocate very large buffers.
                cbuf = new char[len];
            }
            str.getChars(off, (off + len), cbuf, 0);
            write(cbuf, 0, len);
        }
    }

通过源码的查看,我们先找到几个变量的定义

    /**
     * The object used to synchronize operations on this stream.  For
     * efficiency, a character-stream object may use an object other than
     * itself to protect critical sections.  A subclass should therefore use
     * the object in this field rather than <tt>this</tt> or a synchronized
     * method.
     * 用于同步此流上的操作的对象。为了提高效率,字符流对象可以使用其他对象来保护临界区。
     * 因此,子类应该在这个字段中使用对象,而不是<tt>这个</tt>或同步方法。
     */
    protected Object lock;

/**
     * Temporary buffer used to hold writes of strings and single characters
     * 用于保存写字符串和单个字符的临时缓冲区
     */
    private char[] writeBuffer;

    /**
     * Size of writeBuffer, must be >= 1
     */
    private final int writeBufferSize = 1024;

下面进行write(String str, int off, int len) 方法的解读

1.synchronized (lock) 我就不班门弄斧了,因为我平时都没这么用过,只是知道一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。

2.当len长度小于writeBufferSize时writeBuffer = new char[writeBufferSize];
反之cbuf = new char[len];

3.追踪str.getChars(off, (off + len), cbuf, 0);

    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    
    
        if (srcBegin < 0) {
    
    
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
    
    
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
    
    
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

此方法的作用就是把一个数组中某一段字节数据放到另一个数组中。至于从第一个数组中取出几个数据,放到第二个数组中的什么位置都是可以通过这个方法的参数控制的。

4.追踪write(cbuf, 0, len);发现它是个抽象方法

    abstract public void write(char cbuf[], int off, int len) throws IOException;

由此可知,write(cbuf, 0, len)是给子类或接口实现特定功能用的,没有用到的话可以忽略。

三、分析

经过上面一波点点点,我们发现了,无论任何情况下只要调用BufferedWriter的write()方法就会new一个char[],这样的话如果数据量很大,就会造成OOM,原因是实例持有的对象不能得到及时的释放。

四、解决办法

1.增大堆内存。
2.优化代码。
第一种方法只能解一时之痛,因为数据量是不确定的,盲目增大堆内存不仅浪费资源,而且可能影响到其他程序运行。

linux下可以用此命令来查看jvm运行时的默认最大堆内存空间

 java -XX:+PrintFlagsFinal -version | grep MaxHeapSize

在这里插入图片描述
注意下 java HotSpot 是Server 还是Client 因为两种模式下堆内存的默认分配有些许不同。一般来说开发环境下都是Server 模式

//win下可以采用Runtime.getRuntime()下的方法来测试
public class jvm {
    
    
    public static void main(String[] args) {
    
    
        long maxMemory = Runtime.getRuntime().maxMemory();
        long totalMemory = Runtime.getRuntime().totalMemory();
        long freeMemory = Runtime.getRuntime().freeMemory();
        long usableMemory = maxMemory - totalMemory + freeMemory;
        System.out.println("可获得的最大内存是:"+maxMemory/(1024*1024));
        System.out.println("已经分配到的内存大小是:"+(totalMemory/(1024*1024)));
        System.out.println("所分配内存的剩余大小是:"+(freeMemory/(1024*1024)));
        System.out.println("最大可用内存是:"+usableMemory/(1024*1024));
    }
}
//如果懒得新建工程,可以直接新建个java文件,javac编译后直接运行class文件就行了。

第二种方法,优化代码,这个难度就根据具体情况来了。
根据我自己的情况,每当数据读取500w条的时候就重启一次BufferedWriter,目的是释放先前占用的堆内存。其他方法暂时没想到,修改后的代码已经提交给测试了,不知道大数据量下能不能跑通,如果我后续没更新,就应该是没问题了。

五、自说自话

这里我只提供了一个排查的思路,OOM问题还得根据自己代码的具体情况来分析。
这里提供另一篇OOM排查案例,供大家参考
记录一次java.lang.OutOfMemoryError: Java heap space的问题处理【转载】

猜你喜欢

转载自blog.csdn.net/zhuyin6553/article/details/108990035