জাভা প্রোগ্রামিংয়ের জগতে মেমোরি ম্যানেজমেন্ট একটি স্বয়ংক্রিয় প্রক্রিয়া হলেও, এর সঠিক ব্যবহার ও কার্যপ্রণালী না বুঝলে অ্যাপ্লিকেশন ধীরগতির হয়ে পড়তে পারে অথবা মেমোরি ফুরিয়ে যাওয়ার সমস্যায় ভুগতে পারে। গার্বেজ কালেক্টর (Garbage Collector) এই জাদুকরী সুবিধার মূল চালিকাশক্তি, যা অব্যবহৃত অবজেক্ট খুঁজে বের করে মেমোরি পুনরুদ্ধার করে। এই টিউটোরিয়ালে আমরা জাভার বিভিন্ন গার্বেজ কালেক্টর, তাদের বৈশিষ্ট্য, কনফিগারেশন এবং মনিটরিং নিয়ে বিস্তারিত আলোচনা করব। আপনি শিখবেন কীভাবে আপনার অ্যাপ্লিকেশনের প্রয়োজনে সঠিক গার্বেজ কালেক্টর নির্বাচন করবেন এবং জিসি-জনিত সাধারণ সমস্যাগুলো সমাধান করবেন।

গার্বেজ সংগ্রহ কী এবং কেন এটি প্রয়োজন

জাভা ভাষায় তৈরি প্রতিটি অবজেক্ট হিপ (Heap) নামক মেমোরি অঞ্চলে সংরক্ষিত হয়। যখন কোনো অবজেক্ট আর কোনো লাইভ রেফারেন্স দ্বারা পয়েন্টেড থাকে না, তখন সেটি "গার্বেজ" বা আবর্জনা হিসেবে চিহ্নিত হয়। গার্বেজ কালেক্টরের কাজ হলো এই অপ্রয়োজনীয় অবজেক্টগুলো চিহ্নিত করে তাদের দখল করা মেমোরি ফিরিয়ে নেওয়া, যাতে নতুন অবজেক্ট সৃষ্টি নির্বিঘ্নে চলতে পারে। ম্যানুয়াল মেমোরি ম্যানেজমেন্টের (যেমন malloc/free) জটিলতা ও ত্রুটিপ্রবণতা দূর করাই হলো জাভার স্বয়ংক্রিয় গার্বেজ সংগ্রহের মূল উদ্দেশ্য।

জেভিএম (JVM) তার অন্তর্ভুক্ত গার্বেজ কালেক্টর বাস্তবায়নে বিভিন্ন অ্যালগরিদম ব্যবহার করে। প্রতিটি অ্যালগরিদমের পারফরম্যান্স বৈশিষ্ট্য আলাদা, যেমন থ্রুপুট (Throughput), বিরতির সময় (Pause Time) এবং রেসপন্সিভনেস। প্রাথমিক জাভা সংস্করণে কেবল সিরিয়াল কালেক্টর ছিল, কিন্তু বর্তমানে জি১ (G1), জেডজিসি (ZGC) এবং শেনানডোয়া (Shenandoah) এর মতো অত্যাধুনিক, কম-বিরতির কালেক্টর পাওয়া যায়। একেকটি কালেক্টর একেক ধরনের অ্যাপ্লিকেশন ও হার্ডওয়্যার পরিবেশে সেরা ফলাফল দেয়, তাই এদের কার্যপ্রণালী বোঝা দক্ষ ডেভেলপারের জন্য অপরিহার্য।

পূর্বশর্ত

এই টিউটোরিয়াল থেকে পূর্ণ সুবিধা পেতে নিম্নোক্ত বিষয়গুলোর সঙ্গে আপনার পরিচিতি থাকা দরকার:

  • জাভা প্রোগ্রামিংয়ের মৌলিক ধারণা (ক্লাস, অবজেক্ট, মেথড ইত্যাদি)।
  • জেডিকে (JDK) ইনস্টলেশন এবং কমান্ড লাইন থেকে জাভা প্রোগ্রাম চালানোর অভিজ্ঞতা।
  • জেভিএম-এর -Xmx, -Xms এর মতো মেমোরি অপশন সম্পর্কে ধারণা (আগের জ্ঞান না থাকলেও টিউটোরিয়ালটি বোঝা যাবে)।
  • একটি টেক্সট এডিটর বা আইডিই (IntelliJ, Eclipse, VS Code) এবং টার্মিনাল।

ধাপে ধাপে বাস্তবায়ন — গার্বেজ কালেক্টর নির্বাচন ও পর্যবেক্ষণ

এখন আমরা ধাপে ধাপে দেখব কীভাবে নির্দিষ্ট গার্বেজ কালেক্টর নির্বাচন করতে হয়, জেভিএম অপশনের মাধ্যমে সেটি কনফিগার করতে হয় এবং তার কাজের নমুনা সংগ্রহ করতে হয়।

১. উপলব্ধ গার্বেজ কালেক্টরগুলোর সংক্ষিপ্ত পরিচিতি

জাভা হটস্পট জেভিএমে কয়েকটি প্রধান গার্বেজ কালেক্টর রয়েছে। এগুলোর বৈশিষ্ট্য দ্রুত দেখে নেওয়া যাক:

  • Serial GC: একক থ্রেডে কাজ করে, ছোট অ্যাপ্লিকেশন ও সীমিত সিপিইউ কোরের জন্য উপযুক্ত। -XX:+UseSerialGC ফ্ল্যাগ দ্বারা সক্রিয় হয়।
  • Parallel GC: মাল্টি-থ্রেডেড, উচ্চ থ্রুপুট প্রদান করে। ব্যাচ প্রসেসিংয়ের জন্য ভালো। -XX:+UseParallelGC ফ্ল্যাগ।
  • CMS (Concurrent Mark Sweep): কম বিরতির জন্য ডিজাইন করা, কিন্তু জাভা ১৪ থেকে ডেপ্রিকেটেড এবং পরবর্তীতে অপসারিত। এর জায়গায় G1 বা অন্যান্য সুপারিশ করা হয়।
  • G1 GC: ডিফল্ট হিসেবে জাভা ৯ থেকে বর্তমান। বড় হিপ ও কম বিরতির ভারসাম্য রাখে। -XX:+UseG1GC
  • ZGC: অতি-নিম্ন বিরতি (Sub-millisecond), বড় হিপ (টেরাবাইট) সামলাতে সক্ষম। জাভা ১৫+ এ প্রোডাকশন রেডি। -XX:+UseZGC
  • Shenandoah GC: একইভাবে অতি-নিম্ন বিরতি, জেডজিসির বিকল্প। -XX:+UseShenandoahGC

২. প্রথম উদাহরণ — জিসি ফ্ল্যাগ সহ একটি প্রোগ্রাম চালানো

নিচের সরল জাভা প্রোগ্রামটি অনেক স্বল্প-মেয়াদী অবজেক্ট তৈরি করে, যাতে গার্বেজ কালেক্টরের কাজ পর্যবেক্ষণ করা যায়:

public class MemoryEater {
    public static void main(String[] args) {
        System.out.println("Memory eating started...");
        for (int i = 0; i < 100_000; i++) {
            byte[] chunk = new byte[1024]; // 1 KB array
            // chunk immediately eligible for GC after each iteration
        }
        System.out.println("Memory eating completed.");
    }
}

এই কোডটি প্রতিবার লুপের ভেতরে একটি কিলোবাইট আকারের বাইট অ্যারে তৈরি করছে, যা লুপ ইটারেশনের শেষেই অপ্রয়োজনীয় হয়ে যায়। ফলে গার্বেজ কালেক্টর বারবার কল হতে বাধ্য হয়। প্রোগ্রামটি কম্পাইল করতে কমান্ড চালান: javac MemoryEater.java

এখন আমরা একে বিভিন্ন গার্বেজ কালেক্টরের অধীনে চালিয়ে আউটপুট পর্যবেক্ষণ করব।

৩. জিসি লগ এনাবল করা এবং আউটপুট বিশ্লেষণ

জাভা ৯ পরবর্তী সংস্করণে ইউনিফায়েড জিসি লগিং ব্যবহৃত হয়। নিম্নরূপ অপশন দিয়ে প্রোগ্রাম চালান:

java -Xlog:gc*:gc.log -XX:+UseG1GC MemoryEater

এখানে -Xlog:gc*:gc.log বলছে সমস্ত জিসি সম্পর্কিত লগ gc.log ফাইলে সংরক্ষণ করতে। চাইলে সরাসরি কনসোলে দেখতেও পারেন -Xlog:gc* ব্যবহার করে।

উৎপন্ন লগ ফাইলের কিছু গুরুত্বপূর্ণ লাইন নিচের মত হতে পারে:

[0.123s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 5M->4M(64M) 1.234ms
[0.345s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 10M->6M(64M) 0.987ms
[0.567s][info][gc] GC(2) Pause Full (System.gc()) 15M->5M(64M) 3.210ms

প্রথম লাইনটির অর্থ: GC(0) প্রথম গার্বেজ সংগ্রহ ইভেন্ট, এটি একটি Young Generation পজ, যা G1 Evacuation Pause নামে পরিচিত। 5M->4M(64M) বলছে সংগ্রহের পূর্বে হিপের ব্যবহার ৫ মেগাবাইট, পরে ৪ মেগাবাইট (মোট হিপ ৬৪ মেগাবাইট)। শেষের 1.234ms হলো বিরতির সময়কাল।

এই লগ থেকে আমরা বুঝতে পারি যে G1 GC ছোট ছোট ইয়াং পজ নিয়ে কাজ করছে এবং বড় কোনো ফুল জিসি হচ্ছে না — যা কাঙ্ক্ষিত।

সম্পূর্ণ উদাহরণ — একটি মেমোরি ইনটেনসিভ অ্যাপ্লিকেশন ও জিসি বিশ্লেষণ

এবার আমরা একটি সামান্য জটিল উদাহরণ তৈরি করব, যেখানে কিছু অবজেক্ট দীর্ঘজীবী (Long-lived) এবং কিছু স্বল্পজীবী (Short-lived) থাকবে।

import java.util.ArrayList;
import java.util.List;

public class GCDemo {
    private static final int MB = 1024 * 1024;

    public static void main(String[] args) throws InterruptedException {
        List<byte[]> longLived = new ArrayList<>();

        // ১. কিছু দীর্ঘজীবী অবজেক্ট তৈরি
        for (int i = 0; i < 10; i++) {
            longLived.add(new byte[10 * MB]); // 10 MB array
            Thread.sleep(100);
        }

        // ২. প্রচুর স্বল্পজীবী অবজেক্ট তৈরি
        for (int i = 0; i < 1000; i++) {
            byte[] temp = new byte[1 * MB]; // 1 MB
            // temp অবজেক্ট এখানেই ডিরেফারেন্সড
            Thread.sleep(10);
        }

        System.out.println("Completed. Total long-lived objects: " + longLived.size());
    }
}

এই প্রোগ্রাম প্রথমে ১০টি ১০ মেগাবাইটের অ্যারে তৈরি করে তালিকায় রাখে (এগুলো দীর্ঘজীবী), তারপর ১০০০ বার ১ মেগাবাইটের অ্যারে তৈরি করে ফেলে দেয় (স্বল্পজীবী)। ফলে হিপে বড় আকারের ডেটা ওঠানামা করবে এবং বিভিন্ন জিসি ইভেন্ট ট্রিগার হবে।

এখন আমরা G1 এবং Parallel GC এর পার্থক্য দেখি:

# G1 GC সহ
java -Xms128m -Xmx256m -Xlog:gc*:g1_gc.log -XX:+UseG1GC GCDemo

# Parallel GC সহ
java -Xms128m -Xmx256m -Xlog:gc*:parallel_gc.log -XX:+UseParallelGC GCDemo

g1_gc.log এ দেখবেন Young Only পজ এবং কিছু Mixed পজ (যা ইয়াং ও কিছু ওল্ড জেনারেশন একত্রে ক্লিন করে)। parallel_gc.log এ দেখতে পাবেন Parallel Scavenge ও Parallel Compaction ইভেন্ট।

লগ ফাইল তুলনা করলে বোঝা যাবে যে G1 সাধারণত ছোট ছোট পজ দিয়ে হিপ ব্যবস্থাপনা করে, যেখানে Parallel GC বড় থ্রুপুটের বিনিময়ে দীর্ঘতর পজ নিতে পারে। কোনটি ভালো তা অ্যাপ্লিকেশনের প্রয়োজনের ওপর নির্ভর করে: জিইউআই অ্যাপে G1 ভালো, ব্যাচে Parallel ভালো।

সাধারণ সমস্যা ও সমাধান

গার্বেজ কালেক্টর সংক্রান্ত বাস্তব প্রকল্পে নানা সমস্যা দেখা যায়। এখানে কিছু প্রচলিত সমস্যা ও তাদের সমাধান দেওয়া হলো।

OutOfMemoryError: Java heap space

অনেক সময় হিপ পূর্ণ হয়ে গেলে এবং আর কোনো অবজেক্ট রিলিজ করতে না পারলে এই ত্রুটি আসে।

  • কারণ: মেমোরি লিক (যেমন সংগ্রাহক অবজেক্টে অপ্রয়োজনীয় রেফারেন্স রাখা), অপ্রতুল হিপ সাইজ।
  • সমাধান: হিপ ডাম্প (-XX:+HeapDumpOnOutOfMemoryError) নিয়ে Eclipse MAT বা VisualVM দিয়ে লিক বিশ্লেষণ করুন। প্রয়োজনে -Xmx বাড়ান; তবে লিক থাকলে কেবল হিপ বাড়ানো সাময়িক সমাধান।

দীর্ঘ GC Pause বা অ্যাপ্লিকেশন জমে যাওয়া

GC চলাকালে ‘Stop-The-World’ পজ অ্যাপ্লিকেশন থামিয়ে দেয়। যদি পজ বড় হয় তবে ব্যবহারকারী ইন্টারফেস বা রেসপন্স টাইম ক্ষতিগ্রস্ত হয়।

  • কারণ: ভুল GC নির্বাচন, খুব বড় হিপে Full GC ঘন ঘন হওয়া।
  • সমাধান: G1 বা ZGC/Shenandoah ব্যবহার করুন; -XX:MaxGCPauseMillis (G1-র জন্য) দিয়ে টার্গেট পজ সেট করুন; হিপের ফ্র্যাগমেন্টেশন কমানোর জন্য -XX:G1HeapRegionSize ইত্যাদি টিউন করুন।

জিসি লগের অপচয় বা পর্যবেক্ষণের অভাব

প্রোডাকশনে দীর্ঘদিন চলার পর পারফরমেন্স তলানিতে ঠেকে — কারণ ছাড়া।

  • সমাধান: প্রোডাকশনে অন্তত -Xlog:gc*:file=/var/log/java/gc.log::filecount=10,filesize=10M দিয়ে জিসি লগ এনাবল করুন। রেগুলার বিশ্লেষণের মাধ্যমে সমস্যা আগে থেকেই ধরুন।

ভুল জিসি কনফিগারেশন

ডিফল্ট সেটিং অনেক ক্ষেত্রে যথেষ্ট, কিন্তু বিশেষায়িত অ্যাপ্লিকেশন (হাই-ফ্রিকোয়েন্সি ট্রেডিং, বিগ ডেটা) কাস্টম টিউনিং চায়।

  • সমাধান: নিজস্ব ওয়ার্কলোডে বিভিন্ন GC পরীক্ষা করুন। -XX:+PrintFlagsFinal দিয়ে চলতি ফ্ল্যাগ দেখুন ও প্রয়োজনীয় পরিবর্তন করুন। জিসি বিশেষজ্ঞ গাইডলাইন ফলো করুন।

সারসংক্ষেপ

জাভার গার্বেজ কালেক্টর হলো অত্যন্ত শক্তিশালী একটি স্বয়ংক্রিয় মেমোরি ব্যবস্থাপনা উপাদান, যা ডেভেলপারকে ম্যানুয়াল মেমোরি ম্যানেজমেন্টের যন্ত্রণা থেকে মুক্তি দেয়। সঠিক জিসি নির্বাচন ও কনফিগারেশন অ্যাপ্লিকেশনের পারফরম্যান্স, রেসপন্স টাইম এবং স্থিতিশীলতায় সরাসরি প্রভাব ফেলে। এই টিউটোরিয়ালে আমরা জাভার প্রধান গার্বেজ কালেক্টরগুলো চিহ্নিত করেছি, লগিং ও মনিটরিংয়ের মাধ্যমে এদের আচরণ পর্যবেক্ষণ করতে শিখেছি এবং বাস্তব পরিস্থিতির উদাহরণ দিয়েছি।

মনে রাখার মূল বিষয়: “একটি জিসি সবার জন্য শ্রেষ্ঠ নয়” — আপনার অ্যাপ্লিকেশনের চাহিদা বুঝে পরীক্ষা-নিরীক্ষা চালিয়ে যাওয়াই কাঙ্ক্ষিত ফলাফল আনবে। জিসি সংক্রান্ত সমস্যা সমাধানে ধৈর্য ও বিশ্লেষণী দৃষ্টিভঙ্গি বজায় রাখুন, এবং জেভিএমের দেওয়া শক্তিশালী টুলিং সুবিধাকে কাজে লাগান।

Share