আপনার জাভা অ্যাপ্লিকেশন কি হঠাৎ ক্র্যাশ করে java.lang.OutOfMemoryError: Java heap space বার্তা ছুঁড়ে দেয়? OutOfMemoryError কেবল একটি ত্রুটির নাম নয় – এটি জাভা ভার্চুয়াল মেশিন (JVM) এর সম্পূর্ণ মেমরি ব্যবস্থাপনার সমস্যার সংকেত। সঠিক ধারণা ও সঠিক সরঞ্জাম ছাড়া এই ত্রুটি প্রোডাকশন পরিবেশকে অচল করে দিতে পারে। এই টিউটোরিয়ালে আমরা JVM-এর বিভিন্ন মেমরি অঞ্চল, OutOfMemoryError-এর প্রকারভেদ, ত্রুটির পুনরুৎপাদন, বিশ্লেষণ এবং স্থায়ী প্রতিরোধের পদ্ধতি বিস্তারিতভাবে শিখব।
ভূমিকা
জাভা অ্যাপ্লিকেশন চালানোর সময় JVM বিভিন্ন মেমরি অঞ্চলে ডেটা সংরক্ষণ করে – যেমন হিপ (অবজেক্ট সংরক্ষণের জন্য), মেটাস্পেস (ক্লাস মেটাডেটা), ডাইরেক্ট বাফার মেমরি (NIO-র জন্য অফ-হিপ মেমরি), এবং স্ট্যাক (মেথড কল ও লোকাল ভেরিয়েবল)। যখন কোনো অঞ্চলের মেমরি সম্পূর্ণ ভরে যায় এবং JVM নতুন অবজেক্ট বা মেটাডেটা বরাদ্দ করতে না পারে, তখন OutOfMemoryError ছুঁড়ে দেওয়া হয়। এটি Error শ্রেণির অংশ, অর্থাৎ এটি Throwable থেকে এসেছে, কিন্তু Exception নয়; সাধারণত এটি অ্যাপ্লিকেশন ধ্বংস করে দেয় এবং ধরার (catch) চেষ্টা করলেও JVM-এর অবস্থা অস্থিতিশীল থাকে।
OutOfMemoryError কয়েকটি নির্দিষ্ট বার্তা নিয়ে আসে, যেগুলো সমস্যার উৎস সনাক্ত করতে গুরুত্বপূর্ণ:
Java heap space– হিপ পূর্ণ।GC overhead limit exceeded– গারবেজ কালেক্টর খুব বেশি সময় নিচ্ছে।Metaspace– ক্লাস মেটাডেটার জন্য বরাদ্দস্থান শেষ।Direct buffer memory– অফ-হিপ NIO বাফার শেষ।unable to create new native thread– থ্রেড তৈরির জন্য পর্যাপ্ত নেটিভ রিসোর্স নেই।
এই ত্রুটিগুলোর যেকোনো একটি অ্যাপ্লিকেশনের কার্যকারিতা বন্ধ করে দেয়, কিন্তু প্রতিটির জন্য আলাদা সমাধান প্রয়োজন। পরবর্তী অনুচ্ছেদগুলোতে আমরা এগুলো বাস্তব উদাহরণসহ বুঝব।
পূর্বশর্ত
নিচের ধাপগুলো অনুসরণ করতে আপনার থাকতে হবে:
- জাভা ডেভেলপমেন্ট কিট (JDK) ৮ বা তার পরবর্তী সংস্করণ ইনস্টল করা। টার্মিনালে
java -versionচালিয়ে নিশ্চিত হন। - একটি টেক্সট এডিটর অথবা ইন্টিগ্রেটেড ডেভেলপমেন্ট এনভায়রনমেন্ট (IDE) যেমন IntelliJ IDEA, Eclipse বা VS Code।
- JVM মেমরি অপশন সম্পর্কে প্রাথমিক ধারণা (যেমন
-Xmx,-Xms,-XX:MaxMetaspaceSize)। - (ঐচ্ছিক) হিপ ডাম্প বিশ্লেষণের জন্য Eclipse Memory Analyzer (MAT) বা VisualVM টুল।
ধাপে ধাপে বাস্তবায়ন
আমরা এখানে হিপ স্পেস OutOfMemoryError একটি পূর্ণাঙ্গ সিমুলেশন, বিশ্লেষণ এবং সমাধানের ধাপ অনুসরণ করব। একই পদ্ধতি অন্যান্য প্রকারের OutOfMemoryError-তেও প্রযোজ্য।
১. OutOfMemoryError সিমুলেশন কোড তৈরি
নীচের ক্লাসটি একটি ArrayList-এ দীর্ঘ স্কেলে বাইট অ্যারে যোগ করতে থাকে, যা ইচ্ছাকৃতভাবে হিপ পূর্ণ করে:
import java.util.ArrayList;
import java.util.List;
public class HeapOomSimulator {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
int counter = 0;
try {
while (true) {
// 1 MB বরাদ্দ
list.add(new byte[1024 * 1024]);
counter++;
if (counter % 10 == 0) {
System.out.println("বরাদ্দ হয়েছে: " + counter + " MB");
}
}
} catch (OutOfMemoryError e) {
System.err.println("OutOfMemoryError ধরা পড়েছে! মোট বরাদ্দ: " + counter + " MB");
// আংশিক পরিস্কারের চেষ্টা (সচরাচর বর্জনীয়)
list.clear();
}
}
}
এই কোডটি প্রতিটি ইটারেশনে ১ মেগাবাইটের একটি অ্যারে তৈরি করে এবং list-এ রেখে দেয়, ফলে গারবেজ কালেক্টর এগুলো ফ্রি করতে পারে না। যখন হিপ পূর্ণ হবে, OutOfMemoryError ধরা পড়বে এবং আমরা প্রিন্ট করব কত এমবি বরাদ্দ হয়েছে।
২. জেভিএম অপশন সহ রান করা
কোডটি সঠিকভাবে সিমুলেট করতে জেভিএমকে সীমিত হিপ বরাদ্দ দিয়ে চালান। কম্পাইল করা ক্লাসটির জন্য টার্মিনাল কমান্ড:
java -Xmx128m -XX:+HeapDumpOnOutOfMemoryError HeapOomSimulator
এখানে:
-Xmx128mহিপের সর্বোচ্চ সাইজ ১২৮ মেগাবাইট নির্ধারণ করছে।-XX:+HeapDumpOnOutOfMemoryErrorত্রুটি ঘটার সময় একটি হিপ ডাম্প ফাইল (.hprof) তৈরি করবে, যা পরবর্তীতে বিশ্লেষণের জন্য জরুরি।
আপনি দেখবেন অ্যাপ্লিকেশন কিছু এমবি বরাদ্দ করার পর OutOfMemoryError ছুঁড়ে দিয়ে বন্ধ হয়ে যাচ্ছে। লোকেশন অনুযায়ী একটি java_pid<pid>.hprof ফাইল তৈরি হবে।
৩. হিপ ডাম্প বিশ্লেষণ
Eclipse Memory Analyzer (MAT) বা VisualVM ব্যবহার করে .hprof ফাইল খুলুন। Leak Suspects রিপোর্ট দেখলে আপনি জানতে পারবেন কোন ক্লাস/অবজেক্ট সবচেয়ে বেশি মেমরি ধরে রেখেছে। আমাদের উদাহরণে byte[] অ্যারেগুলোই প্রধান দখলদার। ArrayList-এর elementData অ্যারেতে এগুলো রেফারেন্স হিসেবে জমা আছে। MAT থেকে Dominator Tree দেখালে বুঝবেন কোন পাথ অবজেক্টগুলো ধরে রেখেছে।
হিপ ডাম্প ছাড়াও, jmap কমান্ড বা JConsole রিয়েল-টাইম হিপ ব্যবহার পর্যবেক্ষণ করতে পারে।
৪. কোডে মেমরি লিক সংশোধন
আমাদের সিমুলেটেড কোডে সমস্যা হলো সব অ্যারেকে list-এ জমা রাখা, যা ডেটা প্রসেসিংয়ের পরিবেশে অপ্রয়োজনীয়। মেমরি লিক রোধের মূল কৌশলগুলো:
- সংগ্রহ (reference) দ্রুত মুক্ত করুন: অবজেক্ট ব্যবহারের পর সেটি
nullকরে দিন বা স্কোপ থেকে বের করে দিন। - স্ট্রিমিং ব্যবহার: পুরো ডেটা একবারে লোড না করে ইটারেটর বা স্ট্রিম দিয়ে একটু একটু করে প্রসেস করুন।
- বাফার সাইজ নিয়ন্ত্রণ: যেমন
BufferedReader,ByteBufferইত্যাদির জন্য নির্দিষ্ট বাফার সাইজ ব্যবহার করুন। - WeakHashMap / Reference: ক্যাশে প্রয়োজনে
WeakReferenceব্যবহার করুন, যাতে গারবেজ কালেক্টর মেমরি ফ্রি করতে পারে। - try‑with‑resources: ফাইল বা সকেট ব্যবহারের পর রিসোর্স অটোমেটিক ক্লোজ করতে
try-with-resourcesব্যবহার করুন।
নিচের পরিমার্জিত সংস্করণে আমরা পূর্বের উদাহরণটি এমনভাবে লিখেছি যেখানে প্রতি ইটারেশনে অ্যারেটি ব্যবহারের পর সঙ্গে সঙ্গে রেফারেন্স সরিয়ে দিচ্ছি। এটি বাস্তবে মেমরি ফুরাবে না:
public class HeapOomFixed {
public static void main(String[] args) {
for (int i = 0; i < 10_000; i++) {
byte[] data = new byte[1024 * 1024]; // 1 MB
// ডেটা নিয়ে কাজ করুন (এখানে শুধু সিমুলেশন)
process(data);
// ডেটা স্কোপের বাইরে যাচ্ছে, GC ফ্রি করতে পারবে
// data = null; (লিখতে হবে না)
}
System.out.println("প্রক্রিয়াকরণ সম্পন্ন, কোন OutOfMemoryError হয়নি।");
}
private static void process(byte[] data) {
// ডামি প্রসেসিং
for (int j = 0; j < data.length; j += 1000) {
data[j] = (byte) (j % 256);
}
}
}
এখানে প্রতিটি byte অ্যারে শুধুমাত্র লুপ ইটারেশনের ভিতরে জীবিত থাকে এবং লুপ শেষে গারবেজ কালেক্টরের জন্য উপলব্ধ হয়। ফলে হিপ পূর্ণ হয় না।
৫. জেভিএম মেমরি টিউনিং
যখন লজিক্যাল মেমরি লিক দূর করা সম্ভব হয় না (যেমন বড় ইন-মেমরি ক্যাশে সত্যিই দরকার), তখন JVM-এর হিপ সেটিংস বাড়ানো যেতে পারে। তবে অন্ধভাবে -Xmx না বাড়িয়ে নিচের প্যারামিটারগুলোর সাথে পরিচিত হোন:
-Xmx— সর্বোচ্চ হিপ (যেমন-Xmx2g)-Xms— প্রারম্ভিক হিপ (সমসাময়িক রেখে দিলে রি-সাইজ ওভারহেড এড়ানো যায়)-XX:MaxMetaspaceSize— মেটাস্পেস আকার (ডিফল্ট আনলিমিটেড, প্রয়োজনে সীমাবদ্ধ করুন)-XX:MaxDirectMemorySize— ডাইরেক্ট বাফার মেমরি (ডিফল্ট-Xmxএর সমান)-XX:+UseG1GCবা-XX:+UseParallelGC— উন্নত গারবেজ কালেক্টর নির্বাচন
উৎপাদন পরিবেশে JVM মেমরি টিউনিং সর্বদা লোড টেস্ট ও মেট্রিক্স পর্যবেক্ষণের ভিত্তিতে করা উচিত।
সম্পূর্ণ উদাহরণ
নিচে একটি পূর্ণাঙ্গ রানেবল জাভা ক্লাস দেওয়া হলো যা প্রথমে ইচ্ছাকৃতভাবে OutOfMemoryError তৈরি করে এবং পরবর্তীতে একটি স্ট্রিমিং টেকনিকে একই ধরনের কাজ সম্পন্ন করে, কিন্তু মেমরি ফুরায় না। এটি পাঠকরা কপি করে রান করতে পারেন।
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class OutOfMemoryDemo {
public static void main(String[] args) throws IOException {
// প্রথম অংশ: খারাপ উদাহরণ - হিপ ওভারফ্লো
System.out.println("=== OutOfMemoryError সিমুলেশন শুরু ===");
try {
loadAllLinesIntoMemory("large_data.txt");
} catch (OutOfMemoryError e) {
System.err.println("ত্রুটি: " + e.getMessage());
System.err.println("মেমরি লিক হয়েছে — সমস্ত লাইন একসাথে মেমরিতে রাখা হয়েছে।");
}
// দ্বিতীয় অংশ: ভালো উদাহরণ - স্ট্রিম ব্যবহার
System.out.println("\n=== স্ট্রিমিং পদ্ধতি (নিরাপদ) ===");
processWithStream("large_data.txt");
}
/**
* পুরো ফাইলের কন্টেন্ট একবারে মেমরিতে পড়ে এবং লাইনগুলোর একটি লিস্ট তৈরি করে।
* বড় ফাইলের জন্য OutOfMemoryError ঘটবেই।
*/
private static void loadAllLinesIntoMemory(String filePath) throws IOException {
List<String> lines = Files.readAllLines(Paths.get(filePath));
System.out.println("লাইন সংখ্যা: " + lines.size());
// কোনো প্রসেসিং করি না, শুধু মেমরিতে রেখে দিচ্ছি
}
/**
* স্ট্রিম ব্যবহার করে ফাইল প্রক্রিয়াকরণ — একবারে কেবল একটি লাইন মেমরিতে আসে।
* ফলে মেমরি ব্যবহার অত্যন্ত কম থাকে।
*/
private static void processWithStream(String filePath) throws IOException {
long count;
try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
count = lines.peek(line -> {
// এখানে প্রতিটি লাইন নিয়ে কাজ করুন
// যেমন: লাইন প্রসেসিং, ট্রান্সফর্মেশন, ডাটাবেসে লেখা ইত্যাদি
}).count();
}
System.out.println("স্ট্রিমিং প্রক্রিয়াকরণে মোট লাইন: " + count);
}
}
প্রথম অংশ Files.readAllLines পুরো কন্টেন্ট একটি List-এ লোড করে, তাই বড় ফাইল (যেমন কয়েক গিগাবাইট) থাকলে হিপ পূর্ণ হয়ে যায়। দ্বিতীয় অংশ Files.lines একটি স্ট্রিম ফেরত দেয়, যা লাইন ধরে ধরে পড়ে; কোনো অবস্থাতেই পুরো ফাইল একসাথে মেমরিতে রাখা হয় না। এই উদাহরণ চালাতে আপনার একটি বড় টেক্সট ফাইল (large_data.txt) থাকা লাগবে; ছোট ফাইল দিয়ে পরীক্ষা করলে OutOfMemoryError নাও আসতে পারে, কিন্তু কনসেপ্ট স্পষ্ট।
সাধারণ সমস্যা ও সমাধান
জাভা প্রোগ্রামে OutOfMemoryError বিভিন্ন কারণে দেখা দেয়। নিচে সাধারণ কিছু রূপ এবং তার সুপারিশকৃত সমাধান দেওয়া হলো:
-
java.lang.OutOfMemoryError: Java heap space- কারণ: অ্যাপ্লিকেশন খুব বেশি অবজেক্ট তৈরি করছে অথবা অপ্রয়োজনীয় রেফারেন্স ধরে রেখেছে (মেমরি লিক)।
- সমাধান: হিপ ডাম্প বিশ্লেষণ করে মেমরি লিক খুঁজুন, দুর্বল রেফারেন্স (
WeakReference) ব্যবহার করুন, সঠিকভাবে রিসোর্স ক্লোজ করুন, অথবা-Xmxবাড়িয়ে পর্যাপ্ত হিপ দিন। তবে হিপ বাড়ানোর আগে নিশ্চিত হোন যে লিকটি সমাধান করা হয়েছে।
-
OutOfMemoryError: Metaspace- কারণ: ডায়নামিক ক্লাস লোডিং (যেমন অনেক প্রক্সি ক্লাস, Reflection, CGLIB) অতিরিক্ত মেটাস্পেস পূর্ণ করছে।
- সমাধান:
-XX:MaxMetaspaceSizeসেট করে উপরের সীমা দিন। মেটাস্পেস ডাম্প নিয়ে কোন ক্লাসলোডার দায়ী তা দেখুন; অপ্রয়োজনীয় ক্লাস আনলোড করতে টমিং অপ্টিমাইজ করুন অথবা কাস্টম ক্লাসলোডারের রেফারেন্স মুক্ত করুন।
-
OutOfMemoryError: GC overhead limit exceeded- কারণ: গারবেজ কালেক্টর মোট সময়ের ৯৮% সময় কাজ করেও ২% এর কম মেমরি ফ্রি করতে পারছে। এটি একটি প্রতিরক্ষামূলক ত্রুটি।
- সমাধান: অ্যাপ্লিকেশন কোডে অবজেক্ট বরাদ্দ কমান, লাইভ অবজেক্টের আকার বিশ্লেষণ করুন, প্রয়োজনে হিপ সাইজ বাড়ান, অথবা ভিন্ন GC (যেমন G1GC) ব্যবহার করুন।
-
OutOfMemoryError: Direct buffer memory- কারণ: NIO
ByteBuffer.allocateDirect()অতিরিক্ত কল করে অফ-হিপ মেমরি শেষ হয়ে যাচ্ছে। - সমাধান: প্রতিটি ডাইরেক্ট বাফার ব্যবহারের পর ম্যানুয়ালি
((DirectBuffer) buffer).cleaner().clean()কল করুন (অথবাCleanerরেফারেন্স ব্যবহার) অথবা বাফার পুলিং ব্যবহার করুন। JVM আর্গুমেন্ট হিসেবে-XX:MaxDirectMemorySize=512mদিয়ে সীমা নির্ধারণ করুন।
- কারণ: NIO
-
OutOfMemoryError: unable to create new native thread- কারণ: খুব বেশি থ্রেড তৈরি হচ্ছে বা অপারেটিং সিস্টেমের প্রতি প্রসেস থ্রেড সীমায় পৌঁছে গেছে।
- সমাধান: এক্সিকিউটর সার্ভিস এবং ফিক্সড থ্রেড পুল ব্যবহার করে থ্রেড সংখ্যা সীমিত করুন।
-Xss(স্ট্যাক সাইজ) কমিয়ে দিন যাতে প্রতি থ্রেডের জন্য কম নেটিভ মেমরি লাগে। সিস্টেমেরulimit -uচেক করুন এবং প্রয়োজনে বাড়ান।
সারসংক্ষেপ
OutOfMemoryError জাভা ডেভেলপারদের জন্য প্রাথমিকভাবে ভীতিকর মনে হলেও, এটি শুধু একটি ট্রেসেবল মেমরি সমস্যার ইঙ্গিত। আমরা শিখেছি JVM-এর হিপ, মেটাস্পেস, ডাইরেক্ট মেমরি এবং থ্রেড রিসোর্স কিভাবে পূর্ণ হয়ে যায় এবং প্রতিটি ক্ষেত্রে নির্দিষ্ট ত্রুটি বার্তা আমাদের সমস্যার উৎস সনাক্ত করতে সাহায্য করে। ধাপে ধাপে আমরা একটি হিপ স্পেস ত্রুটি সিমুলেট করেছি, হিপ ডাম্প বিশ্লেষণের গুরুত্ব বুঝেছি, এবং সর্বোপরি স্ট্রিমিং ও সঠিক রিসোর্স ব্যবস্থাপনার মাধ্যমে স্থায়ী সমাধান দেখেছি।
মনে রাখবেন, প্রোডাকশনে OutOfMemoryError ঠেকাতে প্রতিনিয়ত মেমরি মনিটরিং, পর্যায়ক্রমিক হিপ ডাম্প বিশ্লেষণ এবং অটোমেটেড টেস্টিং অপরিহার্য। যেকোনো সংশোধন করার আগে সমস্যার মূল কারণ বোঝা জরুরি – শুধুমাত্র হিপ সাইজ না বাড়িয়ে স্মার্ট কোডিং প্র্যাকটিসই দীর্ঘমেয়াদী সমাধান।