There is under the covers going on, even with very simple languages like C.
Memory allocation comes in a range of forms. Every variable (or more abstractly program state) you use needs to be held somewhere. The compiler is responsible for some of these, utility libraries for more, and eventually the operating system.
Short term variables, including variables that only exit as intermediate values inside expressions are usually allocated to machine registers. This is the job of the compiler. It targets the exact machine architecture.
If your language supports subroutines/functions, the local variables are generally stored on the stack. Almost all architectures provide explicit support for a stack. But they didn’t always. So sometimes the compiler needed to create a stack from the raw capability of the machine.
A modern architecture has lots of registers, so the compiler has the freedom to use them for lots of stuff, with one huge performance boost coming from allocating most, if not all, local variables in a function to registers. Similarly parameters can be passed on the stack or in registers, with the compiler doing the leg work. Critically, recursive calls allocate a new area on the stack for each call, thus providing for automatic management of local variables. So much so that these variables were termed automatic in some parlances.
Your program likely has some need for state that is known a-priori that is used by the entire program. So the compiler sets up program execution in a manner where this memory is allocated before the program proper starts execution. This memory allocation does not change, so is static, and usually allocated at program startup from a region of memory designated for this purpose, and usually called something with static in the name.
This far you have enough to write and run lots of programs. The question then arises when you have needs where you don’t know ahead of time exactly what size or number of data items or program state you need. So dynamic memory allocation.
In its simplest form, the operating system provides a mechanism to delineate a region of memory that you can manage dynamically to use for program state. Pretty quickly you want a well known library that does the heavy lifting for you. If you are coding with C, your friends are malloc and free. malloc allocates a region of memory for you (given the size of the region you desire) and returns to you the address of the start of the region you can use. free takes that pointer, and melds the region of memory back into the free memory, ready to use again when needed. It is up to you to make sure you use the memory so allocated responsibly. Don’t accidentally access addresses past the end of the space you asked for. Don’t access stuff before it either. Make sure you call free when you are done, lest you run out of memory to malloc. Make absolutely sure you never access memory using the address you were given if you have called free with it. Never ever call free twice.
Keep to the rules and things work generally quite well. Break the rules, and no mercy is shown. Subtle and devilishly hard to find bugs may lurk, ready to bite you at the most unexpected point. Discipline and care, and you can write good quality robust code.
malloc generally interacts with the operating system, so that when more memory is needed, the OS can allocate more virtual address space for it to operate in, only at this point allocating additional system resources to your running program.
More modern languages, Java, Python, Javascript and so on provide dynamic memory management in a more transparent and safe manner. The underlying language runtime will keep track of allocations, and critically can keep track of what parts of data structures contain pointers. This is the key. By itself there is nothing about a region of memory to tell you what it contains. In C, you use appropriate type casts on the pointer values to make it look like any data structure you like. The underlying run-time has no idea what you are doing. Object oriented languages do know what objects hold. To do so they add additional state information for every allocation that helps it keep track, and alwyas able to know where pointers are. Languages like Java and Python also impose strict rules about pointers to objects. The language and runtime explicitly prohibit code that creates or modifies pointers. The only thing allowed to create a pointer is the object management run-time. Once the management runtime is able to keep track of all pointers, no matter where they may reside, it can reason about what objects are no longer accessible, and can reclaim the memory used. Hence garbage collection. Java and Python go about this in different ways (reference counting versus reachability) but the broad idea is the same. Managing object allocation and garbage collection in parallel systems is a whole new level of fun.
You can go further. Program state might reasonably want to persist between runs of you code. In most languages support for this is pretty desultory. Some form of serialisation that encodes the run-time data in a form that can be written to, and restored from persistent media. JSON for instance. Similar problems if you wish to communicate state between programs. In the large, reasoning about allocation of program state in either persistent or distributed systems is a vastly more complex problem, with a range of solutions of greater or lesser satisfaction. Reasoning about garbage collection becomes fabulously evil.
The manner in which such systems are commonly implemented is to create a virtual machine. This is a machine architecture that is explicitly designed to run the higher level abstractions of the programming language. Most importantly this is an architecture that itself implements the rules about pointers. For java, this is the JVM Java Virtual Machine. So now, every machine architecture that supports Java does so by running a JVM, and the JVM abstracts away everything about the hardware. There are of course efficiency imposts. But with modern hardware, it isn’t as if that matters much.