Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ৬/১৫কঠিন৫৫ মিনিট

Memory Model — manual, RAII, ownership, GC: কে manage করে আর দাম কত

Memory Management Models

Memory ফেরত দেওয়ার সিদ্ধান্তটা চার জায়গায় হতে পারে — programmer-এর হাতে, destructor-এর সময়ে, compiler-এর proof-এ, বা collector-এর অপেক্ষায়; এই লেসনে চারটা model-এর guarantee আর দাম মেপে দেখি।

এই লেসন শেষে আপনি পারবেন

  • চারটা memory management model — manual (C), RAII (C++), ownership+borrow checking (Rust), tracing GC (Java/Go/Python) — প্রতিটা কোন মুহূর্তে, কার সিদ্ধান্তে memory ফেরত দেয় তা ব্যাখ্যা করতে পারবেন
  • প্রতিটা model-এর guarantee আর খরচ — latency profile, throughput cost, memory safety, programmer burden — একটা টেবিলে তুলনা করে কোন workload-এ কোনটা যুক্তিসঙ্গত তা প্রমাণসহ বলতে পারবেন
  • Dangling pointer, double free, use-after-free manual management-এর সরাসরি ফল তা নিজের লেখা C কোডে valgrind/AddressSanitizer দিয়ে ধরে দেখাতে পারবেন
  • একই লজিক Rust-এ লিখলে borrow checker ঠিক কোন লাইনে, কোন error code (E0382, E0502) দিয়ে compile time-এই আটকে দেয় — compiler error পড়ে বুঝতে পারবেন
  • Scope-based arena allocator-এর ডিজাইন ব্যাখ্যা করে বলতে পারবেন কেন হাজারটা free একটা free-তে নামিয়ে আসা যায়, আর এই trick কোথায় কাজে লাগে

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

Operating systems module-এ আপনি malloc আর free-এর ভেতরটা দেখেছেন — bin, tcache, sbrk, mmap। সেখানে প্রশ্নটা ছিল allocator কীভাবে কাজ করে। আজকের প্রশ্নটা এক লেভেল উপরে: কে ঠিক করবে free() ডাকার মুহূর্তটা?

মজার ব্যাপার হলো, প্রোগ্রামিং ভাষার ইতিহাসে এই একটা প্রশ্নের উত্তর চার ভাবে দেওয়া হয়েছে, আর প্রতিটা উত্তরই একটা পুরো ভাষা-পরিবারের পরিচয় হয়ে গেছে:

Modelসিদ্ধান্ত নেয় কেকখনপ্রতিনিধি ভাষা
ManualProgrammerযখন সে ডাকেC
RAIICompiler (destructor insert করে)scope exit-এC++
OwnershipCompiler (proof চায়)compile-এ যাচাই, run time-এ deterministicRust
Tracing GCCollectorযখন সুবিধা হয়Java, Go, C#, JavaScript

চারটাই একই hardware-এর উপর চলে — একই virtual page, একই allocator, একই System Call দিয়ে OS থেকে memory চায়। পার্থক্যটা ভাষার নিয়মে, silicon-এ না। আর প্রতিটা নিয়মের একটা দাম আছে: C-এর দাম হলো segfault, GC-এর দাম হলো pause।

আজকের কাজ দুইটা: (১) চারটা model-এর guarantee আর খরচ একটা সাধারণ scale-এ মাপা, (২) নিজের হাতে একই bug-টা C-তে করে valgrind-এ ধরা, তারপর Rust-এ দেখা compiler সেটা compile time-এই আটকে দিচ্ছে। শেষে build করবেন একটা arena allocator — শিল্পের সবচেয়ে প্রিয় “সুশৃঙ্খল manual management” হাতিয়ার।

মূল ধারণা

প্ল্যাটফর্মটা মনে করে নিই — stack আর heap

OS লেসন থেকে তিনটা সত্য ঝালিয়ে নিই, কারণ চারটা model-ই এই তিনটার উপর দাঁড়িয়ে:

  1. Stack allocation প্রায় বিনামূল্যের। Stack pointer সামান্য সরে, ব্যস — কোনো search নেই, bookkeeping নেই। Function return করলেই ফ্রেমটা মুছে যায়, কারণ stack-এর lifetime নিয়ম LIFO — যে ক্রমে allocate হলো, উল্টো ক্রমে free হয়। এই নিয়মটাই Stack Frame লেসনে দেখেছেন।
  2. Heap allocation দামি হতে পারে, আর lifetime arbitrary। যেকোনো সময়, যেকোনো ক্রমে free — এই স্বাধীনতাই সমস্যার উৎস। Allocator-কে free chunk খুঁজতে হয়, fragmentation সামলাতে হয়, metadata রাখতে হয়।
  3. সমস্যা তখনই শুরু যখন heap object-এর lifetime আর কোডের structure আলাদা হয়ে যায়। এক function-এ allocate, অন্যটায় release — এই দূরত্বেই dangling pointer, double free, leak ঢুকে পড়ে।

চারটা memory model আসলে এই তৃতীয় সত্যটার চারটা ভিন্ন জবাব।

Model ১: Manual — “আমি নিজেই দেখব”

Node *n = malloc(sizeof(Node));
/* ... ব্যবহার ... */
free(n);

নিয়ম মাত্র একটা: যে allocate করলে, তারই দায়িত্ব free করা। কোনো enforcement নেই — free ভুলে গেলে compiler কিছু বলবে না, দুইবার ডাকলেও না (বেশিরভাগ সময় চুপ থাকবে, তারপর একদিন production-এ crash করবে)।

এই model-এর ক্লাসিক ব্যর্থতাগুলো, প্রতিটা experiment section-এ নিজ হাতে করব:

Bugঘটে কখনফল
Memory leakfree কখনো ডাকা হলো নাRSS বাড়তে থাকে, শেষে OOM kill
Dangling pointerfree-র পরেও pointer রয়ে গেলওই address-এর নতুন বাসিন্দাকে ভুল ডেটা পড়া
Use-after-freefree-র পরে dereferencecorruption, security hole (CVE-দের প্রিয় জায়গা)
Double freeএকই pointer দুইবার freeallocator-এর internal list corrupt

Model ২: RAII — “scope-টাই owner”

C++-এর ভাবনা: free ডাকার দায়িত্ব programmer-এর হাত থেকে নিয়ে নিন — কিন্তু GC বানিয়ে না। বরং destructor ব্যবহার করুন:

void send_report() {
    std::string body = build_body();      // heap allocate — constructor-এ
    transmit(body);
}                                          // scope শেষ — destructor স্বয়ংক্রিয়ভাবে
                                           // body-র memory ফেরত দেয়। ঠিক এখানেই।

Compiler প্রতিটা scope-এর closing brace-এ destructor call insert করে দেয়। Pattern-টার নাম RAII — Resource Acquisition Is Initialization: resource কেনা = constructor চলা, ফেরত দেওয়া = destructor চলা। মূল জাদু: destructor-এর timing deterministic — scope exit মানেই মুক্তি। আর scope nesting সবসময় LIFO, তাই stack-এর নিয়মই heap-এ প্রয়োগ হয়ে যায়।

RAII শুধু memory-র জন্য না — একই কৌশল file descriptor, mutex lock, socket, DB transaction সবখানে কাজ করে (C++-এর std::lock_guard lock leak না, unlock ভুলে যাওয়া না — দুটোই অসম্ভব করে)।

Model ৩: Ownership — “compiler-কে proof করে দেখাও”

Rust-এর প্রশ্ন: RAII deterministic তো, কিন্তু C++-এ তো এখনও ভুল করা যায় — একই heap object-এর দুইটা raw pointer রাখা যায়, একটা দিয়ে free করে আরেকটা দিয়ে access করা যায়। Rust বলে: প্রতিটা heap object-এর ঠিক একজন owner থাকবে, আর নিয়মটা compile time-এ প্রমাণ করতে হবে। তিনটা rule:

  1. প্রতিটা value-র ঠিক একটা owner আছে।
  2. Assignment বা pass-by-value-তে value move হয় — পুরনো নাম invalid হয়ে যায় (ব্যবহার করলে compile error)।
  3. Borrowing: হয় একসাথে অনেক immutable borrow (&T), নয় ঠিক একটা mutable borrow (&mut T) — দুটো একসাথে না।
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;                  // move! s1-এর ownership s2-তে গেল
    // println!("{}", s1);        // ERROR [E0382]: borrow of moved value: `s1`
    println!("{}", s2);           // ঠিক আছে — s2-ই এখন owner

    let r1 = &s2;                 // immutable borrow
    let r2 = &s2;                 // আরেকটা immutable borrow — সমস্যা নেই
    // let r3 = &mut s2;          // ERROR [E0502]: cannot borrow `s2` as mutable
                                  //        because it is also borrowed as immutable
}

লক্ষ্য করুন — এই সব error compile time-এ। Run time-এ Rust-এর discipline আসলে RAII-রই সমান: owner-এর scope শেষ হলে destructor (Rust-এর ভাষায় Drop) চলে, সাথে সাথে। মানে Rust = RAII + একটা compiler যেটা প্রমাণ চায় আপনি rules ভাঙছেন না।

Model ৪: Tracing GC — “dead নাকি live, বলব periodic জরিপে”

Java, Go, Python, C#, JavaScript — এই পরিবারে programmer free() ডাকে না। Object allocate হয়, আর একটা collector মাঝে মাঝে জরিপ চালায়: কোন object-এ এখনো reachable কোনো reference আছে? যেগুলোতে নেই, সেগুলো reclaim।

দুটা গভীর consequence এখনই লক্ষ্য করুন (পরের লেসনের বিষয়):

  • Latency deferred: মুক্তির মুহূর্তটা আর code structure-এর সাথে সম্পর্কিত না — collector যখন চলবে তখন। Determinism চলে যায়।
  • Cycle সমস্যা নেই: A আর B একে অপরকে point করলেও, বাইরে থেকে কেউ না পৌঁছালে দুটোই unreachable — collector দুটোই ধরবে। (Reference counting এখানে ফাঁসে — লেসন ৮-এ দেখব।)
সময় ───────────────────────────────────────────────────────►

Manual (C):     [alloc]─────────[free]───────────────►
                         ↑ programmer ডাকলেই, যেকোনো মুহূর্তে
                           (ভুল মুহূর্তেও — UAF!)

RAII (C++):     [ctor]──────────┤scope exit├──────►
                                ↑ deterministic, compiler insert করে

Ownership(Rust):[alloc]─────────┤owner drops├─────►
                                ↑ deterministic + compiler প্রমাণ চেয়েছিল

Tracing GC:     [alloc]─────────────────────[collect]──►
                                              ↑ "যখন collector চলে" — code
                                                structure থেকে বিচ্ছিন্ন
চার model-এ একটা object-এর জীবনকাল — reclaim-এর মুহূর্তটা কোথায় বসে সেটাই পার্থক্য।

মূল টেবিল — guarantee আর দাম

Manual (C)RAII (C++)Ownership (Rust)Tracing GC
Reclaim কখনযখন ডাকবেনScope exitOwner dropCollector cycle-এ
Deterministic?হ্যাঁ (ভুলসহ)হ্যাঁহ্যাঁনা
Dangling/UAF সম্ভব?হ্যাঁহ্যাঁ (raw pointer-এ)Compile time-এ ব্লকনা
Leak সম্ভব?হ্যাঁহ্যাঁহ্যাঁ (Rc cycle-এ)হ্যাঁ (reachable-but-unused)
Allocation খরচমাঝারিমাঝারিমাঝারিসবচেয়ে সস্তা (bump pointer)
Reclaim খরচপ্রতি free-তেপ্রতি drop-এ (প্রায়ই inlined)প্রতি drop-এমাঝে মাঝে বড় batch
Worst-case latencypredictablepredictablepredictableunpredictable pause
Programmer burdenসর্বোচ্চমাঝারিমাঝারি (fighting the checker!)সর্বনিম্ন
Run-time overheadপ্রায় শূন্যপ্রায় শূন্যপ্রায় শূন্যconstant tax

ভেতরে কী ঘটছে

প্রতিটা model আসলে machine-এ কী করে

চারটা model-ই শেষ পর্যন্ত একই জায়গায় পৌঁছায় — malloc-family আর mmap। পার্থক্য কে, কোথায় free-র decision-টা encode করে সেই প্রশ্নে। LayerTrace-এ চার স্তর নামুন:

চার model, একই metal — decision কোথায় বাস করে
  1. Manual (C)Decision কোডে: free(p) লাইনটাই decision। Runtime-এ কোনো tracking নেই — pointer-এর বয়স, validity, কিছুই জানে না।
  2. RAII (C++)Decision scope-এ: compiler প্রতিটা scope-exit path-এ (return, break, exception unwind সহ) destructor call বুনে দেয়। Decision আর code structure এক হয়ে যায়।
  3. Ownership (Rust)Decision compile-এ verify হয়: borrow checker dataflow analysis চালিয়ে প্রমাণ করে প্রতিটা path-এ ঠিক একজনই live owner। Run time-এ যা ঘটে তা C++ RAII-রই সমান — Drop call।
  4. Tracing GCDecision heap-এ: object-দের reference graph নিজেই তথ্য। Collector graph traverse করে live-set বের করে, বাকি সব reclaim। Code-এ free-র কোনো চিহ্ন নেই।

Move semantics আসলে কী ঘটায়

Move শব্দটা শুনে মনে হয় data সরছে। Machine level-এ move = bitwise copy + source-কে invalid চিহ্নিত করা:

let s1 = String::from("hi");      // s1 = ptr, len, cap (৩টা word)

let s2 = s1;

machine-এ যা হলো:
  1. s1-এর 24 বাইট (ptr, len, cap) s2-তে copy     (memcpy — খুব সস্তা)
  2. s1-কে "moved-from" চিহ্নিত করা                (শুধু compiler-এর খাতায়!)

দ্বিতীয় ধাপে কোনো instruction নেই — purely compile-time bookkeeping। এজন্যই move প্রায় বিনামূল্যের: double-free অসম্ভব হয়ে যায়, কারণ s1 আর legal না — free ডাকার পথই বন্ধ। এটাই ownership-এর কেন্দ্রীয় বুদ্ধি: safety runtime mechanism না, static knowledge।

Borrow checker ঠিক কী analysis চালায়

Borrow checker-কে একটা flow-sensitive analysis ভাবুন: প্রতিটা program point-এ সে track করে —

  • কোন variable এখন live (পরে আবার পড়া হবে)
  • প্রতিটা live variable-এর উপর active borrow কয়টা, mutable না immutable

Rule ভাঙার প্রথম মুহূর্তে — যেমন moved হওয়া value-র ব্যবহার — সে exact source span সহ error ছুঁড়ে দেয়। Experiment section-এ E0382 আর E0502 দুটোই নিজের চোখে দেখবেন।

GC-র জগতে allocation কেন এত সস্তা

JVM বা Go-র nursery (young generation)-এ allocation আসলে:

bump-the-pointer:
  new_obj_addr = tlab_ptr;
  tlab_ptr += obj_size;         // ব্যস। একটা addition, একটা compare।

Thread-Local Allocation Buffer-এর ভেতরে কোনো lock, কোনো free-list search নেই। দামটা পরে দিতে হয় — collection-এর সময়। এই “এখন সস্তা, পরে একসাথে” accounting-টাই GC-র পরিচয়; এজন্যই GC-heavy ভাষায় short-lived object (parse tree, iterator, DTO) মুক্তভাবে ছড়ানো যায় — একই কাজ C-তে malloc/free জুড়িয়ে লিখলে অনেক সময় allocator-ই bottleneck হয়।

উদাহরণ

একই লজিক, চারটা ভাষা — কে কোন দায়িত্ব নিল

কাজ: integer array বানাও, এক element সেট করো, sum ছাপাও, সব ফেরত দাও। পার্থক্য দেখুন শুধু memory লাইনে।

C — প্রতিটা ধাপ হাতে:

int *xs = malloc(10 * sizeof(int));   /* কেনা: আমি */
if (!xs) return 1;
xs[0] = 42;
printf("%d\n", xs[0]);
free(xs);                             /* ফেরত: আমি — ভুললে leak, দুইবাড়ার corruption */

C++ — scope-ই owner:

std::vector<int> xs(10);              // কেনা: constructor
xs[0] = 42;
std::cout << xs[0] << "\n";
// ফেরত: ~vector() — scope exit-এ, নিশ্চিত; exception হলেও unwind করে চলবে

Rust — একই RAII, কিন্তু ভুলের রাস্তা compile-এই বন্ধ:

let xs = vec![0i32; 10];              // কেনা: Vec
println!("{}", xs[0]);
}   // ফেরত: Drop — আর কোনো path-ই double-free করার সুযোগ দেয় না

Java — কেউ কিছু ফেরত দেয় না, collector দেয়:

int[] xs = new int[10];               // কেনা: new — ভাষাতেই free-র syntax নেই
xs[0] = 42;
System.out.println(xs[0]);
// ফেরত: এখনই না। কোনো এক collect-এ।

চারটা একই উত্তর ছাপে। কিন্তু failure mode ভিন্ন: C-তে free না লিখলে leak, দুইবার লিখলে corruption। C++-এ vector ভুলে raw pointer-এ escape করালে dangling। Rust-এ ওই escape attempt-ই compiler আটকাবে। Java-তে এই class-এর কোনো ভুলই সম্ভব না — বিনিময়ে কখন reclaim হবে তা আপনার হাতে নেই।

ছবিতে

Lifetime বনাম Structure — সমস্যার মূল ছবিটা

সব memory bug-এর জন্মস্থান এই একটা ছবি: object-এর lifetime যখন code structure-এর সাথে মেলে না।

main()
 ├── parse_config()            config buffer এখানে জন্মায়
 │    │
 │    │   ← lifetime শুরু
 │    ▼
 ├── load_settings(cfg)        pointer এখানে বাইরে যায় ("escapes")
 │                             ┌───────────────────────────┐
 ├── ...                       │  config buffer তখনও বাঁচা?  │
 │                             │  কে জানে?!                 │
 ├── shutdown(cfg)             └───────────────────────────┘
 │    ▼
 └── free(cfg)                 lifetime শেষ — ঠিক এখানে?

চার model-এর প্রতিটা এই ambiguity-কে ভিন্নভাবে ভেঙে দেয়:

Manual:    ambiguity থাকে। আপনার মাথা ঠান্ডা রাখার দায়িত্ব।
RAII:      ambiguity-কে scope-এ বেঁধে ফেলে — scope-এর বাইরে যেতে
           চাইলে explicit transfer (move, clone, release) করতে হয়।
           Transfer-টা visible।
Ownership: RAII-র মতোই, প্লাস compiler প্রমাণ চায় transfer-এর পরে
           পুরনো নামে কেউ ছুঁয়েও দেখেনি।
GC:        ambiguity মুছে দেয় না — "যতক্ষণ reachable, ততক্ষণ বাঁচা"
           সংজ্ঞা দেয়। cfg কেউ ধরে রাখলে সে বাঁচাই থাকবে, leak না —
           কিন্তু "কে ধরে আছে" খুঁজতে heap dump খুলতে হবে।

নিজে চালিয়ে দেখুন

EXPERIMENT

একই bug তিন ভাষায় — C-তে crime, Rust-এ compile error, Java-তে exception

Linux বা macOS (gcc/clang, rustc, java)· ২০ মিনিট

ধাপ ১ — C-তে অপরাধ করুন। একটা leak, একটা use-after-free, একসাথে:

/* crime.c */
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    char *leaked = malloc(1024 * 1024);   /* 1 MB — হারিয়ে গেল, কেউ free করবে না */

    int *p = malloc(sizeof(int) * 4);
    p[0] = 7;
    free(p);
    printf("%d\n", p[0]);                 /* use-after-free! */
    return 0;
}
gcc -g -fsanitize=address -o crime crime.c && ./crime

ASan-এর output (সংক্ষেপে):

==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
READ of size 4 at 0x... thread T0
    #0 main crime.c:11
freed by thread T0 here:
    #0 free
    #1 main crime.c:9
allocated by thread T0 here:
    #0 malloc
    #1 main crime.c:7
SUMMARY: AddressSanitizer: heap-use-after-free crime.c:11

Sanitizer ছাড়া valgrind দিয়েও দেখুন — leak-টাও ধরা পড়বে:

gcc -g -o crime_nosan crime.c && valgrind --leak-check=full ./crime_nosan
# ==12346== HEAP SUMMARY:
# ==12346==   definitely lost: 1,048,576 bytes in 1 blocks

লক্ষ্য করুন: দুইটা bug-ই compiler চুপচাপ compile করেছে — -Wall -Wextra দিলেও UAF ধরা পড়ে না।

ধাপ ২ — একই লজিক Rust-এ:

// crime.rs
fn main() {
    let leaked = vec![0u8; 1024 * 1024];   // leak না — scope-এর শেষে Drop হবে

    let p = Box::new([0i32; 4]);
    drop(p);                               // free-র equivalent
    println!("{}", p[0]);                  // use-after-free-র attempt
}
rustc crime.rs

Compile হবে না। Error-টা পড়ুন:

error[E0382]: borrow of moved value: `p`
 --> crime.rs:7:20
  |
5 |     let p = Box::new([0i32; 4]);
  |         - move occurs because `p` has type `Box<[i32; 4]>`, which
  |           does not implement the `Copy` trait
6 |     drop(p);
  |          - value moved here
7 |     println!("{}", p[0]);
  |                    ^ value borrowed here after move

Rust-এ drop(p) করার পরে p exist করে না — “use after move”। অর্থাৎ UAF-এর Rust-সংস্করণ compile হয়ই না। আর leak? leaked vector-টা scope-এর শেষে নিজে নিজে মুক্ত হয় — manual forgetting সম্ভবই না (cycle ছাড়া, লেসন ৮-এ দেখব)।

ধাপ ৩ — Java:

public class Crime {
    public static void main(String[] args) {
        byte[] leaked = new byte[1024 * 1024];  // reference রাখলাম = leak না
        int[] p = new int[4];
        p[0] = 7;
        p = null;                               // "free"-র মতো কিছু
        System.out.println(p[0]);               // NullPointerException!
    }                                           // ভাঙা address পড়া অসম্ভব
}

UAF-এর জায়গায় Java দেয় clean exception — type system-ই memory safety দেয়, GC শুধু reclaim-এর দায়িত্বে।

যা প্রমাণিত হলো: একই মানসিক ভুল তিন ভাষায় তিন রকম শেষ — silent corruption (C, sanitizer ছাড়া), loud compile error (Rust), loud runtime exception (Java)। Toolchain কোন failure mode বেছে নেবে তা memory model ঠিক করে।

এটা কী প্রমাণ করে

Use-after-free আর leak C-তে নীরবে compile হয়ে valgrind/ASan-এ runtime-এ ধরা পড়ে; হুবহু একই লজিক Rust-এ compile time-এই E0382 error-এ ব্লক হয়; Java-তে দুটোই অসম্ভব — বিনিময়ে reclaim-এর timing অনিশ্চিত।

নিজে বানান

BUILD IT

Scoped Arena Allocator — হাজারটা free এক free-তে

C · ●●○○○
  1. Arena struct: বড় backing block + offset pointer
  2. arena_alloc(): alignment ঠিক করে offset বাড়ান — এটাই bump allocation
  3. Block শেষ হলে নতুন block চেইন করা
  4. JSON-ish tokenizer-এর সব node arena থেকে নেওয়া — কোথাও free নেই
  5. শেষে arena_destroy(): পুরো চেইন এক লুপে free

ভাবনাটা: অনেক object-এর lifetime যদি একই হয় (সবাই “এই request/frame/parse-এর সাথে বাঁচবে, শেষে একসাথে মরবে”), তাহলে প্রতিটার আলাদা malloc/free-র দরকার নেই। একটা বড় block নাও, ভেতর থেকে pointer বাড়িয়ে বিলিয়ে দাও, শেষে block-টা একবারে ফেরত দাও।

/* arena.c — scoped arena allocator */
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

typedef struct Block {
    struct Block *next;
    size_t used, cap;
} Block;

typedef struct { Block *head; } Arena;
#define BLOCK_CAP 4096

static Block *block_new(size_t cap) {
    Block *b = malloc(sizeof(Block) + cap);
    b->next = NULL; b->used = 0; b->cap = cap;
    return b;
}
void arena_init(Arena *a) { a->head = block_new(BLOCK_CAP); }
void *arena_alloc(Arena *a, size_t n) {
    n = (n + 15) & ~(size_t)15;                 /* 16-byte align */
    if (a->head->used + n > a->head->cap) {     /* block শেষ — নতুনটা চেইন করি */
        Block *nb = block_new(n > BLOCK_CAP ? n : BLOCK_CAP);
        nb->next = a->head;
        a->head = nb;
    }
    void *p = (char *)(a->head + 1) + a->head->used;
    a->head->used += n;
    return p;                                   /* কোনো search, কোনো per-object metadata */
}
void arena_destroy(Arena *a) {                  /* পুরো arena — এক লুপ */
    while (a->head) {
        Block *d = a->head; a->head = a->head->next;
        free(d);
    }
}

/* ---- JSON-ish tokenizer: প্রতিটা node arena থেকে, কোথাও free নেই ---- */
typedef struct Token { char *text; struct Token *next; } Token;
Token *tokenize(Arena *a, const char *src) {
    Token *list = NULL, **tail = &list;
    const char *p = src;
    while (*p) {
        while (*p == ' ' || *p == ',' || *p == ':' || *p == '\n') p++;
        if (!*p) break;
        const char *start = p;
        while (*p && *p != ' ' && *p != ',' && *p != ':' && *p != '\n') p++;
        Token *t = arena_alloc(a, sizeof(Token));
        t->text = arena_alloc(a, p - start + 1);
        memcpy(t->text, start, p - start);
        t->text[p - start] = '\0';
        t->next = NULL;
        *tail = t; tail = &t->next;
    }
    return list;
}
int main(void) {
    Arena a; arena_init(&a);
    Token *ts = tokenize(&a, "{\"name\": \"Rust\", \"year\": 2010}");
    int count = 0;
    for (Token *t = ts; t; t = t->next) count++;
    printf("%d tokens parsed\n", count);
    arena_destroy(&a);                          /* হাজারটা object গেল এক কলে */
    printf("all tokens freed with one call\n");
    return 0;
}
gcc -O2 -o arena arena.c && ./arena
4 tokens parsed
all tokens freed with one call

নিজে বাড়ান:

  1. arena_save()/arena_restore() যোগ করুন — save point থেকে rollback (nested scope-এর অনুকরণ)। এটাই “scoped” শব্দের পুরো শক্তি।
  2. valgrind চালিয়ে দেখান পুরো প্রোগ্রামে “definitely lost: 0 bytes” — tokenizer-এ একটাও free না থাকা সত্ত্বেও।
  3. Benchmark: ১০ লাখ arena_alloc(24) বনাম ১০ লাখ malloc(24)+free — speedup নিজে মাপুন; এটাই GC-র bump allocation-এর সাথে manual জগতের মিল।

বাস্তব সিস্টেমে

বাস্তব জগতে চার model-এর ঠিকানা

nginx আর Apache APR pool। Connection প্রতি একটা arena/pool — request শেষে pool destroy, হাজার ছোট allocation এক call-এ মুছে যায়। এই লেসনের arena-টাই production-scale রূপে। Game engine-এর frame arena একই গল্প: প্রতি frame শুরুতে reset, frame-এর সব temp data তাতে।

SQLite। Lookaside slab allocator + arena-style page cache — embedded database-এ malloc traffic কমানোই performance-এর মেরুদণ্ড।

Linux kernel। Kernel নিজের slab allocator চালায়, GC নেই — কিন্তু reference counting ব্যবহার করে (kref)। Manual + counting hybrid-এর বাস্তব নমুনা।

Linux kernel-এর Rust driver আর Firefox-এর Rust components। Ownership model-এর সবচেয়ে বড় industrial validation — memory-critical C/C++ codebase-এ ধীরে ধীরে Rust ঢুকছে, কারণ model GC-free হওয়ায় runtime-এর সাথে negotiate করতে হয় না।

JVM server-দের tuning culture। Production Java team-দের বেশিরভাগ performance আলোচনাই আসলে GC discussion: heap size, pause goal, collector choice। Model বেছে নেওয়ার পরেও তার খরচ ম্যানেজ করতে হয় — এটাই পরের লেসনের প্রবেশদ্বার।

Swift-এর হাইব্রিড। Apple Swift ARC (automatic reference counting) নিয়েছে — burden কম, কিন্তু deterministic destruction ধরে রাখা। Model-গুলো বাক্সে বন্দি category না; ডিজাইনাররা মেশায়, আপনিও করবেন: Rust-এও Arc আছে, Java-তেও off-heap arena আছে।

যে ভুলগুলো সবাই করে

“GC মানেই ধীর ভাষা; manual management মানেই দ্রুত।”

Throughput-এ প্রায়ই উল্টো হয়। Modern GC-র bump allocation হাতে-লেখা malloc-এর চেয়ে দ্রুত, আর bulk reclaim-এর কারণে allocation-heavy benchmark-এ GC ভাষা অনেক সময় C-কে হারায়। GC-র আসল দাম latency distribution-এ — worst-case pause। Manual-এর আসল দাম programmer time আর bug surface। সঠিক প্রশ্ন “কোনটা দ্রুত” না — “আমার workload-এর কোন metric দামি: average throughput না tail latency?”

“Rust-এ GC নেই, তাই Rust-ও C-এর মতো manual management।”

Rust manual না — programmer কোনো free ডাকে না, ownership tree ভাঙলে Drop স্বয়ংক্রিয়ভাবে চলে। পার্থক্য C-এর সাথে: C-তে lifetime সিদ্ধান্ত runtime-এ programmer-এর, Rust-এ compile time-এ compiler-এর প্রমাণিত। Rust-কে “RAII + static proof” ভাবুন — “C কিন্তু ঝুঁকিপূর্ণ” না।

“RAII শুধু memory-র জন্য।”

উল্টো — RAII-র সবচেয়ে বড় industrial লাভ memory ছাড়া resource-এ: file descriptor, mutex, database connection, GPU texture। Destructor pattern নিশ্চিত করে exception path-এও resource ফেরত যায় — C-তে এজন্যই প্রতিটা error branch-এ cleanup goto লিখতে হয় (kernel-এর goto out idiom এই pain-এর চিহ্ন)।

“Stack বনাম heap সিদ্ধান্তটা ভাষার — Java-তে object তাই heap-এ।”

ভাষার নিয়ম শুধু “reference type কোথায় যেতে পারে” ঠিক করে; escape analysis করলে JIT compiler অনেক heap-bound object-কে stack-এ scalar-replace করে দেয় (HotSpot নিয়মিত করে)। C-তেও VLA/alloca দিয়ে heap-এর কাজ stack-এ হয়। অবস্থানটা ভাষা + implementation + optimization-এর joint ফল — fixed label না।

বুঝেছেন কি না দেখুন

1

চার model-এর কোনটায় reclaim-এর মুহূর্ত code-এর structure (scope, function boundary) থেকে সম্পূর্ণ বিচ্ছিন্ন? বাকি তিনটার reclaim trigger এক লাইনে লিখুন।

স্মরণ

Tracing GC — reclaim হয় collector cycle চলার সময়, যা code structure নয়, heap-এর অবস্থা দিয়ে ঠিক হয়।

বাকি তিনটা: Manual — programmer-এর লেখা free() call-এ। RAII — scope exit-এ compiler-inserted destructor-এ। Ownership (Rust) — owner value drop করার সময় (scope-এ বাঁধা, তাই deterministic), তবে সিদ্ধান্তের সঠিকতা compile time-এ যাচাই হয়।

2

Rust-এ let s2 = s1; করার পর s1 ব্যবহার করলে E0382 আসে। Machine level-এ move-এর সময় ঠিক কী হয়, আর কেন এই error double-free অসম্ভব করে?

যুক্তি

Machine level-এ move = s1-এর representation (ptr+len+cap — ৩টা word) s2-তে bitwise copy, তারপর s1-কে compile-time-এ “moved-from” চিহ্নিত করা — কোনো extra runtime instruction নেই।

Double-free অসম্ভব হয় কারণ heap buffer-টার দায় এখন শুধু s2-র; end-of-scope-এ শুধু s2-র Drop চলবে। s1 moved-from — তাকে ব্যবহারই করা যাবে না, free ডাকার পথই compile-এ বন্ধ।

3

একটা web server প্রতি request-এ গড়ে ৫০,০০০টা ছোট object বানায় যা request শেষ হলে আর কেউ ব্যবহার করে না, আর handler-এর বাইরে যায় না। কোন memory strategy সবচেয়ে মানানসই এবং কেন?

প্রয়োগ

Request-scoped arena/pool: handler-এর শুরুতে arena create/reset, সব allocation সেখান থেকে, response পাঠানোর পর একটা reset/destroy।

কারণ: (১) ৫০,০০০টা free-list allocation-এর খরচ বাঁচে — bump pointer-এ নেমে আসে (GC-র সেই সুবিধা, deterministic ভাবে); (২) lifetime-গুলো perfectly aligned এক scope-এ — arena-র ideal case; (৩) leak প্রায় structurally অসম্ভব। বাস্তব উদাহরণ: nginx connection pool, game frame arena। GC-ও কাজ করত (infant mortality high — পরের লেসনের generational hypothesis), কিন্তু pause আর footprint নিয়ন্ত্রণ হাত থেকে যেত।

4

আপনাকে একটা embedded pacemaker firmware লিখতে হবে। চার model-এর মধ্যে কোনটা বাদ দেওয়া সবচেয়ে সহজ এবং কেন? বাকিদের মধ্যে কোন tension থাকবে?

ডিজাইন

Tracing GC সবার আগে বাদ — unpredictable pause medical device-এ unacceptable (heartbeat timing miss মানেই বিপদ), আর memory headroom সীমিত।

Manual C ঐতিহ্যগত পছন্দ, কিন্তু UAF/double-free-র risk এমন device-এ জীবন-মরণ। বাস্তব tension: Rust-এর ownership (GC-free + memory-safe, কিন্তু dynamic allocation এড়াতে হয় — অনেক firmware standard no-heap চায়) বনাম static allocation + সুশৃঙ্খল arena C-তে। References-এর Tock OS পেপারটা (SOSP ‘17) দেখায় ownership no-GC embedded context-এ কাজ করে।

5

“Java-তে memory leak হয় না” — দাবিটা ভেঙে দিন। Leak হলে ধরবেন কীভাবে, C-এর leak ধরার পদ্ধতি থেকে পার্থক্য কোথায়?

যুক্তি

Leak-এর সংজ্ঞা GC-তে বদলায়: reachable কিন্তু আর কখনো ব্যবহৃত হবে না এমন object। Collector-এর কাছে সেগুলো valid live data — static field-এ আটকে থাকা listener list, পুরনো session-এর Map entry, eviction policy-হীন cache। Reference count শূন্য হবেই না; ভাষা সঠিক আচরণই করছে।

ধরার পদ্ধতি তাই উল্টো: C-তে valgrind “definitely lost” (unreachable) খোঁজে; Java-তে heap dump (jmap/MAT) নিয়ে dominator tree-তে দেখতে হয় কোন বড় retained set কার হাতে আটকে — reachable graph-এর ভেতরেই culprit। Tool ভিন্ন, mental model উল্টো।

এরপর কী

এরপর কী

আজ আমরা চার model-এর bird’s-eye view নিয়েছি, কিন্তু tracing GC-কে রেখেছি black box হিসেবে — “collector চলল, মুছে গেল”। কিন্তু collector ঠিক কীভাবে জানে কোন object dead? Reachability জরিপটা কেমন হয় — root কারা, mark আর sweep ঠিক কী traverse করে? Mark-sweep, copying, generational — এই শব্দগুলোর পেছনের algorithm, tri-color marking-এর invariant, আর “objects die young” hypothesis কীভাবে modern collector-দের (G1, ZGC, Shenandoah, Go-র pacer) design নির্ধারণ করে — সবটা পরের লেসনে। শেষ করে আপনি নিজ হাতে C-তে একটা পূর্ণ mark-sweep collector লিখবেন আর fragmentation মাপবেন।

পরের লেসন: Tracing Garbage Collection — mark-sweep থেকে generational পর্যন্ত।

আরও পড়ুন

  • The Garbage Collection Handbook: The Art of Automatic Memory Management, 2nd ed. — Richard Jones, Antony Hosking, Eliot Moss · Automatic memory management-এর বাইবেল — chapter 1-এর taxonomy-ই এই লেসনের spectrum-এর ভিত্তি
  • The Design and Evolution of C++ — Bjarne Stroustrup · RAII-র উৎস — constructor/destructor দিয়ে resource management ভাবনাটা কীভাবে exception safety থেকে জন্ম নিল
  • The Rust Programming Language, Chapter 4 — Understanding Ownership — Steve Klabnik, Carol Nichols · Move semantics আর borrow rules-এর official walkthrough — এই লেসনের Rust উদাহরণের সাথে মেলান
  • Multiprogramming a 64 kB Computer Safely and Efficiently (SOSP '17) — Amit Levy, Bradford Campbell, Branden Ghena, et al. · Tock OS — ownership model দিয়ে embedded system-এ GC ছাড়া memory safety-র বাস্তব প্রমাণ