Olá Herbert,
Isso mesmo, no total ficariam 3 objetos, todas Strings no pool de Strings da JVM. Veja só.
1º Objeto => Literal "a" (criado e incluído no pool)
2º Objeto => Literal "b" (criado e incluído no pool)
3º Objeto => concatenação de "a" com "b" formando "ab" (criado e incluído no pool)
Quando chega na linha seguinte, para imprimir o resultado da comparação da variável ab com a literal "ab", nenhum objeto é criado, pois o Java entende que a literal "ab" já existe no pool de Strings, devido a linha anterior, e acaba devolvendo o mesmo objeto.
Por isso, inclusive, que na saída do console imprime "true", pois trata-se do mesmo objeto em ambos lados do operador "=="
EDIT:
Curiosamente, experimente mudar o código para o seguinte:
String ab = new String("a" + "b");
System.out.println(ab == "ab"); // 0 <br />
Veja que o resultado vai dar false no fim, e no total teremos 4 objetos, sendo 3 no pool de strings (as literais "a" e "b" e o resultado da concatenação de "a" com "b") e um novo (com conteúdo "ab") que se comportará como qualquer outro objeto criado no Heap.