A typical backend for malloc() will be a Buddy or maybe Slab allocator.
There are several competing goals, and some memory is likely to be "wasted", in the sense that requests will be denied even though free fragments could collectively satisfy the request. As long as the wasted fraction remains "small", and {allocations, frees} happen "fast", we have a good allocator. Once having handed out an address to an application, usually we cannot move that memory prior to the app free()ing it. This is in distinction to a compacting GC.
There may be additional debug / verification flags to enable, which can help track down use-after-free, double-free, and similar valgrind type issues.
Once having allocated a chunk of memory, you might choose to have an Arena (or Bump) suballocator hand out small pieces of it. This works well for a Unit of Work, such as processing a web GET request. We might allocate for header, body part 1, part 2, send resulting HTML document to client, and then deallocate all of that all at once since we're finished with the page.