I'm stuck with some weird (it seems) memory leak. I'm using MyBatis 3.4.5 with Spring Boot 1.58, Java 8 and SQL Server 2014.
I've reduced my case to the following:
I'm reading in a loop, a single large table (say CLIENTE ~ 900K rows).
I read in paged mode (20K) using a xml mapper (details below).
For each record, I sometimes insert a record in a second table (say TESTIGO) (mapper below).
There's practically no relation between the records read from CLIENTE and the inserted in TESTIGO.
All quite simple, and it works ok. Except for this:
If a run the full loop inside a Transaction, then it works ok until I stop saving -
at that momentthen the memory consumption starts growing quickly. (no, I'm not storing anything in memory)
I all goes to normality when I save some record, but if I don't, I eventually reach an OutOfMemory exception.
I've tried with and without the spring setting
mybatis.executor-type=REUSE
with no difference.
It's like MyBatis (or Spring?) does not like that I keep doing SELECTs inside an open transaction
if I don't do now and them some UPDATE/INSERT ? Why?
For example, here I (artificially) decided to not save anything for pages 5 to 11
I copy some result below. See how memory increases after page 5
From some heap dumps I'm seeing, it seems that some org.apache.ibatis.cache.impl.PerpetualCache is
eating the memory. I can provide more details if needed.
Regards
Hernan
======================================
Output:
with transaction, No REUSE
page [pag=0, n=20000] mem=41MB saving=true secs=7 tx=true
page [pag=1, n=20000] mem=41MB saving=true secs=13 tx=true
page [pag=2, n=20000] mem=41MB saving=true secs=19 tx=true
page [pag=3, n=20000] mem=41MB saving=true secs=25 tx=true
page [pag=4, n=20000] mem=41MB saving=false secs=28 tx=true
page [pag=5, n=20000] mem=74MB saving=false secs=30 tx=true
page [pag=6, n=20000] mem=107MB saving=false secs=32 tx=true
page [pag=7, n=20000] mem=140MB saving=false secs=35 tx=true
page [pag=8, n=20000] mem=173MB saving=false secs=38 tx=true
page [pag=9, n=20000] mem=206MB saving=false secs=40 tx=true
page [pag=10, n=20000] mem=239MB saving=false secs=45 tx=true
page [pag=11, n=20000] mem=272MB saving=false secs=48 tx=true
page [pag=12, n=20000] mem=41MB saving=true secs=54 tx=true
within transactions - REUSE
page [pag=0, n=20000] mem=41MB saving=true secs=5 tx=true
page [pag=1, n=20000] mem=41MB saving=true secs=10 tx=true
page [pag=2, n=20000] mem=41MB saving=true secs=14 tx=true
page [pag=3, n=20000] mem=41MB saving=true secs=18 tx=true
page [pag=4, n=20000] mem=41MB saving=false secs=21 tx=true
page [pag=5, n=20000] mem=74MB saving=false secs=23 tx=true
page [pag=6, n=20000] mem=107MB saving=false secs=25 tx=true
page [pag=7, n=20000] mem=140MB saving=false secs=28 tx=true
page [pag=8, n=20000] mem=173MB saving=false secs=31 tx=true
page [pag=9, n=20000] mem=206MB saving=false secs=33 tx=true
page [pag=10, n=20000] mem=239MB saving=false secs=38 tx=true
page [pag=11, n=20000] mem=272MB saving=false secs=41 tx=true
page [pag=12, n=20000] mem=41MB saving=true secs=45 tx=true
NO transaction - REUSE
page [pag=0, n=20000] mem=41MB saving=true secs=10 tx=false
page [pag=1, n=20000] mem=41MB saving=true secs=18 tx=false
page [pag=2, n=20000] mem=41MB saving=true secs=27 tx=false
page [pag=3, n=20000] mem=41MB saving=true secs=35 tx=false
page [pag=4, n=20000] mem=41MB saving=false secs=37 tx=false
page [pag=5, n=20000] mem=41MB saving=false secs=40 tx=false
page [pag=6, n=20000] mem=41MB saving=false secs=42 tx=false
page [pag=7, n=20000] mem=41MB saving=false secs=45 tx=false
page [pag=8, n=20000] mem=41MB saving=false secs=47 tx=false
page [pag=9, n=20000] mem=41MB saving=false secs=49 tx=false
page [pag=10, n=20000] mem=41MB saving=false secs=52 tx=false
page [pag=11, n=20000] mem=41MB saving=false secs=54 tx=false
page [pag=12, n=20000] mem=41MB saving=true secs=63 tx=false
Mappers
<select id="selectByPage" parameterType="
myapp.db.Page" resultMap="BaseResultMap">
<![CDATA[
SELECT * FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY TipoDocumento,NumeroDocumento )
AS RowNum, * FROM CLIENTES) AS RowConstrainedResult
WHERE RowNum >= #{rowNumFrom,jdbcType=INTEGER} AND RowNum <= #{rowNumTo,jdbcType=INTEGER}
ORDER BY RowNum
]]>
</select>
<update id="truncate" parameterType="java.lang.String">
TRUNCATE TABLE TESTIGO
</update>
<insert id="insert" parameterType="myapp.db.Testigo">
insert into TESTIGO (DOC) values (#{doc,jdbcType=VARCHAR})
</insert>
------------
Java code:
public void rebuildTestigos(boolean inTransaction) {
final int PAGESIZE = 20000;
Page page = new Page(PAGESIZE);
long t0 = System.currentTimeMillis();
TransactionStatus tx = inTransaction ? dbservice.startTransaction() : null;
try {
dbservice.getTestigoMapper().truncate("");
do {
boolean save = page.getPageNum() < 4 || page.getPageNum() > 11;
List<WhClientes> clis = dbservice.getWhClientsMapper().selectByPage(page);
for( WhClientes c : clis )
if(save) saveRecord(c.docAsString());
LOG.info("page {} mem={}MB saving={} secs={} tx={}", page, getUsedMemory(true) / (1024 * 1024),
save, (System.currentTimeMillis() - t0) / 1000, inTransaction);
if( clis.size() < PAGESIZE ) break;
page = page.nextPage();
} while(true);
} catch(Exception e) {
if( inTransaction ) dbservice.rollback(tx);
e.printStackTrace();
}
if( inTransaction ) dbservice.commit(tx);
}
private void saveRecord( String id) {
Testigo t = new Testigo();
t.setDoc(id);
dbservice.getTestigoMapper().insert(t);
}
/** quick global estimate */
public static long getUsedMemory(boolean gc) {
if( gc ) System.gc();
return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
}