When you declare a variable, you are declaring that you would like to hold a "reference" (memory address) of an object in the "heap". The variable only holds a memory address that references the heap, not the object itself. You can declare multiple variables that refer to the same area of the heap.
ItemClass a = new ItemClass();
ItemClass b = a;
Note that b and a are the same object on the heap. You only get a new object in the heap when you use the new operator, or for certain factory functions and string concatenation. Most primitive types are immutable (cannot be changed), so most methods that work on primitives return a new object on the heap. Custom objects, however, are volatile by default, meaning they can be changed by code.
When you created the list of items, you actually just added the same reference to the list three times, rather than creating three distinct objects in the heap. It's not the objects that are duplicated, but rather the references to the same object that has been duplicated.
You can see this in the debug logs; adding the value to the list is adding the same memory address repeatedly:
10:58:28.1 (9577707)|STATEMENT_EXECUTE|[9]
10:58:28.1 (9587110)|HEAP_ALLOCATE|[9]|Bytes:6
10:58:28.1 (9616559)|VARIABLE_ASSIGNMENT|[9]|this.ItemId|"Item A"|0x61e029cf
10:58:28.1 (9629477)|STATEMENT_EXECUTE|[10]
10:58:28.1 (9817670)|SYSTEM_METHOD_ENTRY|[10]|List<ItemClass>.add(Object)
10:58:28.1 (9929730)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:4
10:58:28.1 (9974642)|SYSTEM_METHOD_EXIT|[10]|List<ItemClass>.add(Object)
10:58:28.1 (10013977)|STATEMENT_EXECUTE|[12]
10:58:28.1 (10026303)|HEAP_ALLOCATE|[12]|Bytes:6
10:58:28.1 (10062476)|VARIABLE_ASSIGNMENT|[12]|this.ItemId|"Item B"|0x61e029cf
10:58:28.1 (10073835)|STATEMENT_EXECUTE|[13]
10:58:28.1 (10142552)|SYSTEM_METHOD_ENTRY|[13]|List<ItemClass>.add(Object)
10:58:28.1 (10162068)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:4
10:58:28.1 (10177472)|SYSTEM_METHOD_EXIT|[13]|List<ItemClass>.add(Object)
10:58:28.1 (10185858)|STATEMENT_EXECUTE|[15]
10:58:28.1 (10191640)|HEAP_ALLOCATE|[15]|Bytes:6
10:58:28.1 (10208963)|VARIABLE_ASSIGNMENT|[15]|this.ItemId|"Item C"|0x61e029cf
10:58:28.1 (10221902)|STATEMENT_EXECUTE|[16]
10:58:28.1 (10254502)|SYSTEM_METHOD_ENTRY|[16]|List<ItemClass>.add(Object)
10:58:28.1 (10271297)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:4
10:58:28.1 (10282736)|SYSTEM_METHOD_EXIT|[16]|List<ItemClass>.add(Object)
10:58:28.1 (10291801)|STATEMENT_EXECUTE|[18]
In my example, 0x61e029cf is the object in the heap. You can see how you just added the same value each time.
In order to fix this, you need to actually use new every time.
List<ItemClass> lst = new List<ItemClass>();
ItemClass ord = new ItemClass();
ord.ItemId = 'Item A';
lst.add(ord);
ord = new ItemClass();
ord.ItemId = 'Item B';
lst.add(ord);
ord = new ItemClass();
ord.ItemId = 'Item C';
lst.add(ord);
System.debug(JSON.serialize(lst));