Non-primitive Types
In short: All data types that aren’t one of the 8 primitive types — i.e. objects, e.g. String, arrays, custom classes, and all wrapper classes.
In more detail: The central difference from primitive types: a non-primitive variable doesn’t store the value itself, but a reference to an object on the heap — which is also why it can be null (no object present), which isn’t possible for primitive types like int.
In Depth
String text = null; // valid! null means "no reference to an object"
// int number = null; // would NOT compile - primitive types can never be null
String a = "Hello";
String b = a; // b points to the SAME object as a, no copy!
a = "New"; // a now points to a different object, b stays "Hello"
int[] numbers = {1, 2, 3};
int[] secondReference = numbers;
secondReference[0] = 99; // changes the same array object!
System.out.println(numbers[0]); // 99 - "numbers" sees the change tooThe last code block shows the most important practical difference from primitive types: because non-primitive variables only store a REFERENCE to an object (not the object itself), two variables with the same reference also share the same underlying object — a change via one variable is also visible via the other. For String, this rarely shows up in practice, because String objects are immutable: every apparent “change” (a = "New") actually creates a completely new object and only makes a point to it, instead of changing the original "Hello" object. For mutable objects like arrays or custom classes, by contrast, this “shared reference” behaviour is a common, surprising bug when you accidentally assume you got a copy.
Comparison with == vs. .equals()
Because non-primitive variables store references, == by default compares whether two variables point to the same object (object identity), not whether the content is equal — a common beginner mistake is if (a == b) for two objects that are equal in content but created separately, which returns false even though the values look identical. For content comparison, .equals() is needed (for custom classes, this method itself has to be meaningfully overridden, otherwise it behaves like ==).
Garbage collection and non-primitive objects
Since non-primitive values live on the heap, Java automatically takes care of cleaning them up via the garbage collector, once no reference to them exists anymore — unlike primitive values, which simply disappear along with the stack frame of the method they were declared in. An object that no variable points to anymore isn’t deleted immediately, but is collected at some later point by the garbage collector, once it runs.
See also: Strings, Wrapper Classes, double, byte, Garbage Collector