记录一次Java递归调用导致java.lang.StackOverflowError错误

简介: 通过递归调用每次查询500条数据的id,再通过id删除记录的方式,数据量一大,导致递归过深,栈帧数超出虚拟栈深度,虚拟机栈过多,报java.lang.StackOverflowError错误。

1 背景

系统有很多的日志表,需要定期进行数据清理,根据配置保留N天。当前已经运行几年,日志数据量很大,如直接调用delete from 表名 where create_time < 清理截止时间,会导致数据库超大事务操作,影响数据库当前的正常运行。

采用递归的方式每次查询500条id集合后调用delete from where id in (500个id),直至查询结果为空或小于500条。同时查询不在serveice层进行,将一个大事务拆分成多个小事务进行。

1.1 主体方法

其中clearHistoryQueryPO中内置属性limit = 500。


   private void clearTableHistory(ClearHistoryQueryPO clearHistoryQueryPO) {

       Set<Long> ids = databaseClearService.listIds(clearHistoryQueryPO);

       if(CollectionUtils.isEmpty(ids)) {

           return;

       }

       databaseClearService.deleteByIds(new ClearHistoryDeletePO(clearHistoryQueryPO,ids));

       LOGGER.debug("清理{}表日志,数量{}条", clearHistoryQueryPO.getTableName(), ids.size());

       //如果结果集数量小于limit,说明已经没有匹配的数据,结束递归

       if (ids.size() < clearHistoryQueryPO.getLimit()) {

           return;

       }

       //递归查询

       clearTableHistory(clearHistoryQueryPO);

   }


1.2 listIds对应mapper配置

   <!-- 根据时间查询limit条id集合-->

   <select id="listIds" parameterType="com.xtt.hd.dao.po.ClearHistoryQueryPO" resultType="java.lang.Long">

       <![CDATA[

       select ${primaryKey} from ${tableName}

       where create_time <= #{clearMaxTime}

       limit #{limit}

       ]]>

   </select>


1.3 deleteByIds对应mapper配置

   <!-- 根据id集合删除数据-->

   <delete id="deleteByIds" parameterType="com.xtt.hd.dao.po.ClearHistoryDeletePO">

       delete from ${tableName}

       where ${primaryKey} in

       <foreach collection="ids" open="(" close=")" item="item" separator=",">

           #{item}

       </foreach>

   </delete>

2 错误

提示堆栈溢出。

image.png

3 分析

通过查阅资料,递归过深,栈帧数超出虚拟栈深度,虚拟机栈过多会引发java.lang.OutOfMemoryError异常。

本程序每天夜间定时执行,相当于每天清理一天的日志信息,不会太大。但是系统已运行几年,当首次执行时候,有些表的清理数据已经达到百万级别,那么就会导致递归太深。

4 修改

将递归调用改成循环调用。


   private void clearTableHistory(ClearHistoryQueryPO clearHistoryQueryPO) {

       Set<Long> ids = null;

       while ((ids = databaseClearService.listIds(clearHistoryQueryPO)) !=null) {

           if(ids.size() == 0) {

               break;

           }

           databaseClearService.deleteByIds(new ClearHistoryDeletePO(clearHistoryQueryPO,ids));

           LOGGER.debug("清理{}表日志,数量{}条", clearHistoryQueryPO.getTableName(), ids.size());

           //如果结果集数量小于limit,说明已经没有匹配的数据,结束递归

           if (ids.size() < clearHistoryQueryPO.getLimit()) {

               break;

           }

       }

   }


5 总结

在无法控制递归深度的前提下,慎用递归,用循环替代。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
Java Exception异常信息怎么打印、记录,几种方式自己选
Java Exception异常信息怎么打印、记录,几种方式自己选
465 0
Java Exception异常信息怎么打印、记录,几种方式自己选
|
Java
java: 错误: 无效的源发行版:15
java: 错误: 无效的源发行版:15
584 0
java: 错误: 无效的源发行版:15
|
10月前
|
机器学习/深度学习 Java
Java方法的嵌套与递归调用
Java方法的嵌套与递归调用
164 0
|
7月前
|
Java Spring 容器
解决java.lang.StackOverflowError at java.base/java.lang.Double.doubleToRawLongBits(Native Method)问题~
解决java.lang.StackOverflowError at java.base/java.lang.Double.doubleToRawLongBits(Native Method)问题~
|
11月前
|
Java 编译器
[Java基础]基本概念(下)运算符,表达式和语句,分支,循环,方法,变量的作用域,递归调用
在上一篇文章[Java基础]基本概念(上)(标识符,关键字,基本数据类型)_小王师傅66的博客-CSDN博客中,我们学习了Java基础基本概念中的标识符,关键字,基本数据类型。这篇文章,我们将学习:运算符,表达式和语句,分支,循环,方法,变量的作用域,递归调用。
|
算法 Java
Java数值计算易出现的10种错误
Java的数值计算容易出现很多错误,本文介绍Java数值计算易出现的10种错误。
476 0
|
Java
命令行下运行JAVA出错:错误的签名:
命令行下运行JAVA出错:错误的签名:
66 0
|
Java 开发工具 C++
Java调用虹软SDK的错误
Java调用虹软SDK的错误
494 0
Java程序员最容易犯的十大SQL错误,你犯过几次?
前言 Java程序员编程时需要混合面向对象思维和一般命令式编程的方法,能否完美地将两者结合起来完全得依靠编程人员的水准: 技能:任何人都能容易学会命令式编程 模式:有些人用“模式-模式”,举个例子,模式可以应用到任何地方,而且都可以归为某一类模式 心境:首先,要写个好的面向对象程序是比命令式程序难得多,你得花费一些功夫 但当Java程序员写SQL语句时,一切都不一样了。SQL是说明性语言而非面向对象或是命令式编程语言。在SQL中要写个查询语句是很简单的。但在Java里类似的语句却不容易,因为程序员不仅要反复考虑编程范式,而且也要考虑算法的问题。 下面是Java程序员在写SQL时常犯的10个错误