The compiler converts that phrase ("x = "+x) into a StringBuilder internally and uses .append(int) to "add" the integer to the string.
To go beyond the practical "how does Java do this", I'll take the advice of Stephen and give the theoretical as well. Conceptually, each value in the concatenation is first converted to a String and then concatenated. Nulls are concatenated as the word "null".
From the Java Language Specification:
15.18.1.1 String Conversion
Any type may be converted to type String by string conversion. A value
x of primitive type T is first converted to a reference value as if by
giving it as an argument to an appropriate class instance creation
expression:
If T is boolean, then use new Boolean(x). If T is char, then use new
Character(x). If T is byte, short, or int, then use new Integer(x). If
T is long, then use new Long(x). If T is float, then use new Float(x).
If T is double, then use new Double(x). This reference value is then
converted to type String by string conversion. Now only reference
values need to be considered. If the reference is null, it is
converted to the string "null" (four ASCII characters n, u, l, l).
Otherwise, the conversion is performed as if by an invocation of the
toString method of the referenced object with no arguments; but if the
result of invoking the toString method is null, then the string "null"
is used instead.
The toString method is defined by the primordial class Object; many
classes override it, notably Boolean, Character, Integer, Long, Float,
Double, and String.
15.18.1.2 Optimization of String Concatenation
An implementation may choose to perform conversion and concatenation
in one step to avoid creating and then discarding an intermediate
String object. To increase the performance of repeated string
concatenation, a Java compiler may use the StringBuffer class or a
similar technique to reduce the number of intermediate String objects
that are created by evaluation of an expression. For primitive types,
an implementation may also optimize away the creation of a wrapper
object by converting directly from a primitive type to a string.
The optimized version will not actually do a full wrapped String conversion first.
This is a good illustration of an optimized version used by the compiler, albeit without the conversion of a primitive, where you can see the compiler changing things into a StringBuilder in the background:
http://caprazzi.net/posts/java-bytecode-string-concatenation-and-stringbuilder/
This java code:
public static void main(String[] args) {
String cip = "cip";
String ciop = "ciop";
String plus = cip + ciop;
String build = new StringBuilder(cip).append(ciop).toString();
}
Generates this - see how the two concatenation styles lead to the very same bytecode:
L0
LINENUMBER 23 L0
LDC "cip"
ASTORE 1
L1
LINENUMBER 24 L1
LDC "ciop"
ASTORE 2
// cip + ciop
L2
LINENUMBER 25 L2
NEW java/lang/StringBuilder
DUP
ALOAD 1
INVOKESTATIC java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String;
INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
ASTORE 3
// new StringBuilder(cip).append(ciop).toString()
L3
LINENUMBER 26 L3
NEW java/lang/StringBuilder
DUP
ALOAD 1
INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
ASTORE 4
L4
LINENUMBER 27 L4
RETURN
The compiler has transformed "cip+ciop" into "new StringBuilder(cip).append(ciop).toString()". In other words, "+" is effectively a shorthand for the more verbose StringBuilder idiom.