এই পর্বে যা শিখবেন
Main.javaথেকে স্ক্রিনের আউটপুট পর্যন্ত প্রতিটি ধাপ — কে কী করে, কখন করেjavap -cদিয়ে নিজের চোখে bytecode পড়া, আর JVM কীভাবে stack ব্যবহার করে হিসাব করে- Interpreter, JIT আর tiered compilation — Java প্রোগ্রাম কেন “গরম” হতে সময় নেয়
- Stack, heap আর reference-এর মানসিক ছবি, যার ওপর পুরো কোর্স দাঁড়িয়ে আছে
- Garbage collection-এর মূল ধারণা, আর AI-এর দেওয়া পুরোনো JVM-তথ্য কীভাবে যাচাই করবেন
১ · Java-র অনুবাদ হয় দুইবার
পর্ব ০৪-এ আপনি দুটি কমান্ড চালিয়েছেন — javac Main.java, তারপর java Main. প্রশ্ন হলো, একটি প্রোগ্রাম চালাতে দুটি কমান্ড কেন? C-তে তো gcc একবার চালালেই চালানো যায় এমন ফাইল পাওয়া যায়। আর Python-এ একটিমাত্র কমান্ড। Java মাঝখানে কেন দাঁড়িয়ে?
উত্তরটি এই পর্বের কেন্দ্রীয় ধারণা। Java আপনার কোডকে সরাসরি আপনার CPU-র ভাষায় অনুবাদ করে না। প্রথম অনুবাদক javac কোডটিকে অনুবাদ করে এমন এক কম্পিউটারের ভাষায়, যার আসলে কোনো শারীরিক অস্তিত্ব নেই। সেই কাল্পনিক কম্পিউটারের নাম Java Virtual Machine, সংক্ষেপে JVM. আর তার ভাষার নাম bytecode.
তারপর আপনার কম্পিউটারে একটি আসল প্রোগ্রাম — java — সেই কাল্পনিক কম্পিউটারের ভূমিকায় অভিনয় করে। সে bytecode পড়ে, বোঝে, আর আপনার আসল CPU-কে দিয়ে কাজটি করিয়ে নেয়। অর্থাৎ অনুবাদ হয় দুইবার: একবার আগে (source → bytecode), আরেকবার চলার সময় (bytecode → আসল machine code).
দুই অনুবাদকের গল্প
Compile once to bytecode, translate again at run time
- আপনার কোড
Main.javaমানুষ পড়ে - javac
Main.classকাল্পনিক কম্পিউটারের নির্দেশ - JVM (java)
bytecode → CPUচলার সময় অনুবাদ - আপনার CPU
x86 / ARMআসল কাজ
.class ফাইলটি কোনো নির্দিষ্ট CPU-র জন্য নয়।ধরুন একজন বাংলাদেশি লেখক একটি বই লিখলেন, যা দশটি দেশে পড়া হবে। দশটি ভাষায় দশবার অনুবাদ না করে প্রকাশক বইটি একবার ইংরেজিতে অনুবাদ করালেন — যে ভাষা প্রতিটি দেশের একজন দোভাষী জানেন। এরপর প্রতিটি দেশে সেই দোভাষী ইংরেজি থেকে স্থানীয় ভাষায় পড়ে শোনান। এখানে ইংরেজি সংস্করণটি হলো bytecode, আর প্রতিটি দেশের দোভাষী হলো সেই দেশের (OS ও CPU-র) জন্য বানানো JVM.
JVM একটি স্পেসিফিকেশন, আবার একটি প্রোগ্রামও। “Java Virtual Machine Specification” নামের নথিতে লেখা আছে কাল্পনিক কম্পিউটারটি কোন নির্দেশ বোঝে, .class ফাইল দেখতে কেমন, কোন নিয়মে যাচাই হবে। এই নিয়ম মেনে যে কেউ JVM বানাতে পারে। আপনি সাধারণত যেটি ব্যবহার করেন, সেটি OpenJDK-র HotSpot JVM — Oracle JDK, Eclipse Temurin, Amazon Corretto, Microsoft Build of OpenJDK সবই মূলত একই HotSpot কোড থেকে তৈরি।
১৯৯১ সালে Sun Microsystems-এ James Gosling-এর দল টিভি সেট-টপ বক্সের মতো যন্ত্রের জন্য একটি ভাষা বানানো শুরু করে, প্রথম নাম ছিল Oak. সমস্যা ছিল, এসব যন্ত্রে নানা কোম্পানির নানা CPU থাকে — প্রতিটির জন্য আলাদা compile করা অসম্ভব। তাই তাঁরা মাঝখানে একটি কাল্পনিক যন্ত্র বসালেন। ১৯৯৫ সালে Java নামে ভাষাটি প্রকাশিত হয়, সাথে Sun-এর বিখ্যাত স্লোগান: “Write once, run anywhere” — একবার লিখুন, সব জায়গায় চালান। মাঝখানের কাল্পনিক যন্ত্রের ধারণা নতুন ছিল না (১৯৭০-এর দশকে Pascal-এর p-code একই চিন্তা), কিন্তু Java সেটিকে কোটি মানুষের হাতে পৌঁছে দেয়।
২ · Source থেকে আউটপুট — পুরো পাইপলাইন
এবার পুরো যাত্রাটি ধাপে ধাপে দেখি। নিচের ছোট প্রোগ্রামটি দুটি সংখ্যা যোগ করে ফল ছাপে। “পরে” চাপুন, আর প্রতিটি ধাপে ভেতরের জিনিস কীভাবে বদলায়, খেয়াল করুন:
- Source
Main.javaআপনার লেখা - javac
Main.classbytecode - Loader + Verifier
java Mainআনা ও যাচাই - Interpreter
এক নির্দেশ করেসাথে সাথে শুরু - JIT
C1 / C2গরম অংশ machine code - আউটপুট
5স্ক্রিনে
public class Main {
public static void main(String[] args) {
int total = add(2, 3);
System.out.println(total);
}
static int add(int a, int b) {
return a + b;
}
}
CA FE BA BE 00 00 00 41 ... ↑ CAFEBABE = “এটি একটি class ফাইল” চিহ্ন ↑ 0x41 = 65 = Java 21-এর class ফাইল version ... তারপর constant pool, method আর তাদের bytecode
Main ← application class loader আনল java.lang.String, System, PrintStream ... ← bootstrap loader (JDK-র নিজের class) verify Main: stack ঠিক আছে? type মিলছে? ✔
iconst_2 // 2 রাখো iconst_3 // 3 রাখো invokestatic add // add(2, 3) ডাকো istore_1 // ফল total-এ রাখো getstatic System.out iload_1 invokevirtual println // ছাপো return
add() ডাকা হয়েছে মাত্র ১ বার → JIT দরকার নেই (একই add() ১০,০০০ বার ডাকলে:) add() → x86/ARM machine code lea eax, [rsi+rdx] ; উদাহরণ মাত্র ret
$ javac Main.java
$ java Main
5
- ধাপ ১ — Source. আপনি
Main.javaফাইলে Java কোড লিখলেন। এটি মানুষের পড়ার জন্য লেখা; CPU এর একটি অক্ষরও বোঝে না। - ধাপ ২ — javac: bytecode তৈরি।
javacপুরো ফাইল পড়ে, ব্যাকরণ আর type যাচাই করে (int-এ লেখা ঢোকালে এখানেই error), তারপর প্রতিটি class-এর জন্য একটি class file বানায়। প্রতিটি class ফাইল শুরু হয় চারটি বিখ্যাত byte দিয়ে:CA FE BA BE. এরপর থাকে version নম্বর — Java 21-এর জন্য 65. পুরোনো JVM নতুন version-এর ফাইল পেলে চালাতে অস্বীকার করে। - ধাপ ৩ — Class loader আর verifier.
java Mainলিখলে JVM চালু হয়। Class loaderMain.classখুঁজে মেমরিতে আনে;String,System-এর মতো JDK-র class আনে আরেকটি loader. তারপর bytecode verifier দেখে নেয় ফাইলটি নিয়ম ভাঙছে কি না — কেউ হাতে বানানো ক্ষতিকর ফাইল দিলেও যেন JVM ক্র্যাশ না করে। - ধাপ ৪ — Interpreter চালাতে শুরু করে। JVM সঙ্গে সঙ্গে কাজ শুরু করে: interpreter একটি একটি bytecode নির্দেশ পড়ে আর সেই অনুযায়ী কাজ করে। অনুবাদের জন্য আগে অপেক্ষা করতে হয় না, তাই শুরু দ্রুত — কিন্তু প্রতিটি নির্দেশ পড়ে-বুঝে চালানোয় চলা তুলনামূলক ধীর।
- ধাপ ৫ — JIT: গরম অংশ machine code-এ। কোনো method বারবার চললে JVM সেটিকে “গরম” (hot) ধরে নেয়, আর JIT compiler সেটিকে আপনার CPU-র আসল machine code-এ অনুবাদ করে। এই ছোট প্রোগ্রামে
addমাত্র একবার চলে, তাই JIT-এর দরকারই পড়ে না। কিন্তু সার্ভারে লাখবার চলা কোডের জন্য এটাই আসল গতির উৎস। - ধাপ ৬ — আউটপুট।
printlnশেষ পর্যন্ত অপারেটিং সিস্টেমকে বলে স্ক্রিনে5লিখতে।mainশেষ হলে JVM বন্ধ হয়, আর তার সব মেমরি OS ফেরত নেয়। এক সেকেন্ডেরও কম সময়ে এত কিছু!
→ class loader → verifier → interpreter → JIT → machine code
| অংশ | কখন কাজ করে | ভুল হলে যা দেখবেন |
|---|---|---|
javac | চালানোর আগে, একবার | error: ';' expected, incompatible types |
| Class loader | কোনো class প্রথমবার দরকার হলে | ClassNotFoundException, NoClassDefFoundError |
| Verifier | class আনার পর, চালানোর আগে | VerifyError (সাধারণ কোডে প্রায় কখনো না) |
| Interpreter / JIT | প্রোগ্রাম চলার পুরো সময় | runtime exception, যেমন NullPointerException |
| Garbage collector | পেছনে, দরকার মতো | OutOfMemoryError, লম্বা pause |
Java 11 থেকে ছোট প্রোগ্রামের জন্য একটি শর্টকাট আছে: java Main.java — এক কমান্ডেই চলে। কিন্তু ভেতরে ঘটনা একই: JVM আগে মেমরিতে compile করে নেয়, তারপর bytecode চালায়। ধাপগুলো লুকানো থাকে, বাদ যায় না।
৩ · Bytecode নিজের চোখে দেখুন — javap
Bytecode কোনো রহস্য নয়। JDK-র সাথেই একটি টুল আসে, javap, যা .class ফাইল খুলে মানুষ পড়তে পারে এমনভাবে দেখায়। ধরুন Calc.java ফাইলে এই ছোট class লিখলেন:
class Calc {
static int add(int a, int b) {
return a + b;
}
}এবার terminal-এ compile করে javap -c চালান (-c মানে “method-এর bytecode-ও দেখাও”):
javac Calc.java
javap -c CalcJDK 21-এ সাধারণত এমন আউটপুট আসে:
Compiled from "Calc.java"
class Calc {
Calc();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
}
কয়েকটি জিনিস খেয়াল করুন। প্রথমত, আপনি কোনো constructor লেখেননি, তবু Calc() আছে — javac নিজে একটি default constructor যোগ করেছে, যা Object-এর constructor ডাকে (পর্ব ১৬-এ বিস্তারিত)। দ্বিতীয়ত, বাঁ পাশের সংখ্যাগুলো (0, 1, 2, 3) byte-এর অবস্থান। প্রতিটি নির্দেশের নাম এক byte-এর একটি সংকেত, তাই নাম bytecode. তৃতীয়ত, নির্দেশের নামেই type লেখা: iload, iadd, ireturn-এর i মানে int. long হলে দেখতেন lload, ladd; double হলে dadd.
JVM হিসাব করে একটি stack দিয়ে
আসল CPU-তে register থাকে — ছোট ছোট দ্রুত ঘর। JVM-এর কাল্পনিক কম্পিউটারে register নেই; আছে প্রতিটি method কলের জন্য একটি operand stack — যেন টেবিলে একটার ওপর একটা প্লেট রাখা। নতুন মান যায় ওপরে, কাজের সময় ওপর থেকে তোলা হয়। add(2, 3) ডাকলে কী হয়, দেখুন:
0iload_01iload_12iadd3ireturn
| local 0 (a) | |
| local 1 (b) | |
| operand stack |
iload_0— method ডাকার সময় দুটি argument জমা হয়েছিল local variable-এর ঘরে: ঘর ০-এa= 2, ঘর ১-এb= 3. এই নির্দেশ ঘর ০-এরintমানটি নিয়ে stack-এর ওপরে রাখল।iload_1— ঘর ১ থেকে 3 এনে আগের প্লেটের ওপরে রাখল। Stack-এ এখন 2-এর ওপর 3.iadd— ওপরের দুটিintতুলে নিল, যোগ করল, ফল 5 আবার stack-এ রাখল। লক্ষ করুন,iaddজানে দুটিইint— চলার সময় type খুঁজতে হয় না। Python-এর VM-কে প্রতিবার দেখতে হয় কোন type, Java-র bytecode-এ সেটা আগেই লেখা থাকে। এটি Java দ্রুত হওয়ার একটি কারণ।ireturn— ওপরের 5 তুলে যে জায়গা থেকেaddডাকা হয়েছিল, সেখানে ফেরত দিল।add-এর frame (তার local ঘর আর stack) মুছে গেল। প্রতিটি method কল এভাবে একটি নতুন frame পায়, শেষ হলে frame সরে যায় — এই frame-এর স্তূপই পরে দেখা “stack”.
Bytecode পড়তে পারা প্রতিদিনের কাজ নয়। তবে মাঝে মাঝে এটি খুব কাজে আসে — যেমন s1 + s2 দিয়ে String জোড়া ভেতরে আসলে কী হয়, বা একটি lambda কী রূপ নেয়, তা javap -c দিয়ে সরাসরি দেখা যায়। কেউ যখন বলে “এটা দ্রুত, ওটা ধীর”, bytecode দেখে নিজেই যাচাই করতে পারেন।
৪ · এক bytecode, সব জায়গায় — portability
এখন বোঝা সহজ, কেন একই Main.class Windows ল্যাপটপে, Linux সার্ভারে আর Mac-এ বদল ছাড়াই চলে। Bytecode কোনো নির্দিষ্ট CPU বা OS-এর জন্য নয় — তা JVM-এর জন্য। আর প্রতিটি প্ল্যাটফর্মের জন্য আলাদা JVM আগে থেকেই বানানো আছে। প্ল্যাটফর্মের সব ঝামেলা JVM-এর নির্মাতারা একবার সামলান; আপনাকে আর সামলাতে হয় না।
🧱 C/C++-এর পথ
প্রতিটি OS ও CPU-র জন্য আলাদা compile. Windows-এর .exe Linux-এ চলে না। বদলে পাওয়া যায় সরাসরি machine code — শুরু থেকেই দ্রুত।
☕ Java-র পথ
একবার compile, এক .class (বা .jar) সব জায়গায়। শর্ত: সেই মেশিনে উপযুক্ত version-এর JVM থাকতে হবে।
একটি সতর্কতা: “সব জায়গায় চলে” মানে bytecode বহনযোগ্য, কিন্তু আপনার কোডের সব আচরণ নয়। যেমন ফাইলের path-এ Windows ব্যবহার করে \, Linux ব্যবহার করে /; কোডে হাতে C:\data লিখলে Linux-এ কাজ করবে না। Java এসব সামলানোর উপায় দেয় (পর্ব ৩৭-এ java.nio.file), কিন্তু ব্যবহার করার দায়িত্ব আপনার।
Android-এর ভিন্ন পথ — DEX আর ART
Android অ্যাপ Java বা Kotlin-এ লেখা হয়, কিন্তু ফোনে কোনো HotSpot JVM চলে না। অ্যাপ build করার সময় আগে সাধারণ .class ফাইল তৈরি হয়, তারপর d8 নামের একটি টুল সেগুলোকে DEX (Dalvik Executable) ফরম্যাটে বদলে দেয় — ফোনের সীমিত মেমরির কথা ভেবে বানানো আরেক ধরনের bytecode. শুরুর দিকে এটি চালাত Dalvik নামের VM; Android 5.0 থেকে তার জায়গা নিয়েছে ART (Android Runtime). ART অ্যাপ ইনস্টল ও ব্যবহারের সময় কোডের বড় অংশ আগেভাগে machine code-এ compile করে রাখে, আবার দরকারে JIT-ও ব্যবহার করে। ভাষা একই, ধারণাও একই — মাঝখানে একটি virtual machine — কিন্তু bytecode আর runtime আলাদা।
একই JVM, অনেক ভাষা
JVM আসলে Java ভাষাকে চেনে না — সে চেনে শুধু class ফাইল। তাই যে ভাষার compiler সঠিক class ফাইল বানাতে পারে, সেই ভাষাই JVM-এ চলে, আর Java-র সব library ব্যবহার করতে পারে। Kotlin (Android-এর জন্য Google-এর পছন্দের ভাষা), Scala (বড় ডেটা প্রক্রিয়াকরণে জনপ্রিয়), Clojure (Lisp-ঘরানার ভাষা) আর Groovy (Gradle build script-এ দেখবেন, পর্ব ৪৬) — সবই একই JVM-এ চলে। একটি Kotlin class একটি Java class-কে সরাসরি ডাকতে পারে, কারণ নিচে দুটিই bytecode.
আপনার কোড এখন কোন JVM-এ চলছে, তা প্রোগ্রাম নিজেই জিজ্ঞেস করতে পারে। নিচের কোডটি চালান — এটি যে JDK-তে চলবে, তার তথ্যই ছাপবে। এই পাতার রানার Java 21 ব্যবহার করে, তাই সেখানে 21 আসবে; আপনার কম্পিউটারে অন্য version থাকলে সংখ্যা বদলাবে:
class Main {
public static void main(String[] args) {
// যে JVM কোডটি চালাচ্ছে, তার নিজের দেওয়া তথ্য
System.out.println("Java version : " + System.getProperty("java.specification.version"));
System.out.println("Runtime.version: " + Runtime.version().feature());
System.out.println("Class file ver: " + System.getProperty("java.class.version"));
System.out.println("JVM name : " + System.getProperty("java.vm.name"));
// কোন class কোন loader আনল?
System.out.println("String-এর loader : " + String.class.getClassLoader());
System.out.println("platform loader : " + ClassLoader.getPlatformClassLoader().getName());
System.out.println("app loader : " + ClassLoader.getSystemClassLoader().getName());
}
}Java 21-এর রানারে প্রথম দুই লাইনে 21 আসে, তৃতীয় লাইনে 65.0 — উপরের stepper-এ দেখা class ফাইলের version 65 এটাই। চতুর্থ লাইনে JVM-এর নাম, সাধারণত OpenJDK 64-Bit Server VM — অর্থাৎ HotSpot. String-এর loader ছাপে null: এর মানে কোনো ভুল নয়, বরং JDK-র মূল class-গুলো আনে bootstrap loader, যা JVM-এর ভেতরে লেখা এবং Java object হিসেবে তার অস্তিত্বই নেই। বাকি দুটি loader-এর নাম platform আর app — পরের অংশে এদের কাজ দেখবেন। (নিজের প্রজেক্টে main class সাধারণত public class Main, ফাইল Main.java — এই পাতার রানারের জন্য public বাদ রাখা হয়েছে।)
C/C++ সরাসরি একটি নির্দিষ্ট CPU-র machine code বানায় — Java-র মতো মাঝের bytecode নেই, চলার সময় অনুবাদও নেই (তুলনার জন্য C টুলচেইন পর্ব)। Python (CPython) Java-র মতোই আগে bytecode বানায়, কিন্তু সাধারণত সেটি interpreter দিয়েই চালায়; HotSpot-এর মতো পরিণত JIT সেখানে ডিফল্টভাবে নেই, আর bytecode-এ type লেখা থাকে না (Python কীভাবে চলে)। মেমরির দিক থেকে: C-তে malloc/free, C++-এ new/delete বা smart pointer (C++ dynamic memory পর্ব); Java-তে শুধু new, মুছে দেয় garbage collector.
৫ · Class loader আর verifier — দরজার পাহারাদার
JVM চালু হলে সব class একসাথে মেমরিতে আনে না। আনে যখন প্রথম দরকার পড়ে তখন — একে বলে lazy loading. আপনার প্রোগ্রাম হয়তো এমন একটি class ব্যবহার করে যা শুধু কোনো বিরল error হলে লাগে; সেই error না ঘটলে class-টি কখনো লোডই হয় না। এতে JVM দ্রুত চালু হয়, মেমরিও বাঁচে।
Class আনার কাজ তিন স্তরের loader ভাগ করে নেয়। কেউ কোনো class চাইলে loader আগে তার ওপরের স্তরকে জিজ্ঞেস করে, “এই class কি আগে থেকেই আছে?” — ওপরে না পেলে তবেই নিজে খোঁজে। এতে কেউ নিজের বানানো নকল java.lang.String ঢুকিয়ে দিতে পারে না, কারণ আসলটি সবসময় আগে bootstrap loader থেকে আসে।
| Loader | কোন class আনে | উদাহরণ |
|---|---|---|
| Bootstrap | JDK-র একেবারে মূল class (JVM-এর ভেতরে লেখা) | java.lang.String, java.lang.Object |
| Platform | JDK-র বাকি কিছু module (Java 9-এর আগে নাম ছিল extension loader) | java.sql module-এর class |
| Application (app) | আপনার class আর classpath-এ রাখা library | Main, Spring বা Jackson-এর class |
লোড হওয়ার পর আসে linking — যার প্রথম কাজ যাচাই। Verifier কিছু প্রশ্নের উত্তর না পেলে class চালাতেই দেয় না:
- প্রতিটি নির্দেশ সঠিক type-এর মান পাচ্ছে কি? (
iadd-কে দুটিintদিতে হবে, একটি String নয়।) - Operand stack কখনো খালি অবস্থায় তোলা হচ্ছে, বা সীমার বেশি ভরে যাচ্ছে কি?
- কোনো jump নির্দেশ method-এর বাইরে বা কোনো নির্দেশের মাঝখানে লাফ দিচ্ছে কি?
privateজিনিসে অন্য class হাত দিচ্ছে কি? কোনো সংখ্যাকে জোর করে reference বানানো হচ্ছে কি?
প্রশ্ন জাগতে পারে — javac তো আগেই সব যাচাই করেছে, আবার কেন? কারণ JVM জানে না class ফাইলটি কে বানিয়েছে। হয়তো javac, হয়তো Kotlin compiler, হয়তো কোনো library যা চলার সময় bytecode তৈরি করে, আবার হয়তো কেউ ইচ্ছা করে হাতে বানানো ক্ষতিকর ফাইল। JVM কাউকে অন্ধভাবে বিশ্বাস করে না। যাচাইয়ের পর class-এর static ঘরগুলো তৈরি হয় এবং initialization হয় — static ভ্যারিয়েবলে প্রাথমিক মান বসে (পর্ব ১৮-এ বিস্তারিত)।
ClassNotFoundException বা NoClassDefFoundError দেখলে সমস্যাটি সাধারণত আপনার কোডের যুক্তিতে নয় — class loader ফাইলটি খুঁজে পায়নি। প্রথমে দেখুন classpath ঠিক আছে কি না, প্রয়োজনীয় .jar যোগ করা হয়েছে কি না, প্যাকেজের নাম আর ফোল্ডারের গঠন মিলছে কি না। Maven/Gradle এসব হিসাব রাখার কাজ সহজ করে (পর্ব ৪৬)।
৬ · Interpreter থেকে JIT — Java কেন চলতে চলতে দ্রুত হয়
Interpreter-এর সুবিধা হলো সঙ্গে সঙ্গে শুরু। অসুবিধা — প্রতিটি নির্দেশ প্রতিবার পড়তে, বুঝতে, তারপর করতে হয়। একটি লুপ দশ লাখবার ঘুরলে একই নির্দেশ দশ লাখবার “বোঝা” হয়। Java-র প্রথম দিকের version (১৯৯০-এর দশকের মাঝামাঝি) মূলত এভাবেই চলত, আর “Java ধীর” খ্যাতিটা তখনই তৈরি।
একটি গার্মেন্টস কারখানার ফ্লোর ভাবুন। নতুন একটি ডিজাইন এলে একজন অভিজ্ঞ কর্মী নির্দেশপত্র দেখে দেখে একটি একটি করে শার্ট বানান — প্রতিটি ধাপে কাগজ পড়েন, তারপর সেলাই করেন। এটি interpreter: সঙ্গে সঙ্গে কাজ শুরু, কিন্তু ধীর। ফ্লোর ম্যানেজার খেয়াল রাখেন কোন ডিজাইনের অর্ডার বারবার আসছে। দশ হাজার পিসের অর্ডার এলে তিনি সেই ডিজাইনের জন্য আলাদা লাইন সাজান — বিশেষ জিগ, আগে থেকে কাটা প্যাটার্ন, প্রতিজনের একটি নির্দিষ্ট কাজ। লাইন সাজাতে সময় লাগে, কিন্তু একবার সাজালে প্রতি পিস অনেক দ্রুত। এটি JIT. আর যে ডিজাইনের অর্ডার মাত্র দুই পিস, তার জন্য লাইন সাজানো সময়ের অপচয় — তাই সব কোড JIT হয় না, শুধু গরম অংশ।
HotSpot নামটিই এসেছে এই চিন্তা থেকে: প্রোগ্রামের বেশিরভাগ সময় খরচ হয় অল্প কিছু “hot spot”-এ। JVM প্রতিটি method কতবার ডাকা হলো আর প্রতিটি লুপ কতবার ঘুরল, গুনে রাখে। সংখ্যা একটি সীমা পেরোলে সেই method-কে compile করতে পাঠায় — পেছনের আলাদা thread-এ, যাতে প্রোগ্রাম থেমে না থাকে।
HotSpot-এ দুটি JIT compiler আছে। C1 (আগে নাম ছিল “client compiler”) দ্রুত compile করে, অপ্টিমাইজেশন মাঝারি। C2 (“server compiler”) সময় বেশি নেয়, কিন্তু অনেক গভীর অপ্টিমাইজেশন করে। দুটিকে ধাপে ধাপে ব্যবহার করার নিয়মকে বলে tiered compilation, যা Java 8 থেকে ডিফল্ট। একটি method ১০,০০০ বার ডাকা হলে তার যাত্রাটা দেখুন:
- Interpreter
tier 0পড়ে পড়ে চালায়, গোনে - C1
tier 3দ্রুত compile + তথ্য সংগ্রহ - C2
tier 4গভীর অপ্টিমাইজেশন
price(item)-এর অবস্থা (কাল্পনিক সংখ্যা)| কতবার ডাকা হলো | |
| কে চালাচ্ছে | |
| প্রতি কলের সময় (তুলনামূলক) |
- কল ১ — interpreter. প্রথমবার
priceচলল interpreter-এ, tier 0. সাথে সাথে চলল, কোনো অপেক্ষা নেই। JVM একটি গণনা-ঘরে লিখে রাখল: “১ বার ডাকা হয়েছে।” - প্রায় ২০০ কল — C1-কে পাঠানো হলো। Method-টি বারবার আসছে। JVM সেটিকে C1-এর কাছে compile করতে পাঠাল, পেছনের একটি compiler thread-এ। যতক্ষণ compile চলছে, interpreter আগের মতোই চালিয়ে যাচ্ছে — প্রোগ্রাম থামে না। (আসল সীমা JVM-এর ভেতরের সেটিং আর লুপের সংখ্যার ওপর নির্ভর করে; এখানে সংখ্যাগুলো শুধু ধারণা দেওয়ার জন্য।)
- C1-এর কোড চলছে, সাথে তথ্য সংগ্রহ। এখন
priceচলছে আসল machine code-এ, interpreter-এর চেয়ে কয়েক গুণ দ্রুত। এই কোডের ভেতরে C1 ছোট ছোট গণনা বসিয়ে রেখেছে: কোনif-এর কোন দিকে বেশি যাওয়া হয়,itemআসলে কোন class-এর object হয়। একে বলে profiling. - প্রায় ৫,০০০ কল — C2. যথেষ্ট তথ্য জমেছে। JVM method-টি C2-কে দিল। C2 সেই তথ্য দেখে সাহসী অপ্টিমাইজেশন করে: ছোট method-কে ডাকার বদলে সরাসরি ভেতরে বসিয়ে দেয় (inlining), কখনো না যাওয়া শাখা বাদ দেয়, লুপ থেকে অপ্রয়োজনীয় হিসাব বের করে আনে। এটি tier 4 — সবচেয়ে দ্রুত।
- ১০,০০০ কল — স্থির অবস্থা। এখন প্রতিটি কল চলে C2-এর কোডে। প্রথম কলের তুলনায় হয়তো বহু গুণ দ্রুত। এই অবস্থায় পৌঁছানোর সময়টুকুকে বলে warm-up. সার্ভারে Java অ্যাপ চালু হওয়ার প্রথম কয়েক সেকেন্ড বা মিনিট তুলনামূলক ধীর থাকে এই কারণেই।
- যদি অনুমান ভুল প্রমাণিত হয় — deoptimization. ধরুন C2 দেখেছিল
itemসবসময়Book, তাই শুধুBook-এর জন্য কোড বানিয়েছিল। এখন হঠাৎ একটিPenএল। C2-এর কোড আর নিরাপদ নয়, তাই JVM সেটি বাতিল করে আবার interpreter-এ ফিরে যায় — একে বলে deoptimization. নতুন তথ্য নিয়ে পরে আবার compile হয়। ভুল ফল কখনো আসে না; শুধু কিছুক্ষণ ধীর।
আগেভাগে compile করা C++ compiler জানে না প্রোগ্রাম চলার সময় কোন ডেটা আসবে। JIT জানে — কারণ সে চলমান প্রোগ্রাম দেখে compile করে। এজন্য দীর্ঘ সময় চলা সার্ভার প্রোগ্রামে Java অনেক ক্ষেত্রে C++-এর কাছাকাছি গতি পায়। দামটা হলো শুরুর ধীরতা, আর compile করার জন্য বাড়তি CPU ও মেমরি।
এই কারণে Java কোডের গতি “একবার চালিয়ে সময় মেপে” বিচার করা যায় না। প্রথম কয়েক হাজার কল interpreter আর C1-এ চলে, আর মাপার ফল ভুল দিকে নিয়ে যায়। পেশাদাররা এজন্য JMH (Java Microbenchmark Harness) ব্যবহার করেন, যা আগে warm-up করিয়ে তারপর মাপে। AI যদি System.currentTimeMillis() দিয়ে একবার মেপে “এটা দ্রুত” বলে, সন্দেহ করুন।
শুরুর সময়ের সমস্যা — native image আর AOT
সার্ভারের জন্য warm-up তেমন সমস্যা নয় — সার্ভার চলে দিনের পর দিন। কিন্তু কিছু জায়গায় শুরুটাই সব: একটি command-line টুল যা আধা সেকেন্ড চলে, বা cloud-এর একটি function যা অনুরোধ এলে চালু হয়ে কয়েক মিলিসেকেন্ডে কাজ শেষ করে। সেখানে JVM চালু হওয়া, class লোড আর warm-up-এর সময়টাই বড় খরচ।
এর একটি সমাধান হলো AOT (ahead-of-time) compilation — চালানোর আগেই পুরো প্রোগ্রাম machine code-এ বানিয়ে রাখা। GraalVM-এর native-image টুল ঠিক এটি করে: আপনার অ্যাপ আর দরকারি JDK অংশ মিলিয়ে একটি স্বতন্ত্র চালানো যায় এমন ফাইল বানায়, যা মিলিসেকেন্ডে চালু হয় আর কম মেমরি নেয়। বিনিময়ে দিতে হয় কিছু: build অনেক ধীর, প্রতিটি OS-এর জন্য আলাদা build (C-র মতোই), আর চলার সময় নতুন class লোড বা reflection-এর মতো কাজের জন্য আগে থেকে জানিয়ে রাখতে হয়। দীর্ঘ সময় চলা অ্যাপে JIT-এর প্রোফাইল-নির্ভর অপ্টিমাইজেশনও হারায়। OpenJDK নিজেও Project Leyden নামে শুরুর সময় কমানোর কাজ করছে। এখনকার জন্য এটুকু জানলেই যথেষ্ট: JIT = ধীর শুরু, দ্রুত দীর্ঘ দৌড়; AOT = দ্রুত শুরু, কম নমনীয়তা।
৭ · Stack আর heap — reference-এর মানসিক ছবি
এখন আসি এই পর্বের সবচেয়ে কাজের ছবিতে, যা পুরো কোর্সে বারবার লাগবে। একটি Java প্রোগ্রাম চলার সময় মেমরির দুটি অংশ নিয়ে ভাবুন:
📚 Stack
প্রতিটি method কলের জন্য একটি frame — তাতে থাকে সেই method-এর local ভ্যারিয়েবল। int, double, boolean-এর মতো primitive মান সরাসরি এখানে, আর object-এর reference-ও এখানে। Method শেষ, frame মুছে গেল — আপনাআপনি, সঙ্গে সঙ্গে।
🏬 Heap
new দিয়ে তৈরি প্রতিটি object এখানে থাকে — array, String, আপনার নিজের class-এর object. Method শেষ হলেও object থেকে যেতে পারে, যদি অন্য কোথাও তার reference থাকে। মুছে দেয় garbage collector.
Stack-এ ভ্যারিয়েবল int x = 5; মানে সেই ঘরে সরাসরি 5. কিন্তু Car a = new Car(); লিখলে দুটি আলাদা জিনিস তৈরি হয়: heap-এ একটি Car object, আর stack-এ a নামের একটি ঘর, যাতে object-টি নেই — আছে শুধু object-টি কোথায় আছে তার খোঁজ। এই খোঁজকে বলে reference.
ঢাকার শাহবাগে কেন্দ্রীয় পাবলিক লাইব্রেরির কথা ভাবুন। তাকে রাখা আছে “পথের পাঁচালী”-র একটিমাত্র কপি — এটি heap-এর object. ক্যাটালগে বইটির দুটি কার্ড আছে: একটি লেখকের নামে (বিভূতিভূষণ), আরেকটি বইয়ের নামে। দুটি কার্ডেই লেখা একই তাক নম্বর। কার্ড দুটি হলো দুটি reference — বই একটিই। কেউ বইয়ের একটি পাতায় দাগ দিলে যে কার্ড ধরেই খুঁজুন, দাগওয়ালা বইটিই পাবেন। আর একটি কার্ড ছিঁড়ে ফেললে বই হারায় না — যতক্ষণ অন্তত একটি কার্ড আছে, বইটি খুঁজে পাওয়া যায়। সব কার্ড হারিয়ে গেলে? বইটি তাকে থাকলেও কেউ আর খুঁজে পাবে না। তখন লাইব্রেরিয়ান সেটি সরিয়ে জায়গা খালি করতে পারেন — এটাই garbage collection.
এবার এই ছবিটি কোডে দেখুন। চারটি লাইন, একটি object, আর তার দিকে কতগুলো reference আছে — খেয়াল রাখুন:
1Car a = new Car();2Car b = a;3a = null;4b = null;5// ... পরে কোনো এক সময় GC চলে
| stack: a | |
| stack: b | |
| heap: Car #1 | |
| Car #1-এর দিকে reference | |
| মোট Car object |
Car a = new Car();— ক্রম খেয়াল করুন: আগে ডান দিক।new Car()heap-এ একটি Car object বানাল (ধরুন নাম “Car #1”). তারপর stack-এaঘর তৈরি হলো, আর তাতে রাখা হলো Car #1-এর reference.Car b = a;— কোনো নতুন Car তৈরি হয়নি।a-র ভেতরে যা আছে, অর্থাৎ reference-টি, কপি হলোb-তে। এখন দুটি ঘর একই object দেখায়।bদিয়ে object বদলালেaদিয়েও সেই বদল দেখা যাবে — ঠিক লাইব্রেরির দুটি কার্ডের মতো।a = null;— null মানে “কোনো object দেখায় না”।aঘরটি এখন খালি খোঁজ রাখে। কিন্তু object মুছে যায়নি!bএখনো তাকে দেখায়। Reference সরানো আর object মোছা এক জিনিস নয়।b = null;— object অনাথ হলো। এখন Car #1-এর দিকে একটিও reference নেই। প্রোগ্রামের কোনো ভ্যারিয়েবল দিয়ে আর তাকে পাওয়া সম্ভব নয়। এমন object-কে বলে unreachable — আর সেটি garbage collection-এর যোগ্য। খেয়াল করুন, “যোগ্য” মানে এখনই মুছে যাবে না।- পরে কোনো এক সময় — GC. Garbage collector যখন চলে (সাধারণত heap-এর একটি অংশ ভরে এলে), তখন সে দেখে কোন object-এ আর পৌঁছানো যায় না, আর তাদের জায়গা ফেরত নেয়। কখন চলবে, আপনি ঠিক করেন না — JVM ঠিক করে। আপনাকে কোথাও
deleteলিখতে হয়নি।
এবার কোডটি নিজে চালিয়ে দেখুন। এখানে দুটি reference একই object দেখায় কি না, তা == দিয়ে পরীক্ষা করা হচ্ছে, আর পাশাপাশি primitive-এর আচরণও:
class Main {
public static void main(String[] args) {
Car a = new Car();
a.color = "লাল";
Car b = a; // নতুন Car নয় — একই object-এর দ্বিতীয় reference
b.color = "নীল";
System.out.println("a.color = " + a.color);
System.out.println("a == b : " + (a == b));
Car c = new Car(); // এবার সত্যিই নতুন object
c.color = "নীল";
System.out.println("a == c : " + (a == c));
int x = 5;
int y = x; // primitive: মানটাই কপি হলো
y = 10;
System.out.println("x = " + x + ", y = " + y);
}
}
class Car {
String color;
}আউটপুট চার লাইন: a.color = নীল, a == b : true, a == c : false, x = 5, y = 10. আপনি a-র রং কখনো নীল করেননি, তবু নীল — কারণ a আর b একই object দেখায়। Object-এর ক্ষেত্রে == জিজ্ঞেস করে “দুটি reference কি একই object দেখায়?” — তাই a == c মিথ্যা, যদিও দুটির রং একই। “দেখতে এক কি না” তুলনার জন্য আছে equals (পর্ব ২৩)। আর int-এর ক্ষেত্রে y = x মানটাই কপি করে, তাই y বদলালে x বদলায় না। Class, object আর field-এর পুরো আলোচনা পর্ব ১৫-এ।
Java-র reference দেখতে C-র pointer-এর মতো, কিন্তু একটি বড় পার্থক্য আছে: reference নিয়ে হিসাব করা যায় না। C-তে p + 1 লিখে পরের ঠিকানায় যাওয়া যায়, বা একটি সংখ্যাকে ঠিকানা বানানো যায়। Java-তে এর কোনোটাই সম্ভব নয় — reference হয় একটি আসল object দেখায়, নয়তো null. ভুল ঠিকানায় হাত দেওয়ার পুরো এক জাতের বাগ এভাবে বন্ধ হয়ে যায়। সবচেয়ে খারাপ যা হতে পারে, তা হলো null reference দিয়ে কিছু করতে গেলে NullPointerException — যা অন্তত স্পষ্টভাবে, সঙ্গে সঙ্গে ধরা পড়ে।
৮ · Garbage collection — কে মুছে দেয়, কীভাবে, কী দামে
C-তে malloc দিয়ে নেওয়া মেমরি আপনাকেই free দিয়ে ফেরত দিতে হয়। ভুলে গেলে মেমরি ফুরোতে থাকে (memory leak); দুবার ফেরত দিলে বা ফেরত দেওয়ার পর আবার ব্যবহার করলে প্রোগ্রাম ক্র্যাশ করে, কিংবা আরও খারাপ — নিরাপত্তা-ছিদ্র তৈরি হয়। Java এই দায়িত্ব প্রোগ্রামারের কাছ থেকে নিয়ে JVM-কে দিয়েছে। এর নাম garbage collection (GC).
প্রশ্নটা “কোনটা garbage?” নয় — “কোনটা জীবিত?”
GC সরাসরি garbage খোঁজে না; সে খোঁজে কোনগুলো এখনো পৌঁছানো যায় (reachable). শুরু করে কিছু নিশ্চিত শুরুর বিন্দু থেকে, যাদের বলে GC root — চলমান প্রতিটি thread-এর stack-এর local ভ্যারিয়েবল, class-এর static ভ্যারিয়েবল ইত্যাদি। সেখান থেকে reference ধরে ধরে যতদূর যাওয়া যায়, সব object জীবিত। বাকি সব — যত বড়ই হোক, যতই দরকারি মনে হোক — garbage. এতে একটি সুন্দর সুবিধা আছে: দুটি object যদি শুধু একে অপরকে দেখায়, কিন্তু root থেকে তাদের কাছে পৌঁছানো না যায়, তবু দুটিই garbage. (Python-এর reference counting এই ধরনের চক্র একা ধরতে পারে না; তার জন্য আলাদা cycle collector লাগে।)
বেশিরভাগ আধুনিক collector একটি পর্যবেক্ষণের ওপর দাঁড়িয়ে: বেশিরভাগ object খুব অল্প বয়সেই মারা যায়। একটি লুপের ভেতরে বানানো অস্থায়ী String, একটি method-এর ভেতরের ছোট list — এগুলো কয়েক মিলিসেকেন্ডেই অনাথ হয়। তাই heap-কে ভাগ করা হয় নতুন (young) আর পুরোনো (old) অংশে; নতুন অংশ ঘন ঘন আর দ্রুত পরিষ্কার হয়, পুরোনো অংশ কদাচিৎ। একে বলে generational collection. বিস্তারিত পর্ব ৪৪-এ।
দাম — GC pause
বিনামূল্যে কিছু নেই। GC-কে কাজ করতে CPU লাগে, আর কাজের সুবিধার জন্য heap-এ বাড়তি ফাঁকা জায়গা রাখতে হয়। সবচেয়ে বড় খরচ — কোনো কোনো মুহূর্তে GC-কে আপনার প্রোগ্রামের সব thread থামিয়ে দিতে হয়, যাতে কাজের মাঝখানে object সরে না যায়। একে বলে stop-the-world pause. একটি ব্যাংকের সার্ভারে হঠাৎ এক সেকেন্ডের pause মানে সেই মুহূর্তের সব bKash বা কার্ড লেনদেনের উত্তর এক সেকেন্ড দেরিতে। এজন্যই গত দুই দশকে JVM-এর সবচেয়ে বেশি গবেষণা হয়েছে এই pause ছোট করা নিয়ে।
| Collector | মূল চিন্তা | কখন থেকে |
|---|---|---|
| G1 | pause ও গতির ভারসাম্য; heap-কে অনেক ছোট অঞ্চলে ভাগ করে | Java 9 থেকে ডিফল্ট |
| ZGC | খুব বড় heap-এও অতি ছোট pause; বেশিরভাগ কাজ প্রোগ্রাম চলার পাশাপাশি | Java 15-এ production-ready; Java 21-এ generational মোড |
| Shenandoah | ZGC-র মতোই ছোট pause-এর লক্ষ্য, Red Hat-এর তৈরি | Java 15-এ production-ready |
| Parallel | মোট কাজের পরিমাণ সর্বোচ্চ; pause বড় হলেও আপত্তি নেই | Java 8-এ সাধারণ সার্ভারে ডিফল্ট ছিল |
| Serial | এক thread-এর সরল collector; ছোট মেশিন ও ছোট heap | খুব ছোট মেশিনে JVM নিজে বেছে নিতে পারে |
আপনাকে এখন কোনোটি বেছে নিতে হবে না — বেশিরভাগ অ্যাপে ডিফল্ট G1 ভালোই কাজ করে। নামগুলো চিনে রাখুন; কোনটি কীভাবে কাজ করে, কখন কোনটি বাছবেন, আর GC log কীভাবে পড়বেন — পর্ব ৪৪-এ।
দুটি প্রচলিত ভুল ধারণা। এক: “Java-তে memory leak হয় না।” হয় — অন্যভাবে। কোনো static list-এ object জমাতে থাকলে আর কখনো বের না করলে, সেগুলো সবসময় reachable থাকে, GC ছোঁয় না, আর একসময় OutOfMemoryError. GC শুধু অপ্রয়োজনীয় নয়, অপৌঁছনীয় object মোছে। দুই: “System.gc() ডাকলে GC চলবে।” এটি শুধু একটি অনুরোধ; JVM উপেক্ষা করতে পারে, আর বাস্তব কোডে এটি ডাকা প্রায় সবসময়ই অপ্রয়োজনীয়।
- Java কি compiled না interpreted? — দুটোই: javac source-কে bytecode-এ compile করে; JVM সেই bytecode interpret করে, আর গরম অংশ JIT দিয়ে machine code-এ compile করে।
- JDK, JRE আর JVM-এর পার্থক্য? — JVM bytecode চালায়; JRE = JVM + library (চালানোর জন্য যথেষ্ট); JDK = JRE + javac, javap-এর মতো টুল (বানানোর জন্য)। Java 11 থেকে Oracle আলাদা JRE দেয় না।
- Java platform-independent কেন? — কারণ
.classফাইলের bytecode কোনো CPU-র জন্য নয়; প্রতিটি প্ল্যাটফর্মের নিজস্ব JVM সেটি চালায়। (JVM নিজে কিন্তু platform-dependent.) - Stack আর heap-এ কী থাকে? — stack-এ method-এর local primitive আর reference; heap-এ
newদিয়ে তৈরি সব object. - কখন একটি object GC-র যোগ্য হয়? — যখন কোনো GC root থেকে reference ধরে তার কাছে আর পৌঁছানো যায় না।
System.gc()শুধু অনুরোধ, নিশ্চয়তা নয়।
৯ · AI-এর যুগে — JVM নিয়ে পুরোনো তথ্য যাচাই
JVM নিয়ে প্রশ্ন করলে AI সাধারণত গুছিয়ে, আত্মবিশ্বাসের সাথে উত্তর দেয়। সমস্যা হলো, ইন্টারনেটে Java 6–8 যুগের লেখা অগণিত — টিউটোরিয়াল, পুরোনো ব্লগ, Stack Overflow উত্তর। AI সেগুলো থেকেই শিখেছে। ফলে বহু বছর আগে বদলে যাওয়া তথ্য সে এখনো বর্তমান হিসেবে বলে। Java প্রতি ছয় মাসে একটি নতুন version পায়, আর JVM-এর ভেতরটা এই সময়ে অনেক বদলেছে।
| AI যা বলতে পারে | এখনকার সঠিক তথ্য |
|---|---|
“Heap-এর একটি অংশ PermGen; -XX:MaxPermSize বাড়ান” | PermGen Java 8-এ বাদ; class-এর তথ্য এখন Metaspace-এ (JEP 122) |
| “Java-র ডিফল্ট GC হলো Parallel” বা “CMS ব্যবহার করুন” | Java 9 থেকে ডিফল্ট G1 (JEP 248); CMS Java 14-এ মুছে ফেলা হয়েছে (JEP 363) |
| “Java interpreted, তাই ধীর” | HotSpot-এর JIT ২০০০ সাল থেকেই (Java 1.3) ডিফল্ট; tiered compilation Java 8 থেকে |
“পরিষ্কারের কাজ finalize()-এ লিখুন” | finalize() Java 9 থেকে deprecated, Java 18-এ মুছে ফেলার জন্য চিহ্নিত (JEP 421); try-with-resources ব্যবহার করুন (পর্ব ২৯) |
“দ্রুত চালু করতে -Xverify:none দিন” | নিরাপত্তা বন্ধ করে দেয়; Java 13 থেকে deprecated |
JVM-এর ভেতরের কোনো বিষয় AI-কে জিজ্ঞেস করলে তাকে version বেঁধে দিন, আর সূত্র চান। তাতে পুরোনো তথ্য ধরা সহজ হয়:
AI-এর JVM-ব্যাখ্যা হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১HotSpot JVM এলো কোথা থেকে? একটি Smalltalk কোম্পানির গবেষণা কীভাবে Java-কে দ্রুত বানাল?
১৯৯৬ সালে Java-র প্রথম version প্রকাশের সময় JVM মূলত একটি সরল interpreter ছিল। “Java ধীর” কথাটি তখন প্রায় সর্বজনীন। সমাধান এলো এক অপ্রত্যাশিত দিক থেকে।
১৯৮০-৯০-এর দশকে Self নামের একটি গবেষণা-ভাষা নিয়ে কাজ চলছিল — David Ungar ও Randall Smith-এর তৈরি, জন্ম Xerox PARC-এ, পরে গবেষণা চলে Stanford ও Sun-এ। Self ছিল অত্যন্ত dynamic — type নির্দিষ্ট করা থাকে না, সবকিছু object. এমন ভাষাকে দ্রুত চালাতে গবেষকেরা, বিশেষ করে Urs Hölzle, adaptive optimization-এর কৌশল তৈরি করেন: প্রোগ্রাম চলতে দাও, দেখো কোন অংশ বারবার চলে আর কোন type আসলে আসে, তারপর সেই তথ্য দিয়ে compile করো; অনুমান ভুল হলে compile করা কোড বাতিল করে ফিরে যাও (deoptimization).
এই গবেষকদের কয়েকজন — Urs Hölzle, Lars Bak প্রমুখ — Animorphic Systems নামে একটি ছোট কোম্পানিতে Smalltalk-এর একটি দ্রুত সংস্করণ (Strongtalk) বানাচ্ছিলেন। তাঁরা বুঝলেন, এই প্রযুক্তি Java-তেও কাজ করবে। ১৯৯৭ সালে Sun কোম্পানিটি কিনে নেয়। ১৯৯৯ সালে HotSpot প্রথমে Java 1.2-এর জন্য বাড়তি অংশ হিসেবে আসে, আর ২০০০ সালে Java 1.3-এ ডিফল্ট JVM হয়ে যায়।
গল্পটির একটি মজার উপসংহার আছে। Lars Bak পরে Google-এ গিয়ে Chrome ব্রাউজারের JavaScript ইঞ্জিন V8 তৈরির দলের নেতৃত্ব দেন (২০০৮)। JavaScript-ও dynamic ভাষা, আর V8 একই ধরনের চিন্তা — চলমান প্রোগ্রাম দেখে compile করা — ব্যবহার করে ওয়েবকে বদলে দেয়। Self-এর গবেষণা তাই আজ Java সার্ভারে, আপনার ব্রাউজারে, এমনকি Node.js-এ চলছে।
মূল উপলব্ধি: চলার সময়ের তথ্য একটি সম্পদ। আগেভাগে compile করা কোড শুধু অনুমান করতে পারে; JIT দেখে নিতে পারে। আর বড় প্রকৌশল-অগ্রগতি অনেক সময় আসে এমন গবেষণা থেকে, যাকে প্রথমে “শুধু একাডেমিক” মনে হয়েছিল।
প্র ০২Java কেন garbage collection বেছে নিল, যখন C/C++ প্রোগ্রামাররা নিজেরাই মেমরি সামলাতেন? এর দাম কী?
Garbage collection নতুন ধারণা ছিল না — ১৯৫৯ সালের দিকে John McCarthy Lisp ভাষার জন্য এটি উদ্ভাবন করেন। কিন্তু দশকের পর দশক এটিকে “ধীর, একাডেমিক ভাষার জিনিস” মনে করা হতো। শিল্পের মূলধারা ছিল C আর C++, যেখানে মেমরি প্রোগ্রামারের দায়িত্ব।
James Gosling-এর দল যখন Java নকশা করছিল, তাঁদের লক্ষ্য ছিল নেটওয়ার্কে চলা, নানা যন্ত্রে চলা, নির্ভরযোগ্য ও নিরাপদ সফটওয়্যার। আর তাঁরা দেখছিলেন C/C++ প্রোগ্রামের সবচেয়ে বেশি বাগ আর নিরাপত্তা-ছিদ্র আসে মেমরির ভুল থেকে: মুছে ফেলা মেমরি আবার ব্যবহার (use-after-free), দুবার মোছা, array-র সীমার বাইরে লেখা, আর ভুলে যাওয়া free. এই সমস্যা আজও আছে — Microsoft ২০১৯ সালে জানায়, তাদের পণ্যে বছরে যত নিরাপত্তা-ত্রুটি ঠিক করা হয়, তার প্রায় ৭০% মেমরি-নিরাপত্তা সংক্রান্ত। Chrome দলও প্রায় একই ধরনের হিসাব দিয়েছে।
Java-র সিদ্ধান্ত ছিল এই পুরো জাতের বাগকে ভাষার স্তরেই অসম্ভব করে দেওয়া: pointer arithmetic নেই, array-র সীমা প্রতিবার যাচাই, আর মেমরি ফেরত দেয় GC. এতে প্রোগ্রামারের সময় বাঁচে, আর কোড অনেক বেশি নিরাপদ হয়।
দাম কী? এক, বাড়তি মেমরি — GC ভালোভাবে কাজ করতে heap-এ ফাঁকা জায়গা লাগে; একই কাজের Java প্রোগ্রাম প্রায়ই C++-এর চেয়ে বেশি RAM নেয়। দুই, CPU খরচ — GC-র নিজের কাজ। তিন, অনিশ্চিত সময় — কখন pause হবে, প্রোগ্রামার ঠিক জানেন না। এজন্য গাড়ির ব্রেকের নিয়ন্ত্রক বা অডিও প্রক্রিয়াকরণের মতো কঠোর সময়-নির্ভর কাজে GC-ভাষা কম ব্যবহৃত হয়। আধুনিক ZGC-র মতো collector এই তৃতীয় দামটি অনেক কমিয়েছে, তবে শূন্য করেনি।
মজার ব্যাপার, পরের প্রজন্মের ভাষা Rust তৃতীয় একটি পথ নিয়েছে — GC ছাড়াই মেমরি-নিরাপত্তা, compile-এর সময় কঠোর নিয়ম দিয়ে। এর দাম আবার শেখার কঠিনতা।
মূল উপলব্ধি: GC হলো মেশিনের সময় দিয়ে মানুষের সময় ও নিরাপত্তা কেনা। বেশিরভাগ ব্যবসায়িক সফটওয়্যারে এটি লাভজনক বিনিময় — আর Java-র বিপুল সাফল্য সেটাই প্রমাণ করে।
প্র ০৩Java 8-এ PermGen সরিয়ে Metaspace আনা হলো কেন? “OutOfMemoryError: PermGen space”-এর গল্পটা কী?
JVM-কে প্রতিটি লোড করা class-এর তথ্য কোথাও রাখতে হয় — class-এর নাম, method-গুলোর bytecode, field-এর তালিকা ইত্যাদি। Java 7 পর্যন্ত HotSpot এগুলো রাখত heap-এর পাশে একটি আলাদা, নির্দিষ্ট আকারের অঞ্চলে, যার নাম ছিল Permanent Generation বা PermGen. এর সর্বোচ্চ আকার JVM চালু করার সময়ই ঠিক করে দিতে হতো (-XX:MaxPermSize), আর ডিফল্ট মান ছিল বেশ ছোট।
সমস্যা প্রকট হতো Tomcat-এর মতো application server-এ। সেখানে একটি ওয়েব অ্যাপ বন্ধ না করেই নতুন version বসানো (redeploy) যায়। প্রতিবার অ্যাপের class নতুন একটি class loader দিয়ে লোড হয়। পুরোনো loader আর তার সব class মোছার কথা — কিন্তু কোথাও একটিমাত্র reference রয়ে গেলে (যেমন কোনো library-র static ভ্যারিয়েবলে) পুরো পুরোনো loader বেঁচে থাকে। কয়েকবার redeploy-এর পর PermGen ভরে যেত, আর আসত সেই কুখ্যাত java.lang.OutOfMemoryError: PermGen space. বহু বছর ধরে এটি Java ডেভেলপারদের একটি পরিচিত দুঃস্বপ্ন ছিল।
সমাধান এল ধাপে ধাপে। Java 7-এ intern করা String আর class-এর static ভ্যারিয়েবল PermGen থেকে সাধারণ heap-এ সরানো হয়। তারপর Java 8-এ JEP 122 পুরো PermGen বাদ দেয়। Class-এর তথ্য এখন থাকে Metaspace-এ — JVM-এর heap-এর বাইরে, সরাসরি অপারেটিং সিস্টেমের কাছ থেকে নেওয়া মেমরিতে, যা ডিফল্টভাবে প্রয়োজন মতো বাড়তে পারে। এর একটি কারণ ছিল Oracle-এর দুটি JVM — HotSpot আর JRockit (যাতে PermGen ছিল না) — এক করার পরিকল্পনা।
তবে খেয়াল করুন: মূল রোগ — class loader leak — সারেনি, শুধু উপসর্গ বদলেছে। এখন সেই leak হলে Metaspace বাড়তেই থাকে, আর সীমা দিলে (-XX:MaxMetaspaceSize) আসে OutOfMemoryError: Metaspace. পুরোনো MaxPermSize flag Java 8-এ শুধু সতর্কবার্তা দিয়ে উপেক্ষা করা হতো; নতুন version-এ এমন অচেনা flag দিলে JVM চালুই না-ও হতে পারে।
মূল উপলব্ধি: একটি নির্দিষ্ট আকারের সীমা সমস্যাকে আগে দেখিয়ে দেয়, আর সীমাহীন জায়গা সমস্যাকে লুকিয়ে রাখে — কোনোটিই leak সারায় না। আর AI বা পুরোনো ব্লগ PermGen-এর পরামর্শ দিলে বুঝবেন, তথ্যটি অন্তত এক দশক পুরোনো।
প্র ০৪G1 কেন Java 9-এ ডিফল্ট হলো, আর ZGC কীভাবে বিশাল heap-এও এক মিলিসেকেন্ডের কম pause দেয়?
Java 8 পর্যন্ত সাধারণ সার্ভারে ডিফল্ট ছিল Parallel collector. এটি মোট কাজের পরিমাণে (throughput) চমৎকার, কিন্তু পুরোনো অংশ পরিষ্কারের সময় পুরো প্রোগ্রাম থামিয়ে পুরো অংশ একবারে পরিষ্কার করে। Heap যত বড়, pause তত লম্বা — কয়েক GB heap-এ সেকেন্ড ছাড়িয়ে যাওয়া অস্বাভাবিক ছিল না। ওয়েব সার্ভারে এটা মানে ব্যবহারকারীর কাছে হঠাৎ আটকে যাওয়া পাতা।
G1 (Garbage-First) নতুন চিন্তা আনে: heap-কে সমান আকারের অনেক ছোট অঞ্চলে (region) ভাগ করা, আর প্রতিবার পুরো heap নয়, যেসব অঞ্চলে সবচেয়ে বেশি garbage সেগুলো আগে পরিষ্কার করা — নামের “Garbage-First” অংশটি এখান থেকেই। আপনি একটি লক্ষ্য দিতে পারেন, যেমন “pause ২০০ মিলিসেকেন্ডের বেশি নয়” (ডিফল্ট লক্ষ্য এটাই), আর G1 সেই অনুযায়ী প্রতিবার কতগুলো অঞ্চল পরিষ্কার করবে ঠিক করে। G1 প্রথম আসে Java 6-এর শেষ দিকে পরীক্ষামূলক হিসেবে, পরিণত হতে কয়েক বছর লাগে, আর JEP 248-এর মাধ্যমে Java 9-এ ডিফল্ট হয়। যুক্তি ছিল: আজকের বেশিরভাগ অ্যাপে সর্বোচ্চ throughput-এর চেয়ে অনুমানযোগ্য pause বেশি জরুরি।
ZGC আরও এক ধাপ এগোয়। Oracle-এর তৈরি এই collector প্রথম আসে Java 11-এ পরীক্ষামূলক হিসেবে, আর Java 15-এ (JEP 377) production-এ ব্যবহারের উপযোগী ঘোষিত হয়। এর মূল কৌশল হলো object সরানো (compaction)-সহ প্রায় সব কাজ প্রোগ্রাম চলার পাশাপাশি করা। কিন্তু প্রোগ্রাম চলছে আর object সরে যাচ্ছে — তাহলে প্রোগ্রাম পুরোনো ঠিকানায় হাত দেবে না? ZGC প্রতিটি reference-এর ভেতরে কয়েকটি বাড়তি bit রাখে (colored pointer), আর প্রতিবার heap থেকে reference পড়ার সময় একটি ছোট যাচাই-কোড (load barrier) চলে; object সরে গিয়ে থাকলে সেই মুহূর্তেই নতুন ঠিকানা ঠিক করে নেয়। Java 16-এ thread stack প্রক্রিয়াকরণও পাশাপাশি করার ব্যবস্থা হওয়ার পর pause সাধারণত এক মিলিসেকেন্ডের নিচে নেমে আসে — heap কয়েকশো MB হোক বা কয়েক TB. Java 21-এ এসেছে generational ZGC (JEP 439), যা young-old ভাগ ব্যবহার করে আরও কম খরচে কাজ করে।
দাম? এই পাশাপাশি কাজের জন্য বাড়তি CPU আর মেমরি লাগে, আর প্রতিটি reference পড়ায় সামান্য বাড়তি কাজ। তাই সর্বোচ্চ throughput চাইলে এখনো Parallel ভালো হতে পারে।
মূল উপলব্ধি: “সেরা GC” বলে কিছু নেই — আছে throughput, latency আর মেমরির মধ্যে একটি ত্রিভুজ। প্রতিটি collector এই ত্রিভুজের একটি ভিন্ন কোণ বেছে নেয়। কোনটি দরকার, তা আপনার অ্যাপের চাহিদা বলে দেবে — পর্ব ৪৪-এ মাপজোখসহ দেখবেন।
প্র ০৫Bytecode verifier কেন নিরাপত্তার অংশ? Applet-এর যুগ থেকে আজ পর্যন্ত এর ভূমিকা কী?
১৯৯৫ সালে Java-র প্রথম বড় আকর্ষণ ছিল applet — ওয়েব পাতার ভেতরে চলা ছোট Java প্রোগ্রাম। ভাবুন ব্যাপারটা: আপনি একটি অচেনা ওয়েবসাইটে গেলেন, আর সেই সাইটের কোড আপনার কম্পিউটারে চলতে শুরু করল! এটি নিরাপদ করতে Java-র নকশায় একটি “sandbox” ছিল, যার তিনটি স্তম্ভ: class loader (কোন class কোথা থেকে আসছে), bytecode verifier (class ফাইলটি নিয়ম মানছে কি না), আর security manager (এই কোড ফাইল পড়তে বা নেটওয়ার্কে যেতে পারবে কি না)।
Verifier কেন দরকার? কারণ আক্রমণকারী javac ব্যবহার করতে বাধ্য নয়। সে হাতে একটি class ফাইল বানাতে পারে, যেখানে একটি সংখ্যাকে object reference হিসেবে ব্যবহার করা হয়েছে — যা দিয়ে মেমরির যেকোনো জায়গায় হাত দেওয়া যেত, বা private field সরাসরি পড়া যেত। Verifier চালানোর আগেই প্রমাণ করে যে এমন কিছু ঘটা সম্ভব নয়: প্রতিটি নির্দেশ সঠিক type পায়, stack উপচে পড়ে না, jump বৈধ জায়গায় যায়। Security manager-এর সব নিয়ম এই প্রমাণের ওপর দাঁড়িয়ে — verifier ভাঙলে বাকি সব অর্থহীন।
বাস্তবেও তাই ঘটেছে। ১৯৯৬ সালে Princeton-এর একটি গবেষক দল (Drew Dean, Edward Felten, Dan Wallach) Java-র নিরাপত্তায় একাধিক ত্রুটি খুঁজে বের করে, যার কয়েকটি verifier ও type system-এর সাথে সম্পর্কিত। পরে Java 6-এ verifier-এর নকশা বদলানো হয়: javac এখন class ফাইলে বাড়তি type-তথ্য (StackMapTable) লিখে দেয়, আর JVM শুধু সেই তথ্য মিলিয়ে দেখে — দ্রুততর, এবং যাচাই করা সহজ।
Applet আর নেই। ব্রাউজার plugin-এর একের পর এক নিরাপত্তা-ছিদ্র, আর ব্রাউজারগুলোর plugin সমর্থন বন্ধ করায় applet হারিয়ে যায়; Java 9-এ Applet API deprecated হয়, Java 11-এ browser plugin JDK থেকে বাদ পড়ে, আর Java 17-এ Security Manager-ও মুছে ফেলার জন্য চিহ্নিত হয় (JEP 411). কিন্তু verifier এখনো প্রতিটি class লোডের সময় কাজ করে। কারণ আজও বহু library চলার সময় bytecode বানায় (Spring, Hibernate, mocking টুল) — তাদের একটি বাগ যেন পুরো JVM ক্র্যাশ না করায়, verifier সেই পাহারাটুকু দেয়। এজন্যই -Xverify:none দিয়ে verifier বন্ধ করার সুযোগ Java 13 থেকে deprecated.
মূল উপলব্ধি: নিরাপত্তা নকশার একটি মৌলিক নীতি — সীমানা পেরিয়ে আসা কোনো ইনপুটকে বিশ্বাস করো না, এমনকি সেটা “আমাদের নিজেদের compiler”-এর বানানো হলেও। ইনপুট যাচাই করো সেই জায়গায়, যেখানে তা ব্যবহার হবে।
অনুশীলন
এগুলোর বেশিরভাগে কোড লিখতে হবে না — হাতে-কলমে দেখা আর ভাবাই আসল কাজ।
-
নিজের হাতে bytecode: পর্ব ০৩-এ ইনস্টল করা JDK দিয়ে উপরের
Calcclass compile করুন, তারপরjavap -c Calcচালান। এবারa + b-কেa * bকরুন, আর পরেint-কেlongকরে আবার দেখুন। কী কী বদলাল?a * bকরলেiadd-এর জায়গায় আসেimul.longকরলে নির্দেশগুলো হয়lload_0,lload_2,lmul(বাladd),lreturn. খেয়াল করুন দ্বিতীয় argument এখন ঘর ২-এ — কারণ JVM-এ একটিlongদুটি local ঘর জুড়ে থাকে। অর্থাৎ bytecode-এ type সবসময় স্পষ্ট লেখা; চলার সময় JVM-কে type খুঁজতে হয় না। -
JIT-কে কাজ করতে দেখুন: একটি লুপে কোনো ছোট method কয়েক লাখবার ডাকে এমন প্রোগ্রাম লিখে চালান
java -XX:+PrintCompilation Mainদিয়ে। আউটপুটে কী দেখছেন? তারপরjava -Xint Mainদিয়ে চালিয়ে সময়ের পার্থক্য অনুভব করুন।-XX:+PrintCompilationপ্রতিবার কোনো method JIT-compile হলে একটি লাইন ছাপে — তাতে একটি tier সংখ্যা (৩ মানে C1-এর profiling সংস্করণ, ৪ মানে C2) আর method-এর নাম থাকে। আপনার ছোট method প্রথমে tier 3-এ, পরে tier 4-এ দেখা দেবে; JDK-র নিজের অনেক method (String-এর কাজ ইত্যাদি)-ও দেখবেন। কখনোmade not entrantলেখা দেখবেন — মানে পুরোনো compile করা সংস্করণ বাতিল হলো।-XintJIT পুরোপুরি বন্ধ করে শুধু interpreter চালায়; ভারী লুপে প্রোগ্রাম অনেক গুণ ধীর হয়ে যায়। সঠিক অনুপাত মেশিনভেদে আলাদা। -
Stack-heap ছবি আঁকুন: কাগজে stack আর heap এঁকে নিচের লাইনগুলো চালান —
Car p = new Car(); Car q = new Car(); Car r = p; p = q; r = null;শেষে কয়টি Car object আছে, আর কয়টি GC-র যোগ্য?দুটি object তৈরি হয়েছে — ধরুন Car #1 (প্রথমে
p) আর Car #2 (q).r = pকরার পর Car #1-এর দিকে দুটি reference (p,r).p = qকরায়pএখন Car #2 দেখায়, Car #1-এর দিকে শুধুr. শেষেr = null— Car #1-এর দিকে আর কোনো reference নেই। ফলে heap-এ এখনো দুটি object (GC চলার আগ পর্যন্ত), যার একটি (Car #1) GC-র যোগ্য; Car #2-কেpআরqদুটোই দেখায়। -
AI অডিট: যেকোনো AI-কে version উল্লেখ না করে জিজ্ঞেস করুন: “JVM-এর memory structure আর default garbage collector সম্পর্কে বলো।” তারপর অংশ ৯-এর টেবিল আর চেকলিস্ট মিলিয়ে উত্তরটি যাচাই করুন।
প্রায়ই দেখা যায়:
- Heap-এর ছবিতে Young/Old-এর পাশে “PermGen” বা “Permanent Generation” রয়ে গেছে — Java 8 থেকে নেই।
- ডিফল্ট GC হিসেবে Parallel বলা, বা CMS-কে “কম pause-এর জন্য সেরা” বলা — বর্তমান Java-তে ডিফল্ট G1, CMS মুছে ফেলা হয়েছে।
- ZGC বা Shenandoah-র নামই নেই, বা “পরীক্ষামূলক” বলা — Java 15 থেকে দুটোই production-ready.
এরপর একই প্রশ্ন অংশ ৯-এর prompt দিয়ে (version বেঁধে, JEP চেয়ে) করুন — উত্তরের মান কতটা বদলায়, তুলনা করুন। নিজের JDK-তে
java -XX:+PrintCommandLineFlags -versionচালালে সাধারণ কম্পিউটারে-XX:+UseG1GCদেখবেন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি Java প্রোগ্রাম চলার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ✍️
Main.javaফাইলে কোড লেখা - 📄
javac— source থেকেMain.class(bytecode) - 📥 Class loader — class ফাইল মেমরিতে আনা
- 🛡️ Verifier — bytecode নিয়ম মানছে কি না যাচাই
- 🐢 Interpreter — bytecode এক এক করে চালানো
- 🚀 JIT — বারবার চলা method আসল machine code-এ
javac Main.java চালালে কী তৈরি হয়?
- Windows-এ চালানোর জন্য একটি .exe ফাইল
- Bytecode-ভরা একটি Main.class ফাইল
- প্রোগ্রামের আউটপুট
- x86 assembly ফাইল
JVM-এর কোন অংশ চালানোর আগে প্রমাণ করে যে class ফাইলের নির্দেশগুলো type ও stack-এর নিয়ম ভাঙছে না?
- Garbage collector
- JIT compiler
- Bytecode verifier
- javap
Car a = new Car(); Car b = a; — এই দুই লাইনের পর heap-এ কয়টি Car object আছে?
- ১টি
- ২টি
- ০টি
- JVM-এর ওপর নির্ভর করে, বলা যায় না
Java-তে একটি object কখন garbage collection-এর যোগ্য হয়?
- যে method-এ object তৈরি হয়েছে, সেটি শেষ হলেই
- প্রোগ্রামার delete লিখলে
- System.gc() ডাকলে
- যখন কোনো জীবিত reference ধরে তার কাছে আর পৌঁছানো যায় না
একটি Java সার্ভার অ্যাপ চালু হওয়ার প্রথম কয়েক মিনিট ধীর থাকে, পরে দ্রুত হয়। মূল কারণ কী?
- শুরুতে garbage collector বেশি কাজ করে
- JIT গরম method-গুলো ধীরে ধীরে machine code-এ compile করে (warm-up)
- javac পেছনে পেছনে কোড compile করতে থাকে
- অপারেটিং সিস্টেম ধীরে ধীরে নেটওয়ার্ক দ্রুত করে
Java 9 থেকে HotSpot JVM-এর ডিফল্ট garbage collector কোনটি (সাধারণ মেশিনে)?
- CMS
- Parallel
- G1
- ZGC
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
javacsource-কে bytecode-এ (.class) অনুবাদ করে; সেটি চালায় JVM — তাই এক binary সব প্ল্যাটফর্মে।- JVM-এর ভেতরে: class loader → verifier → interpreter → JIT (C1, তারপর C2; tiered compilation).
- Java চলতে চলতে দ্রুত হয় (warm-up); দ্রুত শুরু দরকার হলে AOT বা GraalVM native-image.
- Local primitive আর reference থাকে stack-এ, object থাকে heap-এ;
b = areference কপি করে, object নয়। - কোনো reference ধরে পৌঁছানো যায় না এমন object মুছে দেয় garbage collector (ডিফল্ট G1); দাম — বাড়তি মেমরি, CPU আর pause.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৬ · টাইপ ও ভ্যারিয়েবল — primitive, wrapper আর varপরবর্তী পর্বPrimitive আর reference type-এর পার্থক্য কোডে।
- Java প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python কীভাবে চলে — bytecode আর virtual machineতুলনা করুনআরেকটি bytecode-ভিত্তিক ভাষা: reference counting আর GIL.
- C টুলচেইন — compiler, linker আর debuggerপাশের ট্র্যাকBytecode ছাড়া সরাসরি machine code বানানোর পথ।