External Fragmentation in OS
Enough free memory in total, and the request still fails. The space exists, but it is scattered in pieces too short to serve anything.
What External Fragmentation in OS Means
External fragmentation in OS is free memory that exists between allocated blocks in runs too short to satisfy a request. The total is sufficient; no single piece is.
It builds up naturally, whichever allocation strategy is used. Processes of different sizes are allocated and freed over time, each departure leaving a gap. Adjacent gaps merge, but gaps separated by a live block cannot, so the free memory ends up split across the region in short runs.
This is the harder of the two kinds to reason about. The waste is not sealed inside anyone's allocation and could in principle be used — it simply is not contiguous, and a request for contiguous memory cannot be served from scattered pieces.
- Cause: variable-sized allocation and freeing over time
- Location: between allocated blocks
- Symptom: a request fails while total free memory exceeds it
External Fragmentation Example
A 64 KB region currently holds three live blocks and three holes: A (12K), hole 12K, E (5K), hole 24K, H (20K), hole 3K.
Total free memory is 12 + 24 + 3 = 39 KB. A 26 KB request arrives.
The request fails. The largest single run is the 24 KB hole — 2 KB short — and the other 15 KB sits in two separate pieces that cannot be combined with it because live blocks stand in between.
This is the signature of external fragmentation: 39 KB free, a 26 KB request, and a failure. Nothing is wrong with the allocator's arithmetic; the memory is simply in the wrong shape.
- Total free: 39 KB · Request: 26 KB · Result: FAILS
- Largest single run: 24 KB — 2 KB short
- The other 15 KB is stranded in two separate holes
Compaction, Paging, and the Contrast with Internal Fragmentation
The direct cure is compaction: slide the allocated blocks together so the holes merge into one contiguous run. The 39 KB scattered across three gaps becomes 39 KB in one piece, and the 26 KB request succeeds.
Compaction is expensive — every byte of every relocated block is copied — but the real constraint is subtler. It is only possible if addresses are translated at runtime. If a process holds absolute addresses fixed at load time, moving its memory breaks every pointer it owns.
That requirement is why the modern answer is not compaction but paging. Once memory is handed out in fixed-size pages and hardware translates addresses on every access, contiguity is no longer required and external fragmentation cannot arise at all. The cost is the bounded internal fragmentation that paging introduces — an unbounded problem traded for a capped one.
- Compaction merges scattered holes into one usable run
- It requires runtime address translation to be safe
- Paging eliminates the problem rather than curing it
External fragmentation — 39 KB free, a 26 KB request, and a failure
"""External fragmentation: free memory that is scattered, not contiguous."""
def coalesce(blocks):
"""Merge adjacent free runs. Blocks are [owner_or_None, size_kb]."""
merged = []
for owner, size in blocks:
if owner is None and merged and merged[-1][0] is None:
merged[-1][1] += size # join the run we just added
else:
merged.append([owner, size])
return merged
def free(blocks, name):
"""Release a named block, then merge any runs that became adjacent."""
for block in blocks:
if block[0] == name:
block[0] = None
return coalesce(blocks)
def holes(blocks):
return [size for owner, size in blocks if owner is None]
def compact(blocks):
"""Slide live blocks down so all free space forms one run."""
live = [b for b in blocks if b[0] is not None]
free_total = sum(holes(blocks))
return live + ([[None, free_total]] if free_total else [])
def main():
memory = [["A", 12], ["B", 12], ["E", 5], ["C", 24], ["H", 20]]
memory = free(memory, "B")
memory = free(memory, "C")
memory.append([None, 3])
request = 26
print(f"holes: {holes(memory)} total free: {sum(holes(memory))} KB")
print(f"largest run: {max(holes(memory))} KB")
print(f"request {request} KB -> "
f"{'fits' if max(holes(memory)) >= request else 'FAILS'}")
memory = compact(memory)
print(f"after compaction, largest run: {max(holes(memory))} KB")
print(f"request {request} KB -> "
f"{'fits' if max(holes(memory)) >= request else 'FAILS'}")
if __name__ == "__main__":
main()
// External fragmentation: free memory that is scattered, not contiguous.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Block {
std::string owner; // empty means free
int sizeKb;
};
// Merge adjacent free runs into one.
std::vector<Block> coalesce(const std::vector<Block>& blocks) {
std::vector<Block> merged;
for (const Block& block : blocks) {
if (block.owner.empty() && !merged.empty() && merged.back().owner.empty()) {
merged.back().sizeKb += block.sizeKb;
} else {
merged.push_back(block);
}
}
return merged;
}
int largestRun(const std::vector<Block>& blocks) {
int largest = 0;
for (const Block& block : blocks) {
if (block.owner.empty()) {
largest = std::max(largest, block.sizeKb);
}
}
return largest;
}
int main() {
std::vector<Block> memory {
{
"A", 12
}, {"", 12}, {"E", 5}, {"", 24},
{
"H", 20
}, {"", 3}
};
memory = coalesce(memory);
const int request = 26;
std::cout << "largest run: " << largestRun(memory) << " KB\n";
std::cout << "request " << request << " KB -> "
<< (largestRun(memory) >= request ? "fits" : "FAILS") << '\n';
}// External fragmentation: free memory that is scattered, not contiguous.
import java.util.ArrayList;
import java.util.List;
public class ExternalFragmentation {
/** A region of memory. A null owner means the block is free. */
record Block(String owner, int sizeKb) {
boolean isFree() {
return owner == null;
}
}
/** Merge adjacent free runs into one. */
static List<Block> coalesce(List<Block> blocks) {
List<Block> merged = new ArrayList<>();
for (Block block : blocks) {
int last = merged.size() - 1;
if (block.isFree() && last >= 0 && merged.get(last).isFree()) {
merged.set(last,
new Block(null, merged.get(last).sizeKb() + block.sizeKb()));
} else {
merged.add(block);
}
}
return merged;
}
static int largestRun(List<Block> blocks) {
return blocks.stream()
.filter(Block::isFree)
.mapToInt(Block::sizeKb)
.max()
.orElse(0);
}
public static void main(String[] args) {
List<Block> memory = coalesce(List.of(
new Block("A", 12), new Block(null, 12), new Block("E", 5),
new Block(null, 24), new Block("H", 20), new Block(null, 3)));
int request = 26;
System.out.println("largest run: " + largestRun(memory) + " KB");
System.out.println("request " + request + " KB -> "
+ (largestRun(memory) >= request ? "fits" : "FAILS"));
}
}Step through it
Running on 64 KB region; A(12K) B(12K) E(5K) C(24K) H(20K) allocated, then B and C freed
Read all 9 Steps
- a 64 KB region, fully allocated Five blocks fill the region exactly: A (12K), B (12K), E (5K), C (24K) and H (20K). There is no free memory at all, and therefore no fragmentation of any kind yet. Fragmentation is created by the pattern of frees, not by allocation.
- B is freed — one 12 KB hole appears B releases its 12 KB. That memory is now free and sits between A and E. A 12 KB request could be served from it immediately, so nothing is wrong yet: one hole is not fragmentation.
- C is freed — now two separate holes C releases 24 KB. Total free memory is now 12 + 24 = 36 KB, but it sits in two pieces separated by E, a live 5 KB block. The two holes cannot merge because a live block stands between them — this is the moment external fragmentation begins.
- a third hole at the tail A small 3 KB tail is also free at the end of the region. Total free is now 12 + 24 + 3 = 39 KB, scattered across three runs of very different sizes. Note that adjacent holes would have coalesced automatically; these cannot, because live blocks separate them.
- a 26 KB request arrives 39 KB is free, which is comfortably more than the 26 KB requested. The allocator scans the free list: 12 KB is too small, 24 KB is too small by 2 KB, 3 KB is far too small. No single run can hold the request.
- the request fails with memory to spare The request fails. This is the signature of external fragmentation: enough total free memory, and no single contiguous run long enough to use it. Nothing is wrong with the allocator's arithmetic — the memory is simply in the wrong shape. The largest run is 24 KB, just 2 KB short.
- compaction slides the live blocks together Compaction relocates A, E and H to the bottom of the region so that all the free space collects at the top. Every byte of every moved block must be copied, which is why compaction is expensive and is not done casually.
- the same request now succeeds The free memory is unchanged in quantity but now sits in one contiguous 27 KB run, so the 26 KB request is served. Compaction did not create memory; it changed the shape of what was already there.
- why compaction is rarely the real answer Compaction only works if addresses are translated at runtime. If a process holds absolute addresses fixed at load time, moving its memory breaks every pointer it owns. That requirement is why the modern answer is paging: hand out fixed-size pages, translate every access in hardware, and the contiguity requirement disappears along with external fragmentation itself — at the cost of bounded internal fragmentation.