I'd like to understand - how is this approach better / more performant than simply requesting memory from the OS ("malloc()"?) each time the VM needs it?
As has been pointed out in comments and other answers, most implementations of
malloc(3) don't call the system each time. (That's why the manual lists it as a library function in section 3 rather than a system call in section 2.) A naive implementation of
malloc() could simply adjust the break value, each time someone calls it. With
brk(2) being a system call and requiring a switch from user to kernel and another switch back, that gets really expensive really quickly. It also doesn't cover how to deal with
Back when CPU cycles weren't as cheap or plentiful as they are now, the extra expense had a pronounced effect on program performance. Someone figured out that it was much cheaper to adjust the break by a larger amount once and dice up the memory into bits by twiddling the pointers in a doubly-linked list. Since everything in software is a trade-off, doing that gets speed at the expense of space. That approach is essentially how modern
malloc() works, although there are some different things done in special situations.
Most VMs are, at heart, programs that emulate bare metal. Like a real processor, they have to provide memory space and don't know ahead of time where the programs they run will want to write. Without developing a complex virtual memory scheme (i.e., writing an OS within an OS), all they can do is allocate a large swath of memory at startup and, as OJVM does, grow it if needed.
If that sounds wasteful, on some OSes, Linux being one, it isn't. From the
malloc(3) manual page on Linux:
By default, Linux follows an optimistic memory allocation strategy.
This means that when malloc() returns non-NULL there is no guarantee
that the memory really is available. In case it turns out that the
system is out of memory, one or more processes will be killed by the
OOM killer. For more information, see the description of
/proc/sys/vm/overcommit_memory and /proc/sys/vm/oom_adj in proc(5), and
the Linux kernel source file Documentation/vm/overcommit-accounting.
As a more specific question - is code B more performant than code A? Why?
Now that you know how
malloc() works, the answer is that they're equally-performant because neither will call out to the system very often. Your "B" implementation is doing the same thing
malloc() does internally and is why it's best to use the system-provided functions: somebody's already thought of this stuff and written an implementation that's been refined over many years.
One other crucial differnce is that the "A" implementation will continue returning memory as long as the system has it available and "B" will run out after you've used up the megabyte you allocated.