ทำความเข้าใจกับหน่วยความจำใน JVM

ดังที่คุณทราบแล้วว่า JVM รันโปรแกรม Java ภายในตัวมันเอง เช่นเดียวกับเครื่องเสมือนอื่น ๆ มีระบบการจัดการหน่วยความจำของตัวเอง

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

ทำความเข้าใจกับหน่วยความจำใน JVM

สำคัญ! โมเดล Java ดั้งเดิมนั้นไม่ดีพอ ดังนั้นจึงมีการแก้ไขใน Java 1.5 เวอร์ชันนี้ใช้มาจนถึงทุกวันนี้ (Java 14+)

กองกระทู้

โมเดลหน่วยความจำ Java ที่ใช้ภายในโดย JVM แบ่งหน่วยความจำออกเป็นเธรดสแต็กและฮีป มาดูโมเดลหน่วยความจำ Java ซึ่งแบ่งออกเป็นบล็อกอย่างมีเหตุผล:

กองกระทู้

เธรดทั้งหมดที่ทำงานใน JVM มีสแต็กของตัวเอง ในทางกลับกันสแต็กจะเก็บข้อมูลเกี่ยวกับเมธอดที่เธรดเรียกใช้ ฉันจะเรียกสิ่งนี้ว่า "คอลสแตก" call stack ดำเนินการต่อทันทีที่เธรดรันโค้ด

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

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

กอง

ฮีปประกอบด้วยวัตถุทั้งหมดที่สร้างขึ้นในแอปพลิเคชันของคุณ โดยไม่คำนึงว่าเธรดใดที่สร้างวัตถุนั้น ซึ่งรวมถึงการห่อหุ้มประเภทดั้งเดิม (เช่นByte , Integer , Longและอื่น ๆ ) ไม่สำคัญว่าออบเจ็กต์จะถูกสร้างขึ้นและกำหนดให้กับตัวแปรโลคัลหรือสร้างเป็นตัวแปรสมาชิกของออบเจ็กต์อื่น อ็อบเจ็กต์จะถูกเก็บไว้ในฮีป

ด้านล่างนี้เป็นไดอะแกรมที่แสดง call stack และตัวแปรโลคอล (เก็บไว้ใน stack) รวมถึงอ็อบเจกต์ (เก็บไว้ใน heap):

กอง

ในกรณีที่ตัวแปรโลคัลเป็นประเภทดั้งเดิม ตัวแปรนั้นจะถูกเก็บไว้ในสแต็กของเธรด

ตัวแปรโลคัลยังสามารถอ้างอิงถึงวัตถุ ในกรณีนี้ การอ้างอิง (ตัวแปรโลคัล) จะถูกจัดเก็บไว้ในเธรดสแต็ก แต่ตัวออบเจกต์นั้นถูกจัดเก็บไว้ในฮีป

วัตถุมีเมธอด เมธอดเหล่านี้มีตัวแปรโลคัล ตัวแปรโลคัลเหล่านี้ยังถูกจัดเก็บไว้ในเธรดสแต็ก แม้ว่าอ็อบเจ็กต์ที่เป็นเจ้าของเมธอดจะถูกเก็บไว้ในฮีปก็ตาม

ตัวแปรสมาชิกของอ็อบเจกต์จะถูกเก็บไว้ในฮีปพร้อมกับตัวอ็อบเจ็กต์เอง สิ่งนี้เป็นจริงทั้งเมื่อตัวแปรสมาชิกเป็นประเภทดั้งเดิมและเมื่อเป็นการอ้างอิงวัตถุ

ตัวแปรคลาสคงที่จะถูกเก็บไว้ในฮีปพร้อมกับคำจำกัดความของคลาส

ปฏิสัมพันธ์กับวัตถุ

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

การโต้ตอบกับวัตถุ (ฮีป)

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

โปรดทราบว่าทั่วไปวัตถุ 3มีลิงค์ไปยังวัตถุ 2และวัตถุ 4เป็นตัวแปรสมาชิก (แสดงด้วยลูกศร) ผ่านลิงก์เหล่านี้ สามารถเข้าถึงสองเธรดได้วัตถุ 2และวัตถุ4.

ไดอะแกรมยังแสดงตัวแปรโลคัล (ตัวแปรท้องถิ่น 1จากmethodTwo ) แต่ละสำเนามีการอ้างอิงที่แตกต่างกันซึ่งชี้ไปยังวัตถุสองชิ้นที่แตกต่างกัน (วัตถุ 1และวัตถุ 5) และไม่ใช่อันเดียวกัน ตามทฤษฎีแล้ว ทั้งสองเธรดสามารถเข้าถึงได้ทั้งสองอย่างวัตถุ 1, เพื่อที่จะวัตถุ 5หากมีการอ้างอิงถึงวัตถุทั้งสองนี้ แต่ในแผนภาพด้านบน แต่ละเธรดมีการอ้างอิงถึงหนึ่งในสองวัตถุเท่านั้น

ตัวอย่างของการโต้ตอบกับวัตถุ

มาดูกันว่าเราจะสาธิตการทำงานในโค้ดได้อย่างไร:

public class MySomeRunnable implements Runnable() {

    public void run() {
        one();
    }

    public void one() {
        int localOne = 1;

        Shared localTwo = Shared.instance;

        //... do something with local variables

        two();
    }

    public void two() {
        Integer localOne = 2;

        //... do something with local variables
    }
}
public class Shared {

    // store an instance of our object in a variable

    public static final Shared instance = new Shared();

    // member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);
}

เมธอดrun()เรียก เมธอด one()และone() เรียก two()ในทางกลับกัน

เมธอดone()ประกาศตัวแปรท้องถิ่นดั้งเดิม (localOne) ของประเภทintและตัวแปรท้องถิ่น (localTwo) ซึ่งเป็นการอ้างอิงถึงวัตถุ

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

แต่ละเธรดที่ดำเนินการเมธอดone()จะสร้างสำเนาของตัวเองด้วยlocalTwo. อย่างไรก็ตาม สำเนาสองฉบับที่แตกต่างกันlocalTwoจบลงด้วยการชี้ไปที่วัตถุเดียวกันบนฮีป ความจริงก็คือว่าlocalTwoชี้ไปที่วัตถุที่อ้างอิงโดยตัวแปรคงที่ตัวอย่าง. มีสำเนาของตัวแปรสแตติกเพียงสำเนาเดียว และสำเนานั้นจะถูกเก็บไว้ในฮีป

ดังนั้นสำเนาทั้งสองlocalTwoจบลงด้วยการชี้ไปที่ อินส แตน ซ์ ที่ใช้ร่วม กันเดียวกัน อินสแตนซ์ที่ใช้ร่วมกันจะถูกเก็บไว้ในฮีปด้วย มันเข้ากันวัตถุ 3ในแผนภาพด้านบน

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

โปรดทราบว่า เมธอด two()สร้างตัวแปรโลคัลชื่อlocalOne. ตัวแปรโลคัล นี้เป็นการอ้างอิงถึงออบเจกต์ประเภทInteger วิธีการกำหนดลิงค์localOneเพื่อชี้ไปที่อินสแตน ซ์จำนวนเต็ม ใหม่ ลิงค์จะถูกเก็บไว้ในสำเนาlocalOneสำหรับแต่ละเธรด อินสแตน ซ์จำนวนเต็ม สอง อินสแตนซ์ จะถูกเก็บไว้ในฮีป และเนื่องจากเมธอดนี้สร้างออบเจกต์จำนวนเต็มใหม่ทุกครั้งที่ดำเนินการ เธรดสองเธรดที่ดำเนินการเมธอดนี้จะสร้างอินส แตนซ์จำนวนเต็มแยกกัน พวกเขาตรงกันวัตถุ 1และวัตถุ 5ในแผนภาพด้านบน

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