การจัดการหน่วยความจำใน Java

⚡ สรุปอย่างชาญฉลาด

การจัดการหน่วยความจำใน Java อธิบายว่า JVM แบ่งหน่วยความจำขณะรันไทม์อย่างไร โดยแบ่งเป็น Stack, Heap, Codeและส่วนต่างๆ ของหน่วยความจำแบบคงที่ วิธีที่การอ้างอิงวัตถุไหลเวียนระหว่างการเรียกใช้เมธอด และวิธีที่ตัวเก็บขยะ (Garbage Collector) เรียกคืนวัตถุที่ไม่สามารถเข้าถึงได้ เพื่อให้แอปพลิเคชันทำงานได้อย่างเสถียรและปราศจากปัญหาหน่วยความจำรั่วไหล

  • 🧠 โครงสร้างหน่วยความจำของ JVM: Java หน่วยความจำถูกแบ่งออกเป็นฮีปสำหรับอ็อบเจ็กต์ สแต็กสำหรับเฟรมเมธอดและตัวแปรโลคอล รวมถึงพื้นที่เฉพาะสำหรับไบต์โค้ดและข้อมูลคงที่
  • 📚 Stack กับ Heap: เฟรมสแต็กจะเรียงลำดับตามหลักการเข้าหลังออกก่อน (Last-In-First-Out) และจัดเก็บค่าพื้นฐานและค่าอ้างอิง ในขณะที่ฮีปจะเก็บทุกอ็อบเจ็กต์ที่สร้างขึ้นด้วยตัวดำเนินการ `new`
  • ♻️ การเก็บขยะ: คนเก็บขยะ tracการเข้าถึง ks ผ่านทางสายโซ่การอ้างอิง และเรียกคืนหน่วยความจำจากวัตถุที่ไม่มีเธรดใดเข้าถึงได้โดยอัตโนมัติ
  • นักสะสมยุคใหม่: ปริมาณงานด้านการผลิตพึ่งพา G1, ZGC และ Shenandoah มากขึ้นเรื่อยๆ สำหรับการรวบรวมขยะที่มีช่วงเวลาหยุดชะงักน้อยที่สุดในหน่วยความจำขนาดหลายกิกะไบต์
  • 🧪 ตัวอย่างการลงมือปฏิบัติจริง: การสาธิตการใช้งานคลาส Student แบบทีละขั้นตอนจะแสดงให้เห็นว่าเมื่อใดที่การอ้างอิงจะพร้อมสำหรับการรวบรวม และการทำให้การอ้างอิงเป็นค่าว่างจะปลดปล่อยวัตถุที่อยู่เบื้องหลังได้อย่างไร

การจัดการหน่วยความจำใน Java

หน่วยความจำแบบ Stack คืออะไรใน Java?

หน่วยความจำแบบเรียงซ้อนใน Java Stack คือส่วนของหน่วยความจำ JVM ที่เก็บเฟรมของเมธอด ตัวแปรโลคอล และตัวแปรอ้างอิงสำหรับแต่ละเธรด การเข้าถึง Stack จะทำในลำดับ Last-In-First-Out เสมอ ดังนั้นเมธอดที่ถูกเรียกใช้ล่าสุดจะอยู่ด้านบนสุด และตัวแปรโลคอลของเมธอดนั้นจะถูกผลักและดึงออกจาก Stack พร้อมกับเฟรม

แต่ละเส้นด้ายใน Java เครื่องเสมือน (Virtual Machine) จะมี Stack เป็นของตัวเอง ซึ่งทำให้การเรียกใช้เมธอดแยกออกจากกันและปลอดภัยต่อการทำงานแบบมัลติเธรดโดยธรรมชาติ ตัวแปรโลคอลพื้นฐาน เช่น int, boolean และ double จะถูกเก็บไว้ภายในเฟรมโดยตรง ในขณะที่การอ้างอิงวัตถุที่เก็บไว้ใน Stack จะชี้ไปยังวัตถุที่จัดสรรไว้ใน Heap

หน่วยความจำฮีปคืออะไรใน Java?

หน่วยความจำฮีป (Heap memory) คือพื้นที่ส่วนกลางของ JVM ที่ใช้เก็บข้อมูลทุกอย่าง Java อ็อบเจ็กต์และอาร์เรย์ที่สร้างขึ้นด้วยตัวดำเนินการ `new` รวมถึงตัวแปรอ้างอิงใดๆ ที่เป็นของอ็อบเจ็กต์เหล่านั้นในฐานะฟิลด์อินสแตนซ์ ต่างจาก Stack ตรงที่ Heap นั้นใช้ร่วมกันในทุกเธรด ซึ่งเป็นเหตุผลว่าทำไมการเข้าถึงอ็อบเจ็กต์จึงมักต้องใช้การซิงโครไนซ์

ฮีป (Heap) คือพื้นที่ที่ตัวเก็บขยะ (Garbage Collector) จัดการ JVM รุ่นใหม่ๆ ที่ใช้ HotSpot จะแบ่งฮีปออกเป็น Young Generation สำหรับวัตถุที่มีอายุสั้น และ Old Generation สำหรับวัตถุที่มีอายุยาวนาน โดยมีเมตาเดตาของคลาสจัดเก็บไว้ในพื้นที่เฉพาะที่เรียกว่า Metaspace

การจัดสรรหน่วยความจำใน Java

การจัดสรรหน่วยความจำใน Java Heap คือกระบวนการที่ JVM จัดสรรพื้นที่หน่วยความจำเสมือนสำหรับตัวแปรและอินสแตนซ์ของคลาสและโครงสร้างระหว่างการทำงานของโปรแกรม หน่วยความจำไม่ได้ถูกจัดสรรให้กับวัตถุเมื่อมีการประกาศ แต่จะสร้างเพียงการอ้างอิงเท่านั้น การจัดสรรวัตถุจริงเกิดขึ้นผ่านตัวดำเนินการ `new` ดังนั้นทุกวัตถุจึงอยู่ใน Heap

การขอ Java การจัดสรรหน่วยความจำแบ่งออกเป็นส่วนต่างๆ ดังต่อไปนี้:

  1. กอง
  2. กอง
  3. Code
  4. คงที่

การแบ่งหน่วยความจำนี้จำเป็นสำหรับการจัดการระบบรันไทม์อย่างมีประสิทธิภาพ

  • การขอ Code ส่วนนี้ประกอบด้วยข้อมูลที่คุณรวบรวมไว้ รหัสไบต์.
  • การขอ กอง ร้านค้าแผนก วิธีการ ตัวแปรท้องถิ่น และตัวแปรอ้างอิง.
  • การขอ กอง ส่วนประกอบด้วย วัตถุ และอาจมีตัวแปรอ้างอิงที่จัดเก็บไว้ในรูปแบบฟิลด์ของอินสแตนซ์ด้วย
  • การขอ คงที่ ส่วนนี้ถือ ข้อมูลคงที่และเมธอดคงที่ ใช้ร่วมกันในทุกอินสแตนซ์

ความแตกต่างระหว่างตัวแปรท้องถิ่นและอินสแตนซ์

การเข้าใจว่าตัวแปรแต่ละประเภทอยู่ที่ใดใน JVM จะช่วยอธิบายพฤติกรรมของ Stack และ Heap ได้ ตัวแปรอินสแตนซ์ มีการประกาศ ภายในคลาสแต่ภายนอกเมธอดใดๆและมันยังคงดำรงอยู่บนกองขยะนั้นในฐานะส่วนหนึ่งของวัตถุนั้น

class Student{ 
int num; // num is instance variable 
public void showData{}

A ตัวแปรท้องถิ่น มีการประกาศ ภายในเมธอด รวมถึงอาร์กิวเมนต์ของเมธอดด้วยและดำรงอยู่บน Stack ภายในเฟรมที่ใช้งานอยู่

public void sum(int a){

int x = a + 3;

// a, x are local variables

}

ความแตกต่างระหว่างสแต็คและฮีป

Stack และ Heap แก้ปัญหาที่แตกต่างกันภายใน JVM Stack ช่วยให้แต่ละเธรดจัดสรรพื้นที่ได้อย่างรวดเร็วและแน่นอนสำหรับข้อมูลที่มีอายุสั้นซึ่งผูกติดกับขอบเขตของเมธอด ในขณะที่ Heap ให้พื้นที่ส่วนกลางสำหรับวัตถุที่มีอายุยาวนานซึ่งเธรดใด ๆ ก็สามารถอ้างอิงได้ วิดีโอสั้น ๆ ด้านล่างนี้สรุปความแตกต่างก่อนที่จะมีการอธิบายรายละเอียดต่อไป

คลิก Good Farm Animal Welfare Awards หากไม่สามารถเข้าถึงวิดีโอได้

เพื่อให้เข้าใจการทำงานร่วมกันของ Stack และ Heap ลองพิจารณาเมธอด main ที่เรียกใช้เมธอด m1

public void m1{
int x = 20;
}

ใน JVM Stack จะมีการสร้างเฟรมสำหรับเมธอด m1

Java กองและกอง

ตัวแปร x ใน m1 ถูกสร้างขึ้นในเฟรมสำหรับ m1 บน Stack ดังแสดงในภาพด้านล่าง

Java กองและกอง

จากนั้นเมธอด m1 จะเรียกเมธอด m2 ในสแต็ก จะมีการสร้างเฟรมใหม่สำหรับ m2 ทับเฟรมสำหรับ m1

Java กองและกอง

Java กองและกอง

ตัวแปรโลคอล b และ c ถูกสร้างขึ้นภายในเฟรมสำหรับ m2 บน Stack ด้วยเช่นกัน

public void m2(int b){
boolean c;
}

ถัดไป m2 จะเรียกเมธอด m3 อีกครั้ง โดยจะมีการสร้างเฟรมสำหรับ m3 ไว้ที่ด้านบนสุดของ Stack ดังแสดงในภาพด้านล่าง

Java กองและกอง

Java กองและกอง

สมมติว่าเมธอด m3 สร้างอ็อบเจ็กต์สำหรับคลาส Account ซึ่งมีสองส่วน ตัวแปรอินสแตนซ์ int p และ int q

class Account {
    int p;
    int q;
}

นี่คือโค้ดสำหรับเมธอด m3

public void m3(){
    Account ref = new Account();
    // more code
}

คำสั่ง `new Account()` สร้างอ็อบเจ็กต์ของ `Account` บนฮีป (Heap)

Java กองและกอง

ตัวแปรอ้างอิง ref ถูกสร้างขึ้นบน Stack ภายในเฟรมสำหรับ m3

Java กองและกอง

ตัวดำเนินการกำหนดค่าจะทำให้ตัวแปรอ้างอิงชี้ไปยังอ็อบเจ็กต์บนฮีป

Java กองและกอง

เมื่อเมธอดทำงานเสร็จสิ้น การควบคุมจะกลับไปยังเมธอดที่เรียก ซึ่งในกรณีนี้คือเมธอด m2

Java กองและกอง

เฟรมสำหรับเมธอด m3 ถูกล้างออกจากสแต็กแล้ว

Java กองและกอง

เนื่องจากตัวแปรอ้างอิงไม่ได้ชี้ไปยังอ็อบเจ็กต์บนฮีปอีกต่อไป อ็อบเจ็กต์นั้นจึงมีสิทธิ์ได้รับการเก็บกวาดโดยระบบจัดการหน่วยความจำ (garbage collection)

Java กองและกอง

เมื่อเมธอด m2 ทำงานเสร็จสิ้น เมธอดนั้นจะถูกดึงออกจากสแต็ก และตัวแปรทั้งหมดจะไม่สามารถใช้งานได้อีกต่อไป เช่นเดียวกับเมธอด m1 และในที่สุดการควบคุมจะกลับไปยังเมธอดหลัก

แล้วถ้าอ็อบเจ็กต์นั้นเก็บการอ้างอิงอื่นไว้เป็นตัวแปรอินสแตนซ์ล่ะ?

public static void main(String args[]) {
    A parent = new A();
    // more code
}
class A {
    B child = new B();
    int e;
}
class B {
    int c;
    int d;
}

ในกรณีนี้ ตัวแปรอ้างอิง child จะอยู่บนฮีปเป็นส่วนหนึ่งของอ็อบเจ็กต์ A และชี้ไปยังอ็อบเจ็กต์ B ของตัวเอง ดังแสดงในภาพด้านล่าง

Java กองและกอง

Garbage Collection อยู่ในอะไร Java?

เก็บขยะใน Java Garbage Collector คือกระบวนการที่ JVM จัดการหน่วยความจำโดยอัตโนมัติ Garbage Collector จะค้นหาวัตถุที่ไม่สามารถเข้าถึงได้อีกต่อไปจากการอ้างอิงใดๆ และเรียกคืนหน่วยความจำนั้น การจัดสรรหน่วยความจำแบบไดนามิกเกิดขึ้นผ่านตัวดำเนินการ `new` และหน่วยความจำนั้นจะยังคงถูกจัดสรรไว้จนกว่าโปรแกรมจะไม่ถือการอ้างอิงถึงวัตถุนั้นอีกต่อไป

เมื่อไม่มีการอ้างอิงถึงวัตถุนั้นอีกต่อไป วัตถุนั้นจะถือว่าไม่จำเป็นอีกต่อไป และสามารถเรียกคืนหน่วยความจำที่วัตถุนั้นครอบครองได้ ไม่จำเป็นต้องทำลายวัตถุอย่างชัดเจนเพราะ Java จัดการการคืนหน่วยความจำโดยอัตโนมัติผ่านตัวเก็บขยะ (Garbage Collector)

เทคนิคเบื้องหลังเรื่องนี้เรียกว่า... เก็บขยะโปรแกรมที่ไม่สามารถปล่อยหน่วยความจำได้ในที่สุดจะหยุดทำงานเมื่อไม่มีหน่วยความจำเหลือให้จัดสรรอีกต่อไป โปรแกรมประเภทนี้เรียกว่ามี หน่วยความจำรั่ว. เก็บขยะใน Java ทำงานโดยอัตโนมัติ ตลอดอายุการใช้งานของโครงการ ซึ่งจะช่วยลดภาระในการจัดสรรทรัพยากรด้วยตนเองและลดความเสี่ยงของการรั่วไหล

ในภาษา C ในทางตรงกันข้าม โปรแกรมเมอร์มีหน้าที่รับผิดชอบในการปล่อยหน่วยความจำที่จัดสรรแบบไดนามิกผ่านฟังก์ชัน free() นี่คือจุดที่ Java การจัดการหน่วยความจำถือเป็นข้อได้เปรียบที่สำคัญ

JVM รุ่นใหม่ๆ ของ HotSpot มาพร้อมกับ Garbage Collector หลายตัวที่ปรับแต่งมาสำหรับภาระงานที่แตกต่างกัน Garbage Collector G1 ที่เป็นค่าเริ่มต้นนั้นมุ่งเน้นที่ความสมดุลระหว่างปริมาณงานและเวลาหยุดชั่วคราวบนฮีปขนาดหลายกิกะไบต์ ในขณะที่ ZGC และ Shenandoah มุ่งเป้าไปที่เวลาหยุดชั่วคราวต่ำกว่ามิลลิวินาทีบนฮีปขนาดใหญ่มาก ซึ่งทำให้พวกมันมีประสิทธิภาพที่...tracมีประสิทธิภาพสำหรับบริการที่ไวต่อความหน่วงแฝง การเลือกตัวเก็บรวบรวมข้อมูลที่เหมาะสมและการปรับแต่งขนาดของ Young Generation, Old Generation และ Metaspace เป็นทักษะหลักสำหรับ Java งานแสดง.

หมายเหตุ วัตถุทั้งหมดถูกสร้างขึ้นในส่วนของหน่วยความจำที่เรียกว่า Heap ซึ่งเป็นบริเวณที่ Garbage Collector จัดการ

ตัวอย่าง: เรียนรู้กลไกการเก็บขยะใน Java

คู่มือนี้แสดงให้เห็นว่าเมื่อใดที่การอ้างอิงต่างๆ จะมีสิทธิ์ได้รับการลบทิ้งโดยระบบ (Garbage Collection) ภายในโปรแกรมอย่างง่าย

ขั้นตอน 1) คัดลอกโค้ดต่อไปนี้ลงในโปรแกรมแก้ไข

class Student{
int a;
int b;
  public void setData(int c,int d){
    a=c;
    b=d;
  }
  public void showData(){
    System.out.println("Value of a = "+a);
    System.out.println("Value of b = "+b);
  }
  public static void main(String args[]){
    Student s1 = new Student();
    Student s2 = new Student();
    s1.setData(1,2);
    s2.setData(3,4);
    s1.showData();
    s2.showData();
    //Student s3;
    //s3=s2;
    //s3.showData();
    //s2=null;
    //s3.showData();
    //s3=null;
    //s3.showData();
  }
}

ขั้นตอน 2) บันทึก คอมไพล์ และรันโค้ด ดังแสดงในแผนภาพ จะมีการสร้างอ็อบเจ็กต์สองตัวและตัวแปรอ้างอิงสองตัว

 กลไกการเก็บขยะ

ขั้นตอน 3) ลบเครื่องหมายคอมเมนต์ออกจากบรรทัดที่ 20, 21 และ 22 บันทึก คอมไพล์ และรันโค้ด

ขั้นตอน 4) ดังแสดงในแผนภาพด้านล่าง ตัวแปรอ้างอิงสองตัวชี้ไปยังวัตถุเดียวกันแล้ว

กลไกการเก็บขยะ

ขั้นตอน 5) ลบเครื่องหมายคอมเมนต์ออกจากบรรทัดที่ 23 และ 24 บันทึก คอมไพล์ และรันโค้ด

ขั้นตอน 6) ดังแสดงในภาพด้านล่าง s2 จะกลายเป็นค่าว่าง แต่ s3 ยังคงชี้ไปยังวัตถุนั้นอยู่ ดังนั้นวัตถุนั้นจึงยังไม่พร้อมสำหรับการเก็บกวาดขยะ (Garbage Collection)

 กลไกการเก็บขยะ

ขั้นตอน 7) ลบเครื่องหมายคอมเมนต์ออกจากบรรทัดที่ 25 และ 26 บันทึก คอมไพล์ และรันโค้ด

ขั้นตอน 8) ณ จุดนี้ ไม่มีข้อมูลอ้างอิงใดชี้ไปยังวัตถุนั้นอีกแล้ว ดังนั้นวัตถุนั้นจึงมีสิทธิ์ได้รับการเก็บกวาดโดยระบบจัดการขยะ (Garbage Collection) ระบบจัดการขยะจะลบวัตถุนั้นออกจากหน่วยความจำ และไม่มีวิธีใดที่จะเรียกคืนวัตถุนั้นได้อีก

 เรียนรู้การเก็บขยะ

วิธีการลบวัตถุใน Java?

Java ไม่มีตัวดำเนินการลบด้วยตนเอง ดังนั้นวิธีการมาตรฐานคือการลบการอ้างอิงทั้งหมดไปยังวัตถุนั้น เพื่อให้ตัวเก็บขยะสามารถเรียกคืนวัตถุนั้นได้

1) เพื่อให้วัตถุพร้อมสำหรับการเก็บขยะ (Garbage Collection) ให้กำหนดค่าตัวแปรอ้างอิงทุกตัวที่ชี้ไปยังวัตถุนั้นเป็นค่าว่าง (null)

2) ประเภทข้อมูลพื้นฐานไม่ใช่วัตถุ ดังนั้นจึงไม่สามารถกำหนดค่าเป็น null ได้ พื้นที่จัดเก็บจะถูกเรียกคืนโดยอัตโนมัติเมื่อเฟรมสแต็กโดยรอบถูกดึงออก

วิธีการลบวัตถุใน Java

คำถามที่พบบ่อย

Stack ทำหน้าที่จัดเก็บเฟรมของเมธอด ตัวแปรโลคอล และการอ้างอิงตามลำดับ Last-In-First-Out (LIFO) สำหรับแต่ละเธรด ส่วน Heap ทำหน้าที่จัดเก็บอ็อบเจ็กต์ทั้งหมดที่สร้างขึ้นด้วยคำสั่ง new และใช้ร่วมกันระหว่างเธรด ซึ่งเป็นเหตุผลที่ Garbage Collector ทำหน้าที่จัดการมัน

ตัวเก็บขยะ (Garbage Collector) จะตรวจสอบสายโซ่การอ้างอิงจากรากของ GC เช่น เธรดที่ทำงานอยู่และฟิลด์คงที่ วัตถุใดๆ ที่ไม่สามารถเข้าถึงได้จากรากจะถือว่าไม่สามารถเข้าถึงได้และจะพร้อมสำหรับการเรียกคืนในรอบการเก็บขยะครั้งถัดไป

กลุ่ม Young Generation เก็บวัตถุที่มีอายุสั้นในพื้นที่ Eden และ Survivor ส่วนกลุ่ม Old Generation เก็บวัตถุที่อยู่รอดได้หลายคอลเลกชัน Metaspace เป็นพื้นที่หน่วยความจำดั้งเดิมที่เก็บข้อมูลเมตาของคลาส และเข้ามาแทนที่ PermGen เดิม Java 8.

G1 เป็นค่าเริ่มต้นและเหมาะกับเวิร์กโหลดส่วนใหญ่บนฮีปขนาดหลายกิกะไบต์ ZGC และ Shenandoah กำหนดเป้าหมายการหยุดชั่วคราวที่น้อยกว่ามิลลิวินาทีบนฮีปขนาดใหญ่มาก และเป็นตัวเลือกที่ดีสำหรับบริการที่ไวต่อความหน่วงแฝงซึ่งทำงานบนฮีปขนาดใหญ่ Java 17 หรือใหม่กว่า

กำหนดค่าอ้างอิงทั้งหมดที่ชี้ไปยังวัตถุนั้นให้เป็นค่าว่าง หรือปล่อยให้ค่าอ้างอิงเหล่านั้นอยู่นอกขอบเขตเมื่อเฟรมสแต็กถูกดึงออก เมื่อไม่มีค่าอ้างอิงที่ยังใช้งานได้ไปถึงวัตถุนั้นแล้ว ตัวเก็บขยะจะเรียกคืนหน่วยความจำฮีปของวัตถุนั้น

ใช่แล้ว เครื่องมือปรับแต่งที่ขับเคลื่อนด้วย AI จะวิเคราะห์บันทึก GC อัตราการจัดสรร และฮิสโตแกรมการหยุดชั่วคราว เพื่อแนะนำขนาดฮีป แฟล็กตัวเก็บขยะ และอัตราส่วน Young Generation ซึ่งจะช่วยลดระยะเวลาของวงจรการตอบรับเมื่อเทียบกับการปรับแต่งด้วยตนเอง โดยเฉพาะอย่างยิ่งในเวิร์กโหลด G1, ZGC และ Shenandoah

แพลตฟอร์มการตรวจสอบด้วย AI จะรับข้อมูล heap dump และเมตริกแบบเรียลไทม์ จากนั้นจัดกลุ่มกราฟอ็อบเจ็กต์ที่เก็บรักษาไว้เพื่อระบุคลาสและสายโซ่การอ้างอิงที่น่าสงสัย ซึ่งจะช่วยให้พบผู้ที่มีแนวโน้มจะรั่วไหลได้เร็วกว่าการวิเคราะห์ heap dump ด้วยตนเอง และช่วยให้ทีมระบุสาเหตุที่แท้จริงในสภาพแวดล้อมการผลิตได้

สรุปโพสต์นี้ด้วย: