রানটাইম এনভায়রনমেন্ট ও গার্বেজ কালেকশন
এই পাঠে যা শিখবেন
- রানটাইম এনভায়রনমেন্টের ধারণা — আগের মডিউলগুলোর সংশ্লেষণ হিসেবে
- ম্যানুয়াল মেমরি ম্যানেজমেন্টের দুই ক্লাসিক বাগ — মেমরি লিক ও ড্যাংলিং পয়েন্টার
- গার্বেজ কালেকশনের ধারণা ও রেফারেন্স কাউন্টিং কৌশল
- একটি বাস্তব রেফারেন্স সাইকেল তৈরি করে বিশুদ্ধ রেফারেন্স-কাউন্টিং-এর সীমাবদ্ধতা কোড দিয়ে প্রমাণ করা
১ · রানটাইম এনভায়রনমেন্ট — এখন পর্যন্ত শেখা সবকিছুর সংশ্লেষণ
রানটাইম এনভায়রনমেন্টRuntime Environmentএকটি কম্পাইল/ইন্টারপ্রেট হওয়া প্রোগ্রাম চলার সময় যে সাপোর্ট ইনফ্রাস্ট্রাকচারের উপর নির্ভর করে — শুধু জেনারেট হওয়া রও ইনস্ট্রাকশন নয়, তার সাথে সংশ্লিষ্ট মেমরি ব্যবস্থাপনাও। — এতে আছে কল স্ট্যাক (M11/L48-এ বিস্তারিত — প্রতিটি সাবপ্রোগ্রাম কলের অ্যাক্টিভেশন রেকর্ড), হিপ (M8/L39-এর হিপ-ডায়নামিক স্টোরেজ ক্যাটাগরি), এবং (এই পাঠের মূল নতুন বিষয়) স্বয়ংক্রিয় মেমরি ম্যানেজমেন্ট।
২ · ম্যানুয়াল মেমরি ম্যানেজমেন্ট — দুই ক্লাসিক বাগ
C-এর malloc/free-এর মতো ভাষায় প্রোগ্রামার নিজেই স্পষ্টভাবে হিপ মেমরি চেয়ে নেয় ও
মুক্ত করে দেয় — এবং এটি সততার সাথে বলতে হয় খুবই ত্রুটিপ্রবণ। দুটো ক্লাসিক বাগ:
- মেমরি লিক (Memory Leak): এমন হিপ মেমরি যা আর reachable/ব্যবহারযোগ্য নয়, কিন্তু কখনো
স্পষ্টভাবে
freeকরা হয়নি — অনির্দিষ্টকালের জন্য মেমরি অপচয় হতে থাকে। - ড্যাংলিং পয়েন্টার (Dangling Pointer) / use-after-free: যে মেমরি ইতিমধ্যে
freeহয়ে গেছে, তার একটি পয়েন্টার/রেফারেন্স এখনো থেকে যায় এবং সেটি পরে ব্যবহার করা হয় — M10/L47-এর পয়েন্টার-ডিরেফারেন্স ঝুঁকির সরাসরি বিস্তার, একটি গুরুতর ও সাধারণ বাস্তব বাগ শ্রেণি।
৩ · গার্বেজ কালেকশন ও রেফারেন্স কাউন্টিং
গার্বেজ কালেকশনGarbage Collection (GC)রানটাইম স্বয়ংক্রিয়ভাবে যে হিপ মেমরি আর কোনো সক্রিয় রেফারেন্স থেকে reachable নয়, তা চিহ্নিত করে রিক্লেইম করার প্রক্রিয়া — Python, Java, JavaScript, Go-সহ বেশিরভাগ আধুনিক ভাষার পদ্ধতি। একটি অবজেক্ট "reachable" যদি এটি বর্তমানে সক্রিয় কোনো স্কোপ/অ্যাক্টিভেশন রেকর্ডের (M11/L48) কোনো ভ্যারিয়েবল (M8/L37-এর বাইন্ডিং) থেকে সরাসরি বা পরোক্ষভাবে অন্য কোনো reachable অবজেক্টের মাধ্যমে পৌঁছানো যায়।
রেফারেন্স কাউন্টিংReference Countingপ্রতিটি হিপ অবজেক্ট নিজের একটি কাউন্ট রাখে — কতগুলো রেফারেন্স এই মুহূর্তে তাকে নির্দেশ করছে; নতুন রেফারেন্স তৈরি হলে কাউন্ট বাড়ে, রেফারেন্স মুছে গেলে/স্কোপ থেকে বেরিয়ে গেলে কাউন্ট কমে; কাউন্ট শূন্যে পৌঁছালেই অবজেক্ট সাথে সাথে রিক্লেইম হয়। Python-এর প্রকৃত প্রাইমারি GC কৌশল এটাই (M13/L55-এ আরও বিস্তারিত)।
৪ · সীমাবদ্ধতা — রেফারেন্স সাইকেল
বিশুদ্ধ রেফারেন্স কাউন্টিং-এর একটি গুরুত্বপূর্ণ সীমাবদ্ধতা আছে — পারস্পরিকভাবে একে অপরকে রেফারেন্স করা অবজেক্টের সাইকেল (অবজেক্ট A অবজেক্ট B-কে রেফারেন্স করে, B আবার A-কে রেফারেন্স করে, কিন্তু কেউই কোনো সক্রিয় স্কোপ থেকে reachable নয়) কখনোই তাদের রেফারেন্স কাউন্ট শূন্যে পৌঁছাতে দেবে না — যদিও তারা প্রকৃতপক্ষে reachable নয়, তবুও genuine garbage। বাস্তব রেফারেন্স-কাউন্টিং সিস্টেম (Python-সহ) তাই একটি পৃথক সাইকেল-ডিটেক্টিং মেকানিজম প্রয়োজন করে এই বিশেষ কেসটি হ্যান্ডেল করার জন্য।
৫ · বাস্তবায়ন — স্বাভাবিক কালেকশন বনাম রেফারেন্স সাইকেল
নিচের কোড সেলে দুটো কেস দেখানো হয়েছে — প্রথমে একটি সাধারণ, চক্রবিহীন অবজেক্ট সঠিকভাবে কালেক্ট হচ্ছে; তারপর
দুটো অবজেক্ট A ও B একে অপরকে রেফারেন্স করছে (একটি সাইকেল), বাইরের একমাত্র root রেফারেন্স রিলিজ হওয়ার পরেও
উভয়ের ref_count শূন্যের উপরে আটকে থাকছে — যদিও তারা কোনো root থেকে আর reachable নয়।
class RefCountedObject:
"""প্রতিটি অবজেক্ট নিজের ref_count রাখে -- কতগুলো রেফারেন্স এই মুহূর্তে তাকে নির্দেশ করছে।"""
def __init__(self, name):
self.name = name
self.ref_count = 0
self.points_to = [] # এই অবজেক্ট অন্য কাকে রেফারেন্স করছে (সাইকেল দেখানোর জন্য)
self.collected = False
def add_ref(self):
self.ref_count += 1
def remove_ref(self):
self.ref_count -= 1
if self.ref_count <= 0 and not self.collected:
self.collected = True
print(f" [কালেক্টেড] {self.name}: ref_count শূন্যে পৌঁছেছে, মেমরি রিক্লেইম হলো")
def is_reachable_from_roots(obj, roots):
"""একটি অবজেক্ট এখনো কোনো root (সক্রিয় স্কোপের ভ্যারিয়েবল) থেকে reachable কিনা যাচাই করে।"""
visited = set()
def dfs(o):
if id(o) in visited:
return False
visited.add(id(o))
if o is obj:
return True
return any(dfs(p) for p in o.points_to)
return any(dfs(r) for r in roots)
print("== কেস ১: স্বাভাবিক, চক্রবিহীন রেফারেন্স কাউন্টিং ==")
x = RefCountedObject("X")
x.add_ref()
print("X-এর ref_count (১টি root রেফারেন্সের পর):", x.ref_count)
x.remove_ref()
print("X-এর ref_count (root রেফারেন্স রিলিজের পর):", x.ref_count, " collected:", x.collected)
print("\n== কেস ২: ইচ্ছাকৃত রেফারেন্স সাইকেল (A <-> B), কোনো external root ছাড়া ==")
a = RefCountedObject("A")
b = RefCountedObject("B")
a.add_ref() # একটি root ভ্যারিয়েবল সাময়িকভাবে A-কে রেফারেন্স করছে
a.points_to.append(b); b.add_ref() # A -> B রেফারেন্স
b.points_to.append(a); a.add_ref() # B -> A রেফারেন্স (সাইকেল সম্পূর্ণ)
print("সাইকেল তৈরির পর: A.ref_count =", a.ref_count, " B.ref_count =", b.ref_count)
a.remove_ref() # একমাত্র root রেফারেন্সটি স্কোপ থেকে বেরিয়ে গেল
print("root রিলিজের পর: A.ref_count =", a.ref_count, " B.ref_count =", b.ref_count)
print("A collected?", a.collected, " B collected?", b.collected)
roots = [] # আর কোনো root A বা B-কে নির্দেশ করছে না
print("A এখনো কোনো root থেকে reachable?", is_reachable_from_roots(a, roots))
print("B এখনো কোনো root থেকে reachable?", is_reachable_from_roots(b, roots))
print("\nউপসংহার: A ও B উভয়েরই ref_count > 0, অথচ কোনো root থেকেই তারা reachable নয়")
print("-- এটাই বিশুদ্ধ রেফারেন্স কাউন্টিং-এর সাইকেল-সীমাবদ্ধতা, কনক্রিটভাবে প্রমাণিত।")
x.remove_ref() কল হওয়া মাত্রই "[কালেক্টেড]" বার্তা দেখা যায়, যেহেতু
ref_count সাথে সাথে শূন্যে নেমে যায়। কিন্তু কেস ২-তে, বাইরের একমাত্র root রেফারেন্স
রিলিজ হওয়ার পরেও — অর্থাৎ প্রোগ্রামের কোনো সক্রিয় ভ্যারিয়েবল আর A বা B-কে নির্দেশ করছে না — উভয়ের
ref_count এখনো ১ (একে অপরের কাছ থেকে পাওয়া রেফারেন্স), এবং কোনো "[কালেক্টেড]" বার্তা দেখা যায়
না। is_reachable_from_roots স্পষ্টভাবে দেখায় উভয়ে False — genuine garbage, কিন্তু
চিরকাল স্মৃতিতে আটকে থাকবে যদি একটি পৃথক সাইকেল-ডিটেক্টর না থাকে।
গার্বেজ কালেকশন প্রোগ্রামারকে ম্যানুয়াল মেমরি ম্যানেজমেন্টের ঝুঁকি (লিক, ড্যাংলিং পয়েন্টার) থেকে মুক্তি দেয়, কিন্তু রেফারেন্স কাউন্টিং একা যথেষ্ট নয় — সাইকেল একটি বাস্তব, ফাঁকফোকর যা শুধুমাত্র একটি সরাসরি reachability-ভিত্তিক (গ্রাফ ট্রাভার্সাল) মেকানিজম সমাধান করতে পারে। M13/L55-এ দেখা যাবে Python বাস্তবে এই সমস্যা কীভাবে সমাধান করে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
উপরের কোড সেলে যদি b.points_to.append(a); a.add_ref() লাইনটি বাদ দেওয়া হতো (শুধু A -> B, B -> A নয়), তাহলে a.remove_ref()-এর পর কী হতো?
তাহলে আর প্রকৃত সাইকেল থাকত না — শুধু A -> B একমুখী রেফারেন্স। a.remove_ref() কল হলে
A-এর ref_count ০-এ নেমে যেত এবং A কালেক্টেড হতো; A কালেক্ট হওয়ার সময় বাস্তব রেফারেন্স-কাউন্টিং
সিস্টেমে A-এর ধরে রাখা রেফারেন্সগুলোও (এখানে B) রিলিজ করা হয়, ফলে B-এর ref_count-ও ০-এ নেমে
B-ও কালেক্ট হতো — একটি স্বাভাবিক চেইন-রিক্লেমেশন, কোনো সাইকেল-সমস্যা ছাড়াই।
প্র ০২ রেফারেন্স কাউন্টিং-এর একটি সুবিধা হলো এটি "সাথে সাথে" (immediately) মেমরি রিক্লেইম করে। GC-এর অন্যান্য কৌশল (যেমন mark-and-sweep, শুধু নাম-উল্লেখ) কি এই সুবিধা দেয়?
না, সাধারণত নয় — mark-and-sweep-এর মতো ট্রেসিং-ভিত্তিক GC কৌশল সাধারণত পর্যায়ক্রমে (periodically) চলে, পুরো reachability গ্রাফ ট্রাভার্স করে অ্যাক্সেসযোগ্য অবজেক্ট চিহ্নিত করে, তারপর বাকি সব রিক্লেইম করে — একটি অবজেক্ট unreachable হওয়ার সাথে সাথেই নয়, বরং পরের GC পাস চলার সময় রিক্লেইম হয়। রেফারেন্স কাউন্টিং-এর "সাথে সাথে রিক্লেইম" সুবিধা তার প্রধান আকর্ষণ, যদিও সাইকেল-সমস্যা তার বড় দুর্বলতা — বাস্তব সিস্টেম (Python) তাই দুটো কৌশলই একসাথে ব্যবহার করে।
প্র ০৩ মেমরি লিক ও রেফারেন্স সাইকেল — দুটোই "মেমরি অপচয় হচ্ছে" এই অর্থে একরকম শোনায়। এই দুটোর মূল পার্থক্য কী?
মেমরি লিক ম্যানুয়াল মেমরি ম্যানেজমেন্টের একটি প্রোগ্রামার-এরর — প্রোগ্রামার স্পষ্টভাবে free
কল করতে ভুলে যায়, একটি মানুষের ভুল যা GC-যুক্ত ভাষায় সাধারণত ঘটেই না। রেফারেন্স সাইকেল সম্পূর্ণ ভিন্ন —
এটি GC-যুক্ত ভাষাতেই ঘটে, প্রোগ্রামার সবকিছু "সঠিকভাবে" করেছে (কোনো ম্যানুয়াল free ভুলে যাওয়ার
প্রশ্নই নেই), তবুও পারস্পরিক রেফারেন্সের কাঠামোগত কারণে বিশুদ্ধ রেফারেন্স-কাউন্টিং অ্যালগরিদম নিজেই ব্যর্থ
হয় — এটি একটি অ্যালগরিদমিক সীমাবদ্ধতা, মানুষের ভুল নয়।
অনুশীলন
-
চিন্তা করুন: একটি তিন-অবজেক্টের সাইকেল কল্পনা করুন — A -> B -> C -> A (প্রতিটি পরেরটিকে রেফারেন্স করে, শেষেরটি আবার প্রথমটিকে)। বাইরের একমাত্র root রেফারেন্স (A-এর দিকে) রিলিজ হওয়ার পর প্রতিটি অবজেক্টের ref_count কত হবে বলে আপনার মনে হয়?
প্রতিটির
ref_countহবে ১ — A-এর কাউন্ট ১ (শুধু C থেকে আসা রেফারেন্স, root রিলিজ হয়ে যাওয়ার পর), B-এর কাউন্ট ১ (শুধু A থেকে), C-এর কাউন্ট ১ (শুধু B থেকে)। তিনটিই এখনো ০-এর উপরে, তিনটিই কোনো root থেকে reachable নয় — দুই-অবজেক্টের সাইকেলের মতোই সীমাবদ্ধতা, শুধু চক্রের দৈর্ঘ্য বড়। সাইকেলের আকার যাই হোক, বিশুদ্ধ রেফারেন্স কাউন্টিং একইভাবে ব্যর্থ হয়। -
পরীক্ষা করুন: উপরের কোড সেলে কেস ২-এর শেষে
b.remove_ref(); a.remove_ref()যোগ করে Run চেপে দেখুন (এটি ম্যানুয়ালি সাইকেলের ভেতরের রেফারেন্সগুলোও রিলিজ করে দিচ্ছে) — এবার কি "[কালেক্টেড]" বার্তা দেখা যায়?হ্যাঁ —
b.remove_ref()B-এর কাউন্ট ১ থেকে ০-এ নামাবে (B কালেক্টেড), এবংa.remove_ref()A-এর কাউন্টও ১ থেকে ০-এ নামাবে (A কালেক্টেড)। এটি দেখায় সমস্যাটি রেফারেন্স কাউন্টিং অ্যালগরিদমের নিজস্ব সীমাবদ্ধতা, বাস্তব প্রোগ্রামে যদি কেউ ম্যানুয়ালি প্রতিটি অভ্যন্তরীণ রেফারেন্সও ট্র্যাক করে রিলিজ করতে পারত তাহলে সমস্যা হতো না — কিন্তু বাস্তবে কোনো প্রোগ্রামার প্রতিটি সাইকেল হাতে ভেঙে দেওয়ার কথা মনে রাখতে পারে না, তাই একটি স্বয়ংক্রিয় সাইকেল-ডিটেক্টর প্রয়োজন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ M12 শেষ — পরের পাঠ থেকে M13 শুরু: বাস্তব ভাষার ডিজাইন সিদ্ধান্তের কেস স্টাডি।
- কোড অপ্টিমাইজেশন টেকনিক (L53) M12 · আগের পাঠ কোড জেনারেশনের পর, রানটাইমে যাওয়ার আগের শেষ ধাপ — কনস্ট্যান্ট ফোল্ডিং, ডেড কোড ও কমন সাবএক্সপ্রেশন এলিমিনেশন।
- কেস স্টাডি: Python-এর ডিজাইন সিদ্ধান্ত (L55) পরের পাঠ Python বাস্তবে কীভাবে রেফারেন্স কাউন্টিং ব্যবহার করে, এবং সাইকেল সমস্যার প্রকৃত সমাধান কী।