How can we create a separate and isolated address space for each process without changing mapping between physical memory and virtual memory?
This is exactly what virtual memory allows, and what the kernel does: each process gets its own, separate, virtual memory mapping. This is what allows page 1000 in one process to point to physical page X in memory, while page 1000 in another process points to physical page Y in memory, and page 1000 in yet another process is swapped out. Every time the context switches from one process to another, the virtual memory mapping is changed to match the new process.
Highmem is needed because the kernel keeps its own mapping of physical memory in its virtual address space (on 32-bit x86, the top gigabyte of the address space in the typical 3G/1G split). The kernel needs to be able to access any page of physical memory, regardless of whether it’s mapped in the virtual memory of the current process; this is why the direct physical memory mapping is useful. On 32-bit systems, this direct mapping is limited in size compared to potential physical memory sizes: at most approximately 896MiB on 32-bit x86. To access physical memory outside this area, the kernel needs to map pages on the fly. On 64-bit systems, there’s enough room in the kernel side of the address space for a direct mapping of up to 64TiB of physical memory, which is enough for most systems for a little while yet (and on systems where it isn’t, five-level pages tables allow mapping up to 32PiB of physical memory).
See also this recent-ish LWN article which includes a high memory refresher.