Garbage collection depends on the various factor like which collector you're using, machine's physical memory as well as the JVM version you are using. Since you're not mentioned so much here about them, bit hard to predict which could be the cause for this. I assume you're using Java 8 since that's the more popular version nowadays.
Since Java 8, There's a change in JVM memory model. Which is, now there is no Permanent Generation space. This's the space where String Pool located (I'm going with String hence you're using a String concatenation in loops). Refer this Java (JVM) Memory Model – Memory Management in Java document. Permanent Generation is where the class/static references live since you declare them as well.
Instead of Permanent Generation, Since Java 8, there's a new memory location called Metaspace which lives in the Main memory outside of JVM.
Also, when you concatenate String objects like this, it won't modify the existing object as String is immutable type. Instead, it creates new String objects with the new value and put them into the Metaspace. This might be the reason you're seeing a memory usage increment.
Even though Metaspace is located inside the main memory/physical memory and it can dynamically expand, still has the physical memory limitation. That's why I told earlier machine's physical memory as a dependency factor.
When we come to garbage collection, you haven't mention any GC config. So I assume you are using Parallel GC which is the default collector of Java 8 (You can find more about the GCs from same link provided above). I guess the Parallel GC's performance is adequate for this task. Therefore invoking System.gc() whould be enough without any JVM flag.
But, as you mentioned that System.gc() doesn't clean up the memory could occurs hence you're using a separate thread to concatenate these Strings.
Usually, Strings that created using String literal (String s = "abc") would not become garbage eligible. This because there is a implicit reference to the String object in the code of every method that uses the literal (Check this answer When will a string be garbage collected in java). Thus, you have to loose those implicit references by ending the execution of the function.
Since you're using a new Thread to do this concatenation and I can't find any place where you interrupt the thread and you're invoking the Thread.yield()(Thread.yield) to inform the thread scheduler to seize the usage of the CPU for this particular thread and mark the thread is willing to be scheduled as soon as possible again, make pretty much clear this Thread object still lives and refers those String objects not making them garbage eligible. This maybe the reason System.gc() invocation is not working.
As a solution, try to interrupt the thread instead of yield.
Update 1:
Before Java 7, String Pool was located in PermGen, which is not eligible for garbage collection. PermGen has a fixed size and not capable to expand at runtime. If PermGen has not enough space, it gives java.lang.OutOfMemoryError: PermGen error. As a temporary remediation we can increase the PermGen size using -XX:MaxPermSize=512m flag.
But remember this is only works on JVMs before Java 8 and in Java 7, this doesn't make any different in the sense of increasing String Pool size availability hence Java 7 onwards, String Pool has moved to Heap space.