Memory Model — manual, RAII, ownership, GC: কে manage করে আর দাম কত
Memory Management Models
Memory ফেরত দেওয়ার সিদ্ধান্তটা চার জায়গায় হতে পারে — programmer-এর হাতে, destructor-এর সময়ে, compiler-এর proof-এ, বা collector-এর অপেক্ষায়; এই লেসনে চারটা model-এর guarantee আর দাম মেপে দেখি।
আগে এটা বুঝি
Operating systems module-এ আপনি malloc আর free-এর ভেতরটা দেখেছেন — bin, tcache, sbrk, mmap। সেখানে প্রশ্নটা ছিল allocator কীভাবে কাজ করে। আজকের প্রশ্নটা এক লেভেল উপরে: কে ঠিক করবে free() ডাকার মুহূর্তটা?
মজার ব্যাপার হলো, প্রোগ্রামিং ভাষার ইতিহাসে এই একটা প্রশ্নের উত্তর চার ভাবে দেওয়া হয়েছে, আর প্রতিটা উত্তরই একটা পুরো ভাষা-পরিবারের পরিচয় হয়ে গেছে:
| Model | সিদ্ধান্ত নেয় কে | কখন | প্রতিনিধি ভাষা |
|---|---|---|---|
| Manual | Programmer | যখন সে ডাকে | C |
| RAII | Compiler (destructor insert করে) | scope exit-এ | C++ |
| Ownership | Compiler (proof চায়) | compile-এ যাচাই, run time-এ deterministic | Rust |
| Tracing GC | Collector | যখন সুবিধা হয় | 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-ই এই তিনটার উপর দাঁড়িয়ে:
- Stack allocation প্রায় বিনামূল্যের। Stack pointer সামান্য সরে, ব্যস — কোনো search নেই, bookkeeping নেই। Function return করলেই ফ্রেমটা মুছে যায়, কারণ stack-এর lifetime নিয়ম LIFO — যে ক্রমে allocate হলো, উল্টো ক্রমে free হয়। এই নিয়মটাই Stack Frame লেসনে দেখেছেন।
- Heap allocation দামি হতে পারে, আর lifetime arbitrary। যেকোনো সময়, যেকোনো ক্রমে free — এই স্বাধীনতাই সমস্যার উৎস। Allocator-কে free chunk খুঁজতে হয়, fragmentation সামলাতে হয়, metadata রাখতে হয়।
- সমস্যা তখনই শুরু যখন 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 leak | free কখনো ডাকা হলো না | RSS বাড়তে থাকে, শেষে OOM kill |
| Dangling pointer | free-র পরেও pointer রয়ে গেল | ওই address-এর নতুন বাসিন্দাকে ভুল ডেটা পড়া |
| Use-after-free | free-র পরে dereference | corruption, security hole (CVE-দের প্রিয় জায়গা) |
| Double free | একই pointer দুইবার free | allocator-এর 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:
- প্রতিটা value-র ঠিক একটা owner আছে।
- Assignment বা pass-by-value-তে value move হয় — পুরনো নাম invalid হয়ে যায় (ব্যবহার করলে compile error)।
- 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 থেকে বিচ্ছিন্নমূল টেবিল — guarantee আর দাম
| Manual (C) | RAII (C++) | Ownership (Rust) | Tracing GC | |
|---|---|---|---|---|
| Reclaim কখন | যখন ডাকবেন | Scope exit | Owner drop | Collector 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 latency | predictable | predictable | predictable | unpredictable pause |
| Programmer burden | সর্বোচ্চ | মাঝারি | মাঝারি (fighting the checker!) | সর্বনিম্ন |
| Run-time overhead | প্রায় শূন্য | প্রায় শূন্য | প্রায় শূন্য | constant tax |
ভেতরে কী ঘটছে
প্রতিটা model আসলে machine-এ কী করে
চারটা model-ই শেষ পর্যন্ত একই জায়গায় পৌঁছায় — malloc-family আর mmap। পার্থক্য কে, কোথায় free-র decision-টা encode করে সেই প্রশ্নে। LayerTrace-এ চার স্তর নামুন:
- Manual (C)Decision কোডে: free(p) লাইনটাই decision। Runtime-এ কোনো tracking নেই — pointer-এর বয়স, validity, কিছুই জানে না।
- RAII (C++)Decision scope-এ: compiler প্রতিটা scope-exit path-এ (return, break, exception unwind সহ) destructor call বুনে দেয়। Decision আর code structure এক হয়ে যায়।
- Ownership (Rust)Decision compile-এ verify হয়: borrow checker dataflow analysis চালিয়ে প্রমাণ করে প্রতিটা path-এ ঠিক একজনই live owner। Run time-এ যা ঘটে তা C++ RAII-রই সমান — Drop call।
- 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 খুলতে হবে।নিজে চালিয়ে দেখুন
একই bug তিন ভাষায় — C-তে crime, Rust-এ compile error, Java-তে exception
ধাপ ১ — 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 && ./crimeASan-এর 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:11Sanitizer ছাড়া 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.rsCompile হবে না। 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 moveRust-এ 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 অনিশ্চিত।
নিজে বানান
Scoped Arena Allocator — হাজারটা free এক free-তে
- Arena struct: বড় backing block + offset pointer
- arena_alloc(): alignment ঠিক করে offset বাড়ান — এটাই bump allocation
- Block শেষ হলে নতুন block চেইন করা
- JSON-ish tokenizer-এর সব node arena থেকে নেওয়া — কোথাও free নেই
- শেষে 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 && ./arena4 tokens parsed
all tokens freed with one callনিজে বাড়ান:
arena_save()/arena_restore()যোগ করুন — save point থেকে rollback (nested scope-এর অনুকরণ)। এটাই “scoped” শব্দের পুরো শক্তি।- valgrind চালিয়ে দেখান পুরো প্রোগ্রামে “definitely lost: 0 bytes” — tokenizer-এ একটাও free না থাকা সত্ত্বেও।
- 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-এ যাচাই হয়।
2Rust-এ let s2 = s1; করার পর s1 ব্যবহার করলে E0382 আসে। Machine level-এ move-এর সময় ঠিক কী হয়, আর কেন এই error double-free অসম্ভব করে?
যুক্তি
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-র বাস্তব প্রমাণ