Description:
Connection.prepareStatement uses the ServerPreparedStatement constructor, which registers the preparedStatement in the connections openStatements map. If the serverPrepare() call fails (in my case this happens because I use a LIMIT ? in my sql) then the prepared statement is not unregistered from the connection. This causes a memory leak, as the openStatements map grows continuously.
This behaviour is hard to find, because the connection.prepareStatement() method catches the exception and emulates the unsupported prepared statement with a clientPrepareStatement call.
How to repeat:
Use a prepared statement with a LIMIT ? clause. prepare this statement multiple times against the same connection. The connection's openStatements map will grow and the ServerPreparedStatement instances will be kept on the heap.
Suggested fix:
Proper error handling in ServerPreparedStatement constructor - if it throws an exception, it should leave things the way it found them. 
I have implemented this, and it fixes the problem for me.
In the Statement constructor, move:
        if (!this.connection.getDontTrackOpenResources()) {
        	this.connection.registerStatement(this);
        }
to the end of the method - so if anything in the constructor fails, the statement is not registered with the connection.
the ServerPreparedStatement reads:
    public ServerPreparedStatement(Connection conn, String sql, String catalog)
        throws SQLException {
        super(conn, catalog);
        try
        {
            checkNullOrEmptyQuery(sql);
            this.isSelectQuery = StringUtils.startsWithIgnoreCaseAndWs(sql, "SELECT"); //$NON-NLS-1$
            this.useTrueBoolean = this.connection.versionMeetsMinimum(3,
                    21, 23);
            this.hasLimitClause = (StringUtils.indexOfIgnoreCase(sql, "LIMIT") != -1); //$NON-NLS-1$
            this.firstCharOfStmt = StringUtils.firstNonWsCharUc(sql);
            this.originalSql = sql;
            if (this.connection.versionMeetsMinimum(4, 1, 2)) {
                this.stringTypeCode = MysqlDefs.FIELD_TYPE_VAR_STRING;
            } else {
                this.stringTypeCode = MysqlDefs.FIELD_TYPE_STRING;
            }
            serverPrepare(sql);
        }
        catch(Exception ex)
        {
            try
            {
                realClose(false);
            }
            catch(SQLException e)
            {
                conn.getLog().logWarn("Prepare statement failed and then couldn't clean up server prepared statement",e);
            }
            if(ex instanceof SQLException)
                throw (SQLException)ex;
            throw new SQLException(ex.toString(),
                    SQLError.SQL_STATE_GENERAL_ERROR);
        }
    }
  
 
 
Description: Connection.prepareStatement uses the ServerPreparedStatement constructor, which registers the preparedStatement in the connections openStatements map. If the serverPrepare() call fails (in my case this happens because I use a LIMIT ? in my sql) then the prepared statement is not unregistered from the connection. This causes a memory leak, as the openStatements map grows continuously. This behaviour is hard to find, because the connection.prepareStatement() method catches the exception and emulates the unsupported prepared statement with a clientPrepareStatement call. How to repeat: Use a prepared statement with a LIMIT ? clause. prepare this statement multiple times against the same connection. The connection's openStatements map will grow and the ServerPreparedStatement instances will be kept on the heap. Suggested fix: Proper error handling in ServerPreparedStatement constructor - if it throws an exception, it should leave things the way it found them. I have implemented this, and it fixes the problem for me. In the Statement constructor, move: if (!this.connection.getDontTrackOpenResources()) { this.connection.registerStatement(this); } to the end of the method - so if anything in the constructor fails, the statement is not registered with the connection. the ServerPreparedStatement reads: public ServerPreparedStatement(Connection conn, String sql, String catalog) throws SQLException { super(conn, catalog); try { checkNullOrEmptyQuery(sql); this.isSelectQuery = StringUtils.startsWithIgnoreCaseAndWs(sql, "SELECT"); //$NON-NLS-1$ this.useTrueBoolean = this.connection.versionMeetsMinimum(3, 21, 23); this.hasLimitClause = (StringUtils.indexOfIgnoreCase(sql, "LIMIT") != -1); //$NON-NLS-1$ this.firstCharOfStmt = StringUtils.firstNonWsCharUc(sql); this.originalSql = sql; if (this.connection.versionMeetsMinimum(4, 1, 2)) { this.stringTypeCode = MysqlDefs.FIELD_TYPE_VAR_STRING; } else { this.stringTypeCode = MysqlDefs.FIELD_TYPE_STRING; } serverPrepare(sql); } catch(Exception ex) { try { realClose(false); } catch(SQLException e) { conn.getLog().logWarn("Prepare statement failed and then couldn't clean up server prepared statement",e); } if(ex instanceof SQLException) throw (SQLException)ex; throw new SQLException(ex.toString(), SQLError.SQL_STATE_GENERAL_ERROR); } }